feat(ce): upgrade to v0.5.0 with contacts management, advanced settings and ui modernization
This commit is contained in:
@@ -1,48 +1,56 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { CompanyTable } from './components/CompanyTable'
|
||||
import { ContactsTable } from './components/ContactsTable' // NEW
|
||||
import { ImportWizard } from './components/ImportWizard'
|
||||
import { Inspector } from './components/Inspector'
|
||||
import { RoboticsSettings } from './components/RoboticsSettings' // NEW
|
||||
import { LayoutDashboard, UploadCloud, Search, RefreshCw, Settings } from 'lucide-react'
|
||||
import { RoboticsSettings } from './components/RoboticsSettings'
|
||||
import { LayoutDashboard, UploadCloud, RefreshCw, Settings, Users, Building, Sun, Moon } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// Base URL detection (Production vs Dev)
|
||||
const API_BASE = import.meta.env.BASE_URL === '/ce/' ? '/ce/api' : '/api';
|
||||
|
||||
interface Stats {
|
||||
total: number;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [stats, setStats] = useState<Stats>({ total: 0 })
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
const [isImportOpen, setIsImportOpen] = useState(false)
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false) // NEW
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const [selectedCompanyId, setSelectedCompanyId] = useState<number | null>(null)
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/companies?limit=1`)
|
||||
setStats({ total: res.data.total })
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch stats", e)
|
||||
}
|
||||
}
|
||||
const [selectedContactId, setSelectedContactId] = useState<number | null>(null)
|
||||
|
||||
// Navigation State
|
||||
const [view, setView] = useState<'companies' | 'contacts'>('companies')
|
||||
|
||||
// Theme State
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
return localStorage.getItem('theme') as 'dark' | 'light' || 'dark'
|
||||
}
|
||||
return 'dark'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats()
|
||||
}, [refreshKey])
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
localStorage.setItem('theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => setTheme(prev => prev === 'dark' ? 'light' : 'dark')
|
||||
|
||||
const handleCompanySelect = (id: number) => {
|
||||
setSelectedCompanyId(id)
|
||||
setSelectedContactId(null)
|
||||
}
|
||||
|
||||
const handleCloseInspector = () => {
|
||||
setSelectedCompanyId(null)
|
||||
setSelectedContactId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-200 font-sans">
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-200 font-sans transition-colors">
|
||||
<ImportWizard
|
||||
isOpen={isImportOpen}
|
||||
onClose={() => setIsImportOpen(false)}
|
||||
@@ -50,41 +58,62 @@ function App() {
|
||||
onSuccess={() => setRefreshKey(k => k + 1)}
|
||||
/>
|
||||
|
||||
{/* Robotics Logic Settings */}
|
||||
<RoboticsSettings
|
||||
isOpen={isSettingsOpen}
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
apiBase={API_BASE}
|
||||
/>
|
||||
|
||||
{/* Inspector Sidebar */}
|
||||
<Inspector
|
||||
companyId={selectedCompanyId}
|
||||
companyId={selectedCompanyId}
|
||||
initialContactId={selectedContactId}
|
||||
onClose={handleCloseInspector}
|
||||
apiBase={API_BASE}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<header className="border-b border-slate-800 bg-slate-900/50 sticky top-0 z-10 backdrop-blur-md">
|
||||
<header className="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 sticky top-0 z-10 backdrop-blur-md">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-600 rounded-lg">
|
||||
<LayoutDashboard className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white tracking-tight">Company Explorer</h1>
|
||||
<p className="text-xs text-blue-400 font-medium">ROBOTICS EDITION <span className="text-slate-600 ml-2">v0.4.0 (Overwrites & Export)</span></p>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">Company Explorer</h1>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-400 font-medium">ROBOTICS EDITION <span className="text-slate-500 dark:text-slate-600 ml-2">v0.5.0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm text-slate-400">
|
||||
<span className="text-white font-bold">{stats.total}</span> Companies
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
{/* View Switcher */}
|
||||
<div className="hidden md:flex bg-slate-100 dark:bg-slate-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setView('companies')}
|
||||
className={clsx("px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2", view === 'companies' ? "bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white" : "text-slate-500 hover:text-slate-900 dark:hover:text-slate-300")}
|
||||
>
|
||||
<Building className="h-4 w-4" /> Companies
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('contacts')}
|
||||
className={clsx("px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2", view === 'contacts' ? "bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white" : "text-slate-500 hover:text-slate-900 dark:hover:text-slate-300")}
|
||||
>
|
||||
<Users className="h-4 w-4" /> Contacts
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-6 w-px bg-slate-300 dark:bg-slate-700 mx-2 hidden md:block"></div>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500 dark:text-slate-400"
|
||||
title="Toggle Theme"
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
className="p-2 hover:bg-slate-800 rounded-full transition-colors text-slate-400 hover:text-white"
|
||||
className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500 dark:text-slate-400"
|
||||
title="Configure Robotics Logic"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
@@ -92,42 +121,67 @@ function App() {
|
||||
|
||||
<button
|
||||
onClick={() => setRefreshKey(k => k + 1)}
|
||||
className="p-2 hover:bg-slate-800 rounded-full transition-colors text-slate-400 hover:text-white"
|
||||
className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500 dark:text-slate-400"
|
||||
title="Refresh Data"
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-md font-medium text-sm transition-all shadow-lg shadow-blue-900/20"
|
||||
onClick={() => setIsImportOpen(true)}
|
||||
>
|
||||
<UploadCloud className="h-4 w-4" />
|
||||
Import List
|
||||
</button>
|
||||
|
||||
{view === 'companies' && (
|
||||
<button
|
||||
className="hidden md:flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-md font-medium text-sm transition-all shadow-lg shadow-blue-900/20"
|
||||
onClick={() => setIsImportOpen(true)}
|
||||
>
|
||||
<UploadCloud className="h-4 w-4" />
|
||||
Import List
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Nav */}
|
||||
<div className="md:hidden border-t border-slate-200 dark:border-slate-800 flex">
|
||||
<button
|
||||
onClick={() => setView('companies')}
|
||||
className={clsx("flex-1 py-3 text-sm font-medium flex justify-center items-center gap-2 border-b-2", view === 'companies' ? "border-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-slate-500")}
|
||||
>
|
||||
<Building className="h-4 w-4" /> Companies
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('contacts')}
|
||||
className={clsx("flex-1 py-3 text-sm font-medium flex justify-center items-center gap-2 border-b-2", view === 'contacts' ? "border-blue-500 text-blue-600 dark:text-blue-400" : "border-transparent text-slate-500")}
|
||||
>
|
||||
<Users className="h-4 w-4" /> Contacts
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-6 flex gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-2.5 h-5 w-5 text-slate-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search companies..."
|
||||
className="w-full bg-slate-900 border border-slate-700 text-slate-200 rounded-md pl-10 pr-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-xl">
|
||||
<CompanyTable key={refreshKey} apiBase={API_BASE} onRowClick={handleCompanySelect} /> {/* NEW PROP */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 h-[calc(100vh-4rem)]">
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm dark:shadow-xl h-full">
|
||||
{view === 'companies' ? (
|
||||
<CompanyTable
|
||||
refreshKey={refreshKey}
|
||||
apiBase={API_BASE}
|
||||
onRowClick={handleCompanySelect}
|
||||
onImportClick={() => setIsImportOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
<ContactsTable
|
||||
apiBase={API_BASE}
|
||||
onCompanyClick={(id) => { setSelectedCompanyId(id); setView('companies'); }}
|
||||
onContactClick={(companyId, contactId) => {
|
||||
setSelectedCompanyId(companyId);
|
||||
setSelectedContactId(contactId);
|
||||
// setView('companies')? No, we stay in context of 'Contacts' but Inspector opens
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App
|
||||
Reference in New Issue
Block a user