Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from 'react'; | |
| import { Database, Plus, Trash2, RefreshCw } from 'lucide-react'; | |
| export default function PocketBase() { | |
| const [items, setItems] = useState([]); | |
| const [newItem, setNewItem] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [actionLoading, setActionLoading] = useState(false); | |
| const fetchItems = async () => { | |
| setLoading(true); | |
| try { | |
| const res = await fetch('/api/pocketbase'); | |
| const data = await res.json(); | |
| setItems(data.items || []); | |
| } catch (error) { | |
| console.error('Failed to fetch items'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchItems(); | |
| }, []); | |
| const addItem = async () => { | |
| if (!newItem.trim()) return; | |
| setActionLoading(true); | |
| try { | |
| const res = await fetch('/api/pocketbase', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name: newItem }), | |
| }); | |
| if (res.ok) { | |
| setNewItem(''); | |
| fetchItems(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to add item'); | |
| } finally { | |
| setActionLoading(false); | |
| } | |
| }; | |
| const deleteItem = async (id) => { | |
| setActionLoading(true); | |
| try { | |
| const res = await fetch('/api/pocketbase', { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ id }), | |
| }); | |
| if (res.ok) { | |
| fetchItems(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete item'); | |
| } finally { | |
| setActionLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="bg-slate-800 rounded-xl p-6 shadow-xl border border-slate-700"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <div className="flex items-center gap-2"> | |
| <Database className="w-5 h-5 text-emerald-500" /> | |
| <h2 className="text-lg font-semibold text-white">PocketBase Manager</h2> | |
| </div> | |
| <button | |
| onClick={fetchItems} | |
| disabled={loading} | |
| className="text-slate-400 hover:text-white transition-colors p-1 rounded-md hover:bg-slate-700" | |
| title="Refresh" | |
| > | |
| <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> | |
| </button> | |
| </div> | |
| <div className="flex gap-2 mb-6"> | |
| <input | |
| type="text" | |
| value={newItem} | |
| onChange={(e) => setNewItem(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && addItem()} | |
| placeholder="Add new record..." | |
| className="flex-1 bg-slate-900 border border-slate-600 rounded-lg px-4 py-2 text-white placeholder-slate-500 focus:ring-2 focus:ring-emerald-500 focus:border-transparent outline-none" | |
| /> | |
| <button | |
| onClick={addItem} | |
| disabled={actionLoading} | |
| className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg transition-colors disabled:opacity-50 flex items-center justify-center" | |
| > | |
| <Plus className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="space-y-2 max-h-64 overflow-y-auto pr-2"> | |
| {loading ? ( | |
| <p className="text-slate-500 text-sm text-center py-4">Loading records...</p> | |
| ) : items.length === 0 ? ( | |
| <p className="text-slate-500 text-sm text-center py-4">No records found</p> | |
| ) : ( | |
| items.map((item) => ( | |
| <div | |
| key={item.id} | |
| className="flex items-center justify-between bg-slate-900 p-3 rounded-lg border border-slate-700 group hover:border-slate-500 transition-colors" | |
| > | |
| <span className="text-slate-300 truncate mr-2">{item.name || item.id}</span> | |
| <button | |
| onClick={() => deleteItem(item.id)} | |
| className="text-red-400 opacity-0 group-hover:opacity-100 transition-opacity hover:text-red-300 p-1 hover:bg-red-900/30 rounded" | |
| title="Delete" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |