185 lines
10 KiB
TypeScript
185 lines
10 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import axios from 'axios';
|
|
import Layout from '../components/Layout';
|
|
import BrandKit from '../components/BrandKit';
|
|
import { Settings, Save, Plus, Trash2, ToggleLeft, ToggleRight, Search } from 'lucide-react';
|
|
|
|
interface ConfigItem {
|
|
key: string;
|
|
value: string;
|
|
description?: string;
|
|
}
|
|
|
|
export default function ConfigPage() {
|
|
const [configs, setConfigs] = useState<ConfigItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState('');
|
|
const [newItem, setNewItem] = useState({ key: '', value: '', description: '' });
|
|
const [editingItem, setEditingItem] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchConfigs();
|
|
}, []);
|
|
|
|
const fetchConfigs = async () => {
|
|
try {
|
|
const res = await axios.get('/api/admin/config');
|
|
setConfigs(res.data.configs);
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async (key: string, value: string, description?: string) => {
|
|
try {
|
|
await axios.put('/api/admin/config', { key, value, description });
|
|
setEditingItem(null);
|
|
fetchConfigs();
|
|
if (key === newItem.key) setNewItem({ key: '', value: '', description: '' });
|
|
} catch (err) {
|
|
alert('Failed to save config');
|
|
}
|
|
};
|
|
|
|
const filteredConfigs = (configs || []).filter(c =>
|
|
c.key.toLowerCase().includes(search.toLowerCase()) ||
|
|
(c.description || '').toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="max-w-5xl mx-auto p-8">
|
|
|
|
<div className="flex justify-between items-end mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-black text-stone-900 tracking-tight flex items-center gap-3">
|
|
<Settings className="w-8 h-8 text-stone-400" />
|
|
System Configuration
|
|
</h1>
|
|
<p className="text-stone-500 mt-2">Manage global variables, feature flags, and system limits.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<BrandKit />
|
|
|
|
{/* New Config Card */}
|
|
<div className="bg-white border border-stone-200 shadow-sm rounded-2xl p-6 mb-8 group focus-within:ring-2 ring-purple-500/20 transition-all">
|
|
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest mb-4 flex items-center gap-2">
|
|
<Plus className="w-4 h-4" /> Add New Variable
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<input
|
|
placeholder="KEY_NAME (e.g. MAX_TOKENS)"
|
|
className="bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm font-mono font-bold focus:outline-none focus:border-purple-500 transition-all"
|
|
value={newItem.key}
|
|
onChange={e => setNewItem({ ...newItem, key: e.target.value.toUpperCase() })}
|
|
/>
|
|
<input
|
|
placeholder="Value"
|
|
className="bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-purple-500 transition-all"
|
|
value={newItem.value}
|
|
onChange={e => setNewItem({ ...newItem, value: e.target.value })}
|
|
/>
|
|
<input
|
|
placeholder="Description (Optional)"
|
|
className="bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-purple-500 transition-all"
|
|
value={newItem.description}
|
|
onChange={e => setNewItem({ ...newItem, description: e.target.value })}
|
|
/>
|
|
<button
|
|
disabled={!newItem.key || !newItem.value}
|
|
onClick={() => handleSave(newItem.key, newItem.value, newItem.description)}
|
|
className="bg-stone-900 text-white rounded-xl font-bold text-sm hover:bg-purple-600 disabled:opacity-50 disabled:hover:bg-stone-900 transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
<Save className="w-4 h-4" /> Save Variable
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search Bar */}
|
|
<div className="relative mb-6">
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-stone-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search configuration keys..."
|
|
className="w-full pl-10 pr-4 py-3 bg-white border border-stone-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-stone-100 transition-all"
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Config Table */}
|
|
<div className="bg-white border border-stone-200 shadow-sm rounded-3xl overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="bg-stone-50 border-b border-stone-100">
|
|
<tr>
|
|
<th className="text-left py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Key Name</th>
|
|
<th className="text-left py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Value</th>
|
|
<th className="text-left py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Description</th>
|
|
<th className="text-right py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-stone-100">
|
|
{filteredConfigs.map(config => (
|
|
<tr key={config.key} className="hover:bg-stone-50/50 transition-colors group">
|
|
<td className="py-4 px-6 font-mono text-xs font-bold text-purple-600 select-all">
|
|
{config.key}
|
|
</td>
|
|
<td className="py-4 px-6">
|
|
{editingItem === config.key ? (
|
|
<input
|
|
defaultValue={config.value}
|
|
className="w-full bg-white border border-purple-200 rounded-lg px-2 py-1 text-sm focus:outline-none"
|
|
onBlur={(e) => handleSave(config.key, e.target.value, config.description)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleSave(config.key, e.currentTarget.value, config.description);
|
|
}}
|
|
autoFocus
|
|
/>
|
|
) : (
|
|
<button
|
|
onClick={() => setEditingItem(config.key)}
|
|
className="text-sm font-medium text-stone-700 hover:text-purple-600 border-b border-stone-200 border-dashed hover:border-purple-300 transition-colors text-left"
|
|
>
|
|
{config.value === 'true' || config.value === 'false' ? (
|
|
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-[10px] uppercase font-black tracking-wider ${config.value === 'true' ? 'bg-green-100 text-green-700' : 'bg-red-50 text-red-500'}`}>
|
|
{config.value === 'true' ? <ToggleRight className="w-4 h-4" /> : <ToggleLeft className="w-4 h-4" />}
|
|
{config.value.toUpperCase()}
|
|
</span>
|
|
) : (
|
|
<span className="truncate max-w-[200px] block" title={config.value}>{config.value}</span>
|
|
)}
|
|
</button>
|
|
)}
|
|
</td>
|
|
<td className="py-4 px-6 text-xs text-stone-400 font-medium">
|
|
{config.description || '-'}
|
|
</td>
|
|
<td className="py-4 px-6 text-right">
|
|
<button
|
|
onClick={() => setEditingItem(config.key)}
|
|
className="p-2 text-stone-300 hover:text-stone-900 transition-colors"
|
|
>
|
|
<Settings className="w-4 h-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{filteredConfigs.length === 0 && (
|
|
<tr>
|
|
<td colSpan={4} className="py-10 text-center text-stone-400 text-sm">
|
|
No configuration keys found.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|