feat: Implement main application shell with navigation and color scheme toggle
- Added Appshell component with responsive navbar and main content area - Integrated ColorSchemeToggle for light/dark mode switching - Created Welcome component with styled title and introductory text - Developed ChatPage for LLM interaction with WebSocket support - Implemented Biblioteca for managing notes with rich text editor - Added LoginPage for user authentication with error handling - Introduced MessageList and MessageBubble components for chat messages - Styled components with CSS modules for consistent design
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
|
||||
import { LlamadorAPI } from './LlamadorAPI';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
|
||||
|
||||
export function Consulta_API() {
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
<LlamadorAPI />
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Grid } from '@mantine/core';
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import { GridDashboard } from './Grid_dashboard_component';
|
||||
|
||||
export function Grid_Dashboard() {
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
|
||||
|
||||
<GridDashboard></GridDashboard>
|
||||
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Card, Text, Switch, Group, useMantineTheme, useComputedColorScheme } from '@mantine/core';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import { useState } from 'react';
|
||||
|
||||
const GRID_SIZE = 30;
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const sanitized = hex.replace('#', '');
|
||||
const bigint = parseInt(sanitized, 16);
|
||||
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
interface CardData {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const initialCards: CardData[] = [
|
||||
{ id: '1', x: 0, y: 0, width: GRID_SIZE * 2, height: GRID_SIZE * 2 },
|
||||
{ id: '2', x: GRID_SIZE * 2, y: 0, width: GRID_SIZE * 3, height: GRID_SIZE * 2 },
|
||||
{ id: '3', x: GRID_SIZE * 3, y: 0, width: GRID_SIZE * 3, height: GRID_SIZE * 2 },
|
||||
];
|
||||
|
||||
export const GridDashboard = () => {
|
||||
const theme = useMantineTheme();
|
||||
const colorScheme = useComputedColorScheme(); // ✅ directamente 'light' o 'dark'
|
||||
const isDark = colorScheme === 'dark';
|
||||
|
||||
// Color de la rejilla adaptado al modo del tema
|
||||
const gridBaseColor = isDark ? theme.colors.dark[4] : theme.colors.gray[3];
|
||||
const gridColor = hexToRgba(gridBaseColor, 0.25); // Ajusta la opacidad aquí
|
||||
|
||||
const [cards, setCards] = useState<CardData[]>(initialCards);
|
||||
const [showGrid, setShowGrid] = useState(true);
|
||||
|
||||
const updateCard = (id: string, updates: Partial<CardData>) => {
|
||||
setCards((prev) =>
|
||||
prev.map((card) => (card.id === id ? { ...card, ...updates } : card))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group mb="xs">
|
||||
<Switch
|
||||
checked={showGrid}
|
||||
onChange={(event) => setShowGrid(event.currentTarget.checked)}
|
||||
label="Mostrar cuadrícula"
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '700px',
|
||||
backgroundSize: `${GRID_SIZE}px ${GRID_SIZE}px`,
|
||||
backgroundImage: showGrid
|
||||
? `linear-gradient(to right, ${gridColor} 1px, transparent 1px),
|
||||
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`
|
||||
: 'none',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{cards.map((card) => (
|
||||
<Rnd
|
||||
key={card.id}
|
||||
size={{
|
||||
width: Math.round(card.width / GRID_SIZE) * GRID_SIZE,
|
||||
height: Math.round(card.height / GRID_SIZE) * GRID_SIZE,
|
||||
}}
|
||||
position={{ x: card.x, y: card.y }}
|
||||
minWidth={GRID_SIZE * 8}
|
||||
minHeight={GRID_SIZE * 8}
|
||||
bounds="parent"
|
||||
grid={[GRID_SIZE, GRID_SIZE]}
|
||||
onDragStop={(_, d) =>
|
||||
updateCard(card.id, {
|
||||
x: Math.round(d.x / GRID_SIZE) * GRID_SIZE,
|
||||
y: Math.round(d.y / GRID_SIZE) * GRID_SIZE,
|
||||
})
|
||||
}
|
||||
onResizeStop={(_, __, ref, ___, pos) =>
|
||||
updateCard(card.id, {
|
||||
width: Math.round(ref.offsetWidth / GRID_SIZE) * GRID_SIZE,
|
||||
height: Math.round(ref.offsetHeight / GRID_SIZE) * GRID_SIZE,
|
||||
x: Math.round(pos.x / GRID_SIZE) * GRID_SIZE,
|
||||
y: Math.round(pos.y / GRID_SIZE) * GRID_SIZE,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={{ height: '100%', userSelect: 'none' }}
|
||||
>
|
||||
<Text fw={500}>Card {card.id}</Text>
|
||||
<Text size="sm">Mueve o redimensiona</Text>
|
||||
</Card>
|
||||
</Rnd>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
TextInput,
|
||||
Textarea,
|
||||
Button,
|
||||
Box,
|
||||
Center,
|
||||
Stack,
|
||||
Badge,
|
||||
Group,
|
||||
} from '@mantine/core';
|
||||
import { MetodoSelect } from './MetodoSelect';
|
||||
import { useMantineTheme } from '@mantine/core';
|
||||
|
||||
export function LlamadorAPI() {
|
||||
const [direccion, setDireccion] = useState('http://localhost:8000/api/v1/ping/');
|
||||
const [metodo, setMetodo] = useState('GET');
|
||||
const [contenido, setContenido] = useState('');
|
||||
const [respuesta, setRespuesta] = useState('');
|
||||
const [codigoRespuesta, setCodigoRespuesta] = useState<number | null>(null);
|
||||
|
||||
const colorCodigo = (status: number): string => {
|
||||
if (status >= 200 && status < 300) return 'green';
|
||||
if (status >= 300 && status < 400) return 'yellow';
|
||||
if (status >= 400 && status < 500) return 'orange';
|
||||
return 'red';
|
||||
};
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
|
||||
const llamarAPI = async () => {
|
||||
try {
|
||||
const options: RequestInit = {
|
||||
method: metodo,
|
||||
mode: 'cors',
|
||||
headers: {},
|
||||
};
|
||||
|
||||
if (metodo !== 'GET') {
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
try {
|
||||
JSON.parse(contenido);
|
||||
options.body = contenido;
|
||||
} catch (err) {
|
||||
setRespuesta('Error: El contenido no es un JSON válido');
|
||||
setCodigoRespuesta(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(direccion, options);
|
||||
setCodigoRespuesta(res.status);
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await res.json();
|
||||
const formatted = JSON.stringify(data, null, 2);
|
||||
setRespuesta(formatted);
|
||||
} else {
|
||||
const text = await res.text();
|
||||
setRespuesta(text);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error en la API:', error);
|
||||
setRespuesta(`Error: ${error.message || error}`);
|
||||
setCodigoRespuesta(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
llamarAPI();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [metodo, direccion, contenido]);
|
||||
|
||||
return (
|
||||
<Box style={{ flex: 1, padding: 40 }}>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Stack style={{ width: 600 }}>
|
||||
<TextInput
|
||||
label="Dirección"
|
||||
placeholder="http://localhost:8000/api/..."
|
||||
value={direccion}
|
||||
onChange={(e) => setDireccion(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<MetodoSelect metodo={metodo} setMetodo={setMetodo} />
|
||||
|
||||
<Textarea
|
||||
label="Contenido (JSON)"
|
||||
placeholder='{"contenido": "Hola"}'
|
||||
value={contenido}
|
||||
onChange={(e) => setContenido(e.currentTarget.value)}
|
||||
autosize
|
||||
minRows={3}
|
||||
/>
|
||||
|
||||
<Button onClick={llamarAPI}
|
||||
variant="gradient"
|
||||
gradient={{
|
||||
from: theme.colors.brand[7],
|
||||
to: theme.colors.brand[4],
|
||||
}}>
|
||||
Enviar solicitud
|
||||
</Button>
|
||||
|
||||
{codigoRespuesta !== null && (
|
||||
<Group>
|
||||
<Badge color={colorCodigo(codigoRespuesta)} size="lg">
|
||||
Código: {codigoRespuesta}
|
||||
</Badge>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
label="Respuesta de la API"
|
||||
value={respuesta}
|
||||
readOnly
|
||||
autosize
|
||||
minRows={6}
|
||||
/>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Select, Group } from '@mantine/core';
|
||||
import { IconCheck } from '../../assets/icons';
|
||||
|
||||
interface MetodoSelectProps {
|
||||
metodo: string;
|
||||
setMetodo: (value: string) => void;
|
||||
}
|
||||
|
||||
const metodoData = [
|
||||
{ value: 'GET', label: 'GET' },
|
||||
{ value: 'POST', label: 'POST' },
|
||||
{ value: 'PUT', label: 'PUT' },
|
||||
{ value: 'DELETE', label: 'DELETE' },
|
||||
];
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
GET: '#10b981', // verde
|
||||
POST: '#3b82f6', // azul
|
||||
PUT: '#f59e0b', // naranja
|
||||
DELETE: '#ef4444', // rojo
|
||||
};
|
||||
|
||||
const backgroundMap: Record<string, string> = {
|
||||
GET: '#d1fae5',
|
||||
POST: '#e0f2fe',
|
||||
PUT: '#fef3c7',
|
||||
DELETE: '#fee2e2',
|
||||
};
|
||||
|
||||
export function MetodoSelect({ metodo, setMetodo }: MetodoSelectProps) {
|
||||
return (
|
||||
<Select
|
||||
label="Método"
|
||||
value={metodo}
|
||||
onChange={(value) => setMetodo(value!)}
|
||||
data={metodoData}
|
||||
renderOption={({ option, checked }) => (
|
||||
<Group style={{ color: colorMap[option.value], fontWeight: 600 }}>
|
||||
{option.label}
|
||||
{checked && <IconCheck/>}
|
||||
</Group>
|
||||
)}
|
||||
styles={{
|
||||
input: {
|
||||
backgroundColor: backgroundMap[metodo],
|
||||
color: '#111',
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AppShellWithMenu } from '../FitzStudio/Appshell/Appshell';
|
||||
import { Card, Grid, Title, Loader } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
type ChartOption = any;
|
||||
|
||||
function useChartOption(endpoint: string) {
|
||||
const [option, setOption] = useState<ChartOption | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/v1/charts/${endpoint}`)
|
||||
.then((res) => res.json())
|
||||
.then((json) => setOption(json))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, [endpoint]);
|
||||
|
||||
return { option, loading };
|
||||
}
|
||||
|
||||
export function VisualizacionesRandom() {
|
||||
const charts = [
|
||||
{ title: 'Gráfico de barras', endpoint: 'bar' },
|
||||
{ title: 'Gráfico de líneas', endpoint: 'line' },
|
||||
{ title: 'Gráfico de pastel', endpoint: 'pie' },
|
||||
{ title: 'Scatter plot', endpoint: 'scatter' },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppShellWithMenu>
|
||||
<Grid>
|
||||
{charts.map(({ title, endpoint }, idx) => {
|
||||
const { option, loading } = useChartOption(endpoint);
|
||||
|
||||
return (
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 6, lg: 3 }} key={idx}>
|
||||
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
||||
<Title order={4}>{title}</Title>
|
||||
{loading || !option ? (
|
||||
<Loader mt="md" />
|
||||
) : (
|
||||
<ReactECharts option={option} style={{ height: 250, marginTop: 16 }} />
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</AppShellWithMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user