MOHAN 5b4834e6a5 Migrate frontend from Next.js to Vite + React Router
Full technology change: Next.js App Router replaced with Vite +
react-router-dom v6. All 21 pages and 5 landing components converted
(next/link -> Link, next/navigation -> react-router-dom hooks, next/font
-> @fontsource/inter, process.env -> import.meta.env). Dropped the unused
Stripe API routes, which can't run in a client-only Vite build anyway and
were never wired to any page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 17:10:03 +05:30

174 lines
7.3 KiB
TypeScript

import { useState, useEffect } from 'react';
import { api, session } from '@/lib/api';
import { Plus, Trash2, TestTube2, Globe, CheckCircle } from 'lucide-react';
interface Instance {
instance_name: string;
url: string;
database: string;
username: string;
type: string;
}
export default function Connections() {
const [instances, setInstances] = useState<Instance[]>([]);
const [showForm, setShowForm] = useState(false);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({
instance_name: 'default',
url: '',
database: '',
username: '',
credential: '',
is_api_key: true,
});
function load() {
const key = session.getKey();
if (!key) return;
api.listCredentials(key).then((r) => setInstances(r.instances)).catch(console.error);
}
useEffect(load, []);
async function handleAdd(e: React.FormEvent) {
e.preventDefault();
const key = session.getKey();
if (!key) return;
setLoading(true);
try {
await api.addCredential(key, form);
setShowForm(false);
setForm({ instance_name: 'default', url: '', database: '', username: '', credential: '', is_api_key: true });
load();
} catch (err) {
alert(err instanceof Error ? err.message : 'Failed to add connection');
} finally {
setLoading(false);
}
}
async function handleDelete(name: string) {
if (!confirm(`Remove "${name}"?`)) return;
const key = session.getKey();
if (!key) return;
await api.deleteCredential(key, name);
load();
}
return (
<div className="max-w-4xl space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">Odoo Connections</h1>
<p className="text-sm text-gray-500 mt-0.5">Manage your Odoo ERP instances.</p>
</div>
<button onClick={() => setShowForm(true)}
className="flex items-center gap-2 gradient-brand text-white px-4 py-2 rounded-xl text-sm font-medium hover:opacity-90 transition-opacity">
<Plus size={16} /> Add Connection
</button>
</div>
{/* Existing connections */}
{instances.length === 0 && !showForm && (
<div className="bg-white rounded-xl border border-dashed border-gray-300 p-12 text-center">
<Globe size={32} className="mx-auto text-gray-300 mb-3" />
<p className="text-sm font-medium text-gray-600 mb-1">No connections yet</p>
<p className="text-xs text-gray-400 mb-4">Add your Odoo instance to get started.</p>
<button onClick={() => setShowForm(true)}
className="gradient-brand text-white px-4 py-2 rounded-xl text-sm font-medium">
Add Connection
</button>
</div>
)}
{instances.map((inst) => (
<div key={inst.instance_name} className="bg-white rounded-xl border border-gray-100 shadow-sm p-5">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<CheckCircle size={18} className="text-green-600" />
</div>
<div>
<p className="font-semibold text-sm text-gray-900">{inst.instance_name}</p>
<p className="text-xs text-gray-500">{inst.url}</p>
</div>
</div>
<button onClick={() => handleDelete(inst.instance_name)}
className="text-gray-400 hover:text-red-500 p-1 transition-colors">
<Trash2 size={15} />
</button>
</div>
<div className="mt-4 grid grid-cols-3 gap-4 text-xs">
<div><p className="text-gray-400 mb-0.5">Database</p><p className="text-gray-700 font-mono">{inst.database}</p></div>
<div><p className="text-gray-400 mb-0.5">Username</p><p className="text-gray-700">{inst.username}</p></div>
<div><p className="text-gray-400 mb-0.5">Auth Type</p><p className="text-gray-700 capitalize">{inst.type}</p></div>
</div>
</div>
))}
{/* Add connection form panel */}
{showForm && (
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<h2 className="text-sm font-semibold text-gray-900 mb-5">Add Odoo Connection</h2>
<form onSubmit={handleAdd} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Field label="Connection Name" value={form.instance_name}
onChange={(v) => setForm({ ...form, instance_name: v })} placeholder="e.g. production" />
<Field label="Odoo URL" value={form.url}
onChange={(v) => setForm({ ...form, url: v })} placeholder="https://mycompany.odoo.com" />
</div>
<div className="grid grid-cols-2 gap-4">
<Field label="Database Name" value={form.database}
onChange={(v) => setForm({ ...form, database: v })} placeholder="mydb" />
<Field label="Odoo Username" value={form.username}
onChange={(v) => setForm({ ...form, username: v })} placeholder="admin@company.com" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1.5">Authentication Method</label>
<select value={form.is_api_key ? 'apikey' : 'password'}
onChange={(e) => setForm({ ...form, is_api_key: e.target.value === 'apikey' })}
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500">
<option value="apikey">API Key (Recommended)</option>
<option value="password">Password</option>
</select>
</div>
<Field
label={form.is_api_key ? 'Odoo API Key' : 'Password'}
value={form.credential}
onChange={(v) => setForm({ ...form, credential: v })}
placeholder={form.is_api_key ? 'Paste your Odoo API key' : 'Odoo password'}
type="password"
/>
<div className="flex gap-3 pt-2">
<button type="submit" disabled={loading}
className="gradient-brand text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:opacity-90 disabled:opacity-60 flex items-center gap-2">
{loading ? 'Saving…' : <><TestTube2 size={14} /> Test & Save</>}
</button>
<button type="button" onClick={() => setShowForm(false)}
className="border border-gray-200 text-gray-600 px-5 py-2.5 rounded-xl text-sm font-medium hover:bg-gray-50">
Cancel
</button>
</div>
</form>
</div>
)}
</div>
);
}
function Field({ label, value, onChange, placeholder, type = 'text' }: {
label: string; value: string; onChange: (v: string) => void; placeholder?: string; type?: string;
}) {
return (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1.5">{label}</label>
<input
type={type} value={value} onChange={(e) => onChange(e.target.value)}
placeholder={placeholder} required
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
/>
</div>
);
}