latest
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { ThemeProvider, createTheme, useMediaQuery, Box, Grid, Card, CardContent, CardActions, Typography, Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, styled } from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useTLDraw } from '../../../../../contexts/TLDrawContext';
|
||||
import { supabase } from '../../../../../supabaseClient';
|
||||
|
||||
type Cabinet = { id: string; name: string };
|
||||
|
||||
const Toolbar = styled('div')(() => ({ display: 'flex', gap: '8px', marginBottom: '8px' }));
|
||||
|
||||
export const CCCabinetsPanel: React.FC = () => {
|
||||
const { tldrawPreferences, authToken } = useTLDraw() as { tldrawPreferences?: { colorScheme?: 'light' | 'dark' | 'system' }, authToken?: string };
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [renameOpen, setRenameOpen] = useState<null | Cabinet>(null);
|
||||
const [newName, setNewName] = useState('');
|
||||
|
||||
const theme = useMemo(() => {
|
||||
const mode = (tldrawPreferences?.colorScheme === 'system')
|
||||
? (prefersDarkMode ? 'dark' : 'light')
|
||||
: (tldrawPreferences?.colorScheme === 'dark' ? 'dark' : 'light');
|
||||
return createTheme({ palette: { mode, divider: 'var(--color-divider)' } });
|
||||
}, [tldrawPreferences?.colorScheme, prefersDarkMode]);
|
||||
|
||||
const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
|
||||
|
||||
type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined;
|
||||
const apiFetch = async (url: string, init?: RequestInitLite) => {
|
||||
const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`;
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
const bearer = session?.access_token || authToken || '';
|
||||
const res = await fetch(fullUrl, {
|
||||
...init,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${bearer}`,
|
||||
...(init?.headers || {})
|
||||
}
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const loadCabinets = async () => {
|
||||
const data = await apiFetch('/database/cabinets');
|
||||
setCabinets([...(data.owned || []), ...(data.shared || [])]);
|
||||
};
|
||||
|
||||
useEffect(() => { loadCabinets(); /* eslint-disable-line react-hooks/exhaustive-deps */ }, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newName.trim()) return;
|
||||
await apiFetch('/database/cabinets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName }) });
|
||||
setNewName('');
|
||||
setCreateOpen(false);
|
||||
await loadCabinets();
|
||||
};
|
||||
|
||||
const handleRename = async () => {
|
||||
if (!renameOpen || !newName.trim()) return;
|
||||
await apiFetch(`/database/cabinets/${renameOpen.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName }) });
|
||||
setRenameOpen(null);
|
||||
setNewName('');
|
||||
await loadCabinets();
|
||||
};
|
||||
|
||||
const handleDelete = async (cabinetId: string) => {
|
||||
await apiFetch(`/database/cabinets/${cabinetId}`, { method: 'DELETE' });
|
||||
await loadCabinets();
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box sx={{ p: 1, height: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Toolbar>
|
||||
<Button size="small" variant="outlined" startIcon={<AddIcon/>} onClick={() => { setNewName(''); setCreateOpen(true); }}>New Cabinet</Button>
|
||||
</Toolbar>
|
||||
<Grid container spacing={1} sx={{ overflow: 'auto' }}>
|
||||
{cabinets.map(c => (
|
||||
<Grid item xs={12} key={c.id}>
|
||||
<Card variant="outlined">
|
||||
<CardContent sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Typography variant="subtitle1" sx={{ color: 'var(--color-text)' }}>{c.name}</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'var(--color-text-secondary)' }}>{c.id}</Typography>
|
||||
</div>
|
||||
<CardActions>
|
||||
<IconButton size="small" onClick={() => { setRenameOpen(c); setNewName(c.name); }} title="Rename">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => handleDelete(c.id)} title="Delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Dialog open={createOpen} onClose={() => setCreateOpen(false)}>
|
||||
<DialogTitle>Create Cabinet</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField autoFocus fullWidth label="Name" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleCreate} disabled={!newName.trim()}>Create</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={!!renameOpen} onClose={() => setRenameOpen(null)}>
|
||||
<DialogTitle>Rename Cabinet</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField autoFocus fullWidth label="New name" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setRenameOpen(null)}>Cancel</Button>
|
||||
<Button onClick={handleRename} disabled={!newName.trim()}>Save</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user