modul-e / src /pages /Settings.tsx
sitcod3's picture
Upload src/pages/Settings.tsx with huggingface_hub
0c4dec5 verified
Raw History Blame Contribute Delete
20.1 kB
import {
KeyRound,
Plus,
Trash2,
Copy,
CheckCircle2,
AlertCircle,
ShieldCheck,
Sparkles,
Globe,
Clock,
Sliders,
Terminal,
Activity,
Database
} from "lucide-react";
import { useState, useEffect } from "react";
import { useApiKey } from "../context/ApiKeyContext";
import { apiClient } from "../lib/apiClient";
import FormDrawer from "../components/FormDrawer";
import ConfirmModal from "../components/ConfirmModal";
import toast from "react-hot-toast";
import { supabase, isSupabaseConfigured, supabaseProjectRef, supabaseDashboardUrl } from "../lib/supabaseClient";
export default function Settings() {
const { apiKeys, activeKeyId, setActiveKeyId, addApiKey, removeApiKey, renameApiKey } = useApiKey();
const [activeTab, setActiveTab] = useState<"keys" | "preferences" | "sandbox" | "database">("keys");
const [isAddKeyOpen, setIsAddKeyOpen] = useState(false);
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
const [testResult, setTestResult] = useState<{ status: "idle" | "testing" | "success" | "error"; message?: string; latency?: number }>({ status: "idle" });
const [supabaseStatus, setSupabaseStatus] = useState<"testing" | "connected" | "error" | "unconfigured">("testing");
useEffect(() => {
async function checkSupabase() {
if (!isSupabaseConfigured() || !supabase) {
setSupabaseStatus("unconfigured");
return;
}
try {
// Simple ping to check if credentials are valid
// Using auth.getSession() as a lightweight check
const { error } = await supabase.auth.getSession();
if (error) throw error;
setSupabaseStatus("connected");
} catch (err) {
console.error("Supabase connection error:", err);
setSupabaseStatus("error");
}
}
if (activeTab === "database") {
checkSupabase();
}
}, [activeTab]);
const testActiveConnection = async () => {
setTestResult({ status: "testing" });
const startTime = Date.now();
try {
const res = await apiClient.get("/campaigns");
const latency = Date.now() - startTime;
setTestResult({
status: "success",
message: `Connected successfully. Response latency: ${latency}ms. Found ${res.data?.length || 0} campaigns.`,
latency,
});
toast.success("API Key verified successfully");
} catch (err: any) {
setTestResult({
status: "error",
message: err.response?.data?.error || "Failed to reach BRIDGEModulE API gateway",
});
toast.error("API connection test failed");
}
};
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold tracking-tight text-zinc-100 flex items-center gap-2.5">
<KeyRound className="w-6 h-6 text-indigo-400" />
Settings & API Credentials
</h1>
<p className="text-zinc-400 text-sm mt-1">
Manage BRIDGEModulE API keys, system preferences, and test live gateway endpoints.
</p>
</div>
{/* Tabs */}
<div className="border-b border-zinc-800 flex items-center gap-2 text-sm font-medium">
<button
onClick={() => setActiveTab("keys")}
className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
activeTab === "keys"
? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
: "border-transparent text-zinc-400 hover:text-zinc-200"
}`}
>
<KeyRound className="w-4 h-4" /> API Key Manager ({apiKeys.length})
</button>
<button
onClick={() => setActiveTab("preferences")}
className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
activeTab === "preferences"
? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
: "border-transparent text-zinc-400 hover:text-zinc-200"
}`}
>
<Sliders className="w-4 h-4" /> System Preferences
</button>
<button
onClick={() => setActiveTab("sandbox")}
className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
activeTab === "sandbox"
? "border-indigo-500 text-indigo-400 bg-indigo-500/5"
: "border-transparent text-zinc-400 hover:text-zinc-200"
}`}
>
<Terminal className="w-4 h-4" /> API Sandbox & Architecture
</button>
<button
onClick={() => setActiveTab("database")}
className={`px-4 py-2.5 border-b-2 font-medium text-xs flex items-center gap-2 transition-colors ${
activeTab === "database"
? "border-emerald-500 text-emerald-400 bg-emerald-500/5"
: "border-transparent text-zinc-400 hover:text-zinc-200"
}`}
>
<Database className="w-4 h-4" /> Supabase Connection
</button>
</div>
{/* TAB 1: API KEYS */}
{activeTab === "keys" && (
<div className="space-y-6 max-w-4xl">
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-5 shadow-sm space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-100">Configured BRIDGEModulE API Keys</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Store multiple keys for distinct environments or client workspaces.
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={testActiveConnection}
disabled={testResult.status === "testing"}
className="px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5 border border-zinc-700"
>
<Activity className="w-3.5 h-3.5 text-emerald-400" />
{testResult.status === "testing" ? "Testing..." : "Test Connection"}
</button>
<button
onClick={() => setIsAddKeyOpen(true)}
className="px-3.5 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5"
>
<Plus className="w-3.5 h-3.5" /> Add API Key
</button>
</div>
</div>
{/* Test Connection Banner */}
{testResult.status === "success" && (
<div className="p-3 bg-emerald-500/10 border border-emerald-500/20 rounded-lg flex items-center gap-2.5 text-xs text-emerald-400">
<CheckCircle2 className="w-4 h-4 shrink-0" />
<span>{testResult.message}</span>
</div>
)}
{testResult.status === "error" && (
<div className="p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg flex items-center gap-2.5 text-xs text-rose-400">
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{testResult.message}</span>
</div>
)}
{/* Keys List */}
<div className="space-y-3 pt-2">
{apiKeys.map((k) => {
const isActive = k.id === activeKeyId;
const maskedKey = k.key.length > 8
? `${k.key.substring(0, 4)}••••••••••••••••${k.key.substring(k.key.length - 4)}`
: "••••••••••••";
return (
<div
key={k.id}
className={`p-4 rounded-xl border transition-all flex flex-col sm:flex-row sm:items-center justify-between gap-4 ${
isActive
? "bg-indigo-950/20 border-indigo-500/40 shadow-sm"
: "bg-zinc-950/60 border-zinc-800/80 hover:border-zinc-700"
}`}
>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold text-xs text-zinc-100">{k.name}</span>
{isActive && (
<span className="text-[10px] font-bold uppercase tracking-wider bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full">
Active Key
</span>
)}
</div>
<div className="font-mono text-xs text-zinc-400 mt-1 flex items-center gap-2">
<span>{maskedKey}</span>
<button
onClick={() => {
navigator.clipboard.writeText(k.key);
toast.success("API key copied to clipboard");
}}
className="text-zinc-500 hover:text-zinc-300 p-1"
>
<Copy className="w-3.5 h-3.5" />
</button>
</div>
</div>
<div className="flex items-center gap-2">
{!isActive && (
<button
onClick={() => {
setActiveKeyId(k.id);
toast.success(`Switched active key to "${k.name}"`);
}}
className="px-3 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-medium rounded-md transition-colors"
>
Set as Active
</button>
)}
<button
onClick={() => setKeyToDelete(k.id)}
className="p-1.5 text-zinc-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-md transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
{apiKeys.length === 0 && (
<div className="p-8 text-center text-xs text-zinc-500 bg-zinc-950/40 rounded-xl border border-dashed border-zinc-800">
No custom API keys saved. The app is currently operating in high-fidelity mock database mode.
</div>
)}
</div>
</div>
</div>
)}
{/* TAB 2: PREFERENCES */}
{activeTab === "preferences" && (
<div className="space-y-6 max-w-2xl bg-zinc-900 border border-zinc-800 rounded-xl p-6">
<h3 className="text-base font-semibold text-zinc-100 mb-4">Workspace Outreach Defaults</h3>
<div className="space-y-4 text-xs">
<div>
<label className="block font-medium text-zinc-300 mb-1">Default Sending Timezone</label>
<select
defaultValue="America/New_York"
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-100"
>
<option value="America/New_York">America/New_York (EST)</option>
<option value="America/Chicago">America/Chicago (CST)</option>
<option value="America/Los_Angeles">America/Los_Angeles (PST)</option>
<option value="Europe/London">Europe/London (GMT)</option>
</select>
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">Global Mailbox Rate Limiting</label>
<input
type="number"
defaultValue="50"
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg p-2.5 text-zinc-100"
/>
<span className="text-[11px] text-zinc-500 mt-1 block">Maximum outbound emails per mailbox / 24 hours</span>
</div>
<div className="pt-4 flex justify-end">
<button
type="button"
onClick={() => toast.success("Preferences updated")}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium"
>
Save Preferences
</button>
</div>
</div>
</div>
)}
{/* TAB 3: SANDBOX */}
{activeTab === "sandbox" && (
<div className="space-y-4 max-w-4xl bg-zinc-900 border border-zinc-800 rounded-xl p-6">
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
<Terminal className="w-5 h-5 text-indigo-400" /> Backend Gateway Architecture
</h3>
<p className="text-xs text-zinc-400 leading-relaxed">
All requests from the frontend client route securely through the Node.js/Express proxy (`/api/*`).
The proxy attaches the active API key as a query parameter (`?api_key=...`) to the core REST API endpoint (`https://server.smartlead.ai/api/v1`).
</p>
<div className="p-4 bg-zinc-950 border border-zinc-800 rounded-lg font-mono text-xs text-zinc-300 space-y-2">
<span className="text-emerald-400"># Example Proxy Request</span><br />
GET /api/campaigns → https://server.smartlead.ai/api/v1/campaigns?api_key=&#123;KEY&#125;<br />
POST /api/leads/search → https://server.smartlead.ai/api/v1/leads/?api_key=&#123;KEY&#125;
</div>
</div>
)}
{/* TAB 4: DATABASE / SUPABASE */}
{activeTab === "database" && (
<div className="space-y-6 max-w-4xl">
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-sm space-y-6">
<div>
<h3 className="text-base font-semibold text-zinc-100 flex items-center gap-2">
<Database className="w-5 h-5 text-emerald-400" /> Supabase Integration Status
</h3>
<p className="text-xs text-zinc-400 mt-1">
Check the connection to your Supabase project instance ({supabaseProjectRef()}).
To configure, provide your URL and Anon Key in your environment variables.
</p>
</div>
<div className={`p-4 rounded-xl border flex items-start gap-4 transition-colors ${
supabaseStatus === "connected" ? "bg-emerald-500/10 border-emerald-500/30" :
supabaseStatus === "unconfigured" ? "bg-amber-500/10 border-amber-500/30" :
supabaseStatus === "error" ? "bg-rose-500/10 border-rose-500/30" :
"bg-zinc-950/60 border-zinc-800"
}`}>
{supabaseStatus === "connected" && <CheckCircle2 className="w-6 h-6 text-emerald-400 shrink-0" />}
{supabaseStatus === "unconfigured" && <AlertCircle className="w-6 h-6 text-amber-400 shrink-0" />}
{supabaseStatus === "error" && <AlertCircle className="w-6 h-6 text-rose-400 shrink-0" />}
{supabaseStatus === "testing" && <Activity className="w-6 h-6 text-zinc-400 shrink-0 animate-pulse" />}
<div className="space-y-1">
<h4 className={`text-sm font-semibold ${
supabaseStatus === "connected" ? "text-emerald-400" :
supabaseStatus === "unconfigured" ? "text-amber-400" :
supabaseStatus === "error" ? "text-rose-400" :
"text-zinc-300"
}`}>
{supabaseStatus === "connected" ? "Connection Established" :
supabaseStatus === "unconfigured" ? "Missing Credentials" :
supabaseStatus === "error" ? "Connection Failed" :
"Testing Connection..."}
</h4>
<p className="text-xs text-zinc-400 leading-relaxed">
{supabaseStatus === "connected" ? `Successfully communicating with Supabase project ${supabaseProjectRef()}.` :
supabaseStatus === "unconfigured" ? "Cannot connect to Supabase. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY are set in your environment variables (.env file)." :
supabaseStatus === "error" ? "Failed to establish a connection with the provided credentials. Please verify your Project URL and Anon Key." :
"Pinging Supabase endpoints..."}
</p>
</div>
</div>
<div className="space-y-3 pt-2">
<h4 className="text-sm font-semibold text-zinc-200">Configuration Guide</h4>
<ol className="list-decimal list-inside space-y-2 text-xs text-zinc-400">
<li>Go to your Supabase Dashboard: <a href={supabaseDashboardUrl()} target="_blank" rel="noreferrer" className="text-emerald-400 hover:underline">Project {supabaseProjectRef()}</a></li>
<li>Navigate to Project Settings {"->"} API.</li>
<li>Copy the <strong>Project URL</strong> and add it to <code className="bg-zinc-950 px-1 py-0.5 rounded text-zinc-300">VITE_SUPABASE_URL</code>.</li>
<li>Copy the <strong>anon public</strong> key and add it to <code className="bg-zinc-950 px-1 py-0.5 rounded text-zinc-300">VITE_SUPABASE_ANON_KEY</code>.</li>
</ol>
</div>
</div>
</div>
)}
{/* Add Key Drawer */}
<FormDrawer
isOpen={isAddKeyOpen}
onClose={() => setIsAddKeyOpen(false)}
title="Add BRIDGEModulE API Key"
subtitle="Retrieve your API key from Settings -> Profile -> API Keys"
>
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const name = (form.elements.namedItem("name") as HTMLInputElement).value;
const key = (form.elements.namedItem("key") as HTMLInputElement).value;
addApiKey(name, key);
toast.success("API Key saved & activated");
setIsAddKeyOpen(false);
}}
className="space-y-4 text-xs"
>
<div>
<label className="block font-medium text-zinc-300 mb-1">Key Friendly Name *</label>
<input name="name" required placeholder="e.g. Production Main Key" className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100" />
</div>
<div>
<label className="block font-medium text-zinc-300 mb-1">API Key Token *</label>
<input name="key" type="password" required placeholder="sl_live_..." className="w-full bg-zinc-900 border border-zinc-800 rounded-lg p-2.5 text-zinc-100 font-mono" />
</div>
<div className="pt-4 flex justify-end gap-2 border-t border-zinc-800">
<button
type="button"
onClick={() => setIsAddKeyOpen(false)}
className="px-3 py-2 bg-zinc-800 text-zinc-300 rounded-lg"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium shadow"
>
Save Key
</button>
</div>
</form>
</FormDrawer>
{/* Delete Key Modal */}
<ConfirmModal
isOpen={Boolean(keyToDelete)}
onClose={() => setKeyToDelete(null)}
onConfirm={() => {
if (keyToDelete) {
removeApiKey(keyToDelete);
toast.success("API key deleted");
setKeyToDelete(null);
}
}}
title="Delete API Key"
description="Are you sure you want to remove this stored API key?"
/>
</div>
);
}