This commit is contained in:
184
pages/ConfigPage.tsx
Normal file
184
pages/ConfigPage.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user