Replace CRA boilerplate with custom dashboard setup, remove unused files, and add new styles and components.
Some checks failed
continuous-integration/drone Build was killed

This commit is contained in:
2025-11-16 11:52:12 -04:00
parent a78a146129
commit deb38ca331
27 changed files with 1458 additions and 164 deletions

57
.drone.yml Normal file
View File

@@ -0,0 +1,57 @@
kind: pipeline
type: docker
name: deploy-backoffice
trigger:
branch:
- master
steps:
- name: deploying-project
image: alpine
environment:
SSH_USERNAME:
from_secret: ssh_username
SSH_HOSTNAME:
from_secret: ssh_hostname
SSH_PRIVATE_KEY:
from_secret: ssh_id_rsa
commands:
- apk add --no-cache rsync openssh
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- |
rsync -avz --delete \
-e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
./ $SSH_USERNAME@$SSH_HOSTNAME:/var/www/${DRONE_REPO_OWNER}/${DRONE_REPO_NAME} \
--exclude .git \
--exclude node_modules \
--exclude .drone.yml
when:
branch:
- master
event:
- push
- name: restarting-project
image: alpine
environment:
SSH_USERNAME:
from_secret: ssh_username
SSH_HOSTNAME:
from_secret: ssh_hostname
SSH_PRIVATE_KEY:
from_secret: ssh_id_rsa
commands:
- apk add --no-cache openssh
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- CONTAINER_NAME="${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}"
- ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa $SSH_USERNAME@$SSH_HOSTNAME "docker restart $CONTAINER_NAME"
when:
branch:
- develop
event:
- push

10
.idea/php.xml generated
View File

@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpStanOptionsConfiguration"> <component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>

View File

@@ -7,8 +7,8 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"react": "^19.2.0", "react": "18.2.0",
"react-dom": "^19.2.0", "react-dom": "18.2.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,43 +1,20 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="es">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Request Scheduler - Panel de Control"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
manifest.json provides metadata used when your web app is installed on a <title>Request Scheduler - Panel de Control</title>
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,23 +1,31 @@
import logo from './logo.svg'; import React, { useState } from 'react';
import './App.css'; import './styles/globals.css';
import { Sidebar, Header } from './components/Layout';
import { DashboardCards } from './components/Dashboard';
import RequestTable from './components/Requests/RequestTable';
import RequestForm from './components/Requests/RequestForm';
function App() { function App() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const handleToggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
return ( return (
<div className="App"> <div className="container">
<header className="App-header"> <Sidebar
<img src={logo} className="App-logo" alt="logo" /> isMobileMenuOpen={isMobileMenuOpen}
<p> onToggleMobileMenu={handleToggleMobileMenu}
Edit <code>src/App.js</code> and save to reload. />
</p> <main className="main-content">
<a <Header />
className="App-link" <section className="content-wrapper">
href="https://reactjs.org" <DashboardCards />
target="_blank" <div className="content-grid">
rel="noopener noreferrer" <RequestForm />
> <RequestTable />
Learn React </div>
</a> </section>
</header> </main>
</div> </div>
); );
} }

View File

@@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,58 @@
import React from 'react';
import StatsCard from './StatsCard';
import '../../styles/components.css';
const DashboardCards = () => {
const statsData = [
{
type: 'primary',
value: '42',
title: 'Solicitudes Programadas',
trend: '12% desde la semana pasada',
trendDirection: 'up',
icon: 'fas fa-clock'
},
{
type: 'warning',
value: '28',
title: 'Solicitudes Pendientes',
trend: '5% desde ayer',
trendDirection: 'down',
icon: 'fas fa-hourglass-half'
},
{
type: 'success',
value: '156',
title: 'Solicitudes Ejecutadas',
trend: '8% desde ayer',
trendDirection: 'up',
icon: 'fas fa-check-circle'
},
{
type: 'danger',
value: '12',
title: 'Solicitudes Fallidas',
trend: '3% desde ayer',
trendDirection: 'down',
icon: 'fas fa-exclamation-circle'
}
];
return (
<div className="dashboard-cards">
{statsData.map((stat, index) => (
<StatsCard
key={index}
type={stat.type}
value={stat.value}
title={stat.title}
trend={stat.trend}
trendDirection={stat.trendDirection}
icon={stat.icon}
/>
))}
</div>
);
};
export default DashboardCards;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import '../../styles/components.css';
const StatsCard = ({ type, value, title, trend, trendDirection, icon }) => {
return (
<div className={`card ${type}`}>
<div className="card-header">
<div>
<div className="card-value">{value}</div>
<div className="card-title">{title}</div>
<div className={`card-trend ${trendDirection}`}>
<i className={`fas fa-arrow-${trendDirection}`}></i> {trend}
</div>
</div>
<div className={`card-icon ${type}`}>
<i className={icon}></i>
</div>
</div>
</div>
);
};
export default StatsCard;

View File

@@ -0,0 +1,2 @@
export { default as DashboardCards } from './DashboardCards';
export { default as StatsCard } from './StatsCard';

View File

@@ -0,0 +1,24 @@
import React from 'react';
import '../../styles/components.css';
const Header = () => {
return (
<div className="header">
<div className="page-title">
<i className="fas fa-tachometer-alt"></i>
<span>Dashboard</span>
</div>
<div className="header-actions">
<div className="search-box">
<i className="fas fa-search"></i>
<input type="text" placeholder="Buscar solicitudes..." />
</div>
<button className="btn btn-primary">
<i className="fas fa-plus"></i> Nueva Solicitud
</button>
</div>
</div>
);
};
export default Header;

View File

@@ -0,0 +1,69 @@
import React from 'react';
import '../../styles/components.css';
const Sidebar = ({ isMobileMenuOpen, onToggleMobileMenu }) => {
const navItems = [
{ icon: 'fas fa-tachometer-alt', label: 'Dashboard', active: true, badge: null },
{ icon: 'fas fa-list', label: 'Solicitudes Programadas', active: false, badge: '12' },
{ icon: 'fas fa-plus-circle', label: 'Crear Solicitud', active: false, badge: null },
{ icon: 'fas fa-history', label: 'Historial', active: false, badge: null },
{ icon: 'fas fa-cog', label: 'Configuración', active: false, badge: null },
{ icon: 'fas fa-question-circle', label: 'Ayuda', active: false, badge: null }
];
return (
<>
<button
className="menu-toggle"
id="menuToggle"
onClick={onToggleMobileMenu}
>
<i className="fas fa-bars"></i>
</button>
<div className={`sidebar ${isMobileMenuOpen ? 'active' : ''}`} id="sidebar">
<div className="logo-container">
<div className="logo">
<div className="logo-icon">
<i className="fas fa-clock"></i>
</div>
<div className="logo-text">Request <span>Scheduler</span></div>
</div>
</div>
<div className="nav-container">
<ul className="nav-links">
{navItems.map((item, index) => (
<li
key={index}
className={item.active ? 'active' : ''}
data-tooltip={item.label}
>
<a href="#">
<i className={item.icon}></i>
<span>{item.label}</span>
{item.badge && <span className="badge">{item.badge}</span>}
</a>
</li>
))}
</ul>
</div>
<div className="sidebar-footer">
<div className="user-info">
<div className="user-avatar">JP</div>
<div className="user-details">
<div className="user-name">Juan Pérez</div>
<div className="user-role">Administrador</div>
</div>
<a href="#" style={{color: 'white'}}>
<i className="fas fa-sign-out-alt"></i>
</a>
</div>
</div>
</div>
</>
);
};
export default Sidebar;

View File

@@ -0,0 +1,2 @@
export { default as Sidebar } from './Sidebar';
export { default as Header } from './Header';

View File

@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import '../../styles/components.css';
const RequestForm = () => {
const [formData, setFormData] = useState({
url: '',
method: 'POST',
headers: '',
body: '',
executeDate: '',
executeTime: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Aquí podrías enviar los datos al backend
// console.log('Form data', formData);
// Mostrar notificación de éxito
const notification = document.createElement('div');
notification.textContent = 'Solicitud guardada correctamente';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: var(--success);
color: white;
padding: 12px 16px;
border-radius: 8px;
box-shadow: var(--shadow);
z-index: 2000;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 2500);
};
return (
<div className="form-container">
<div className="form-header">
<h3><i className="fas fa-paper-plane"></i> Nueva Solicitud</h3>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>URL del Endpoint</label>
<input
type="url"
name="url"
value={formData.url}
onChange={handleChange}
placeholder="https://api.ejemplo.com/endpoint"
required
/>
</div>
<div className="form-row">
<div className="form-group">
<label>Método</label>
<select name="method" value={formData.method} onChange={handleChange}>
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
<option>PATCH</option>
</select>
</div>
<div className="form-group">
<label>Fecha de Ejecución</label>
<input
type="date"
name="executeDate"
value={formData.executeDate}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label>Hora de Ejecución</label>
<input
type="time"
name="executeTime"
value={formData.executeTime}
onChange={handleChange}
/>
</div>
</div>
<div className="form-group">
<label>Headers (JSON)</label>
<textarea
name="headers"
rows="3"
value={formData.headers}
onChange={handleChange}
placeholder='{"Authorization": "Bearer ..."}'
/>
</div>
<div className="form-group">
<label>Cuerpo (JSON)</label>
<textarea
name="body"
rows="4"
value={formData.body}
onChange={handleChange}
placeholder='{"campo": "valor"}'
/>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary">
<i className="fas fa-save"></i> Guardar
</button>
<button type="reset" className="btn btn-outline" onClick={() => setFormData({ url: '', method: 'POST', headers: '', body: '', executeDate: '', executeTime: '' })}>
<i className="fas fa-eraser"></i> Limpiar
</button>
</div>
</form>
</div>
);
};
export default RequestForm;

View File

@@ -0,0 +1,100 @@
import React from 'react';
import '../../styles/components.css';
const RequestTable = () => {
const requests = [
{
id: '#00125',
url: 'https://api.ejemplo.com/users',
method: 'POST',
date: '15/10/2023 14:30',
status: 'pending',
statusText: 'Pendiente'
},
{
id: '#00124',
url: 'https://api.ejemplo.com/products/update',
method: 'PUT',
date: '15/10/2023 12:15',
status: 'completed',
statusText: 'Completada'
},
{
id: '#00123',
url: 'https://api.ejemplo.com/orders',
method: 'GET',
date: '15/10/2023 10:45',
status: 'failed',
statusText: 'Fallida'
}
];
const getStatusIcon = (status) => {
switch (status) {
case 'pending': return 'fas fa-clock';
case 'completed': return 'fas fa-check';
case 'failed': return 'fas fa-exclamation-triangle';
default: return 'fas fa-circle';
}
};
return (
<div className="table-container">
<div className="table-header">
<h3>Solicitudes Recientes</h3>
<div className="table-actions">
<button className="btn btn-outline btn-sm">
<i className="fas fa-filter"></i> Filtrar
</button>
<button className="btn btn-outline btn-sm">
<i className="fas fa-download"></i> Exportar
</button>
</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>URL</th>
<th>Método</th>
<th>Fecha de Ejecución</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{requests.map((request, index) => (
<tr key={index}>
<td>{request.id}</td>
<td>{request.url}</td>
<td>{request.method}</td>
<td>{request.date}</td>
<td>
<span className={`status ${request.status}`}>
<i className={getStatusIcon(request.status)}></i> {request.statusText}
</span>
</td>
<td className="actions">
<button className="view" title="Ver detalles">
<i className="fas fa-eye"></i>
</button>
<button
className="execute"
title={request.status === 'completed' ? 'Ejecutar ahora' : 'Reintentar'}
disabled={request.status === 'completed'}
>
<i className={request.status === 'failed' ? 'fas fa-redo' : 'fas fa-play'}></i>
</button>
<button className="cancel" title="Cancelar">
<i className="fas fa-times"></i>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default RequestTable;

View File

@@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -1,17 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import './index.css'; import './styles/globals.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root')); const container = document.getElementById('root');
const root = createRoot(container);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>
); );
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

674
src/styles/components.css Normal file
View File

@@ -0,0 +1,674 @@
/* Sidebar Styles */
.sidebar {
width: var(--sidebar-width);
background: linear-gradient(180deg, var(--secondary) 0%, #1e293b 100%);
color: white;
padding: 0;
transition: var(--transition);
box-shadow: var(--shadow-lg);
z-index: 100;
position: fixed;
height: 100vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.logo-container {
padding: 24px 20px;
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
}
.logo-icon {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3);
}
.logo-text {
font-size: 1.25rem;
font-weight: 700;
}
.logo-text span {
color: var(--primary-light);
}
.nav-container {
flex: 1;
padding: 20px 0;
}
.nav-links {
list-style: none;
}
.nav-links li {
margin: 4px 16px;
border-radius: 10px;
overflow: hidden;
transition: var(--transition);
}
.nav-links li.active {
background: rgba(99, 102, 241, 0.15);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-links li:hover:not(.active) {
background: rgba(255, 255, 255, 0.05);
}
.nav-links a {
color: white;
text-decoration: none;
display: flex;
align-items: center;
padding: 14px 16px;
font-weight: 500;
transition: var(--transition);
}
.nav-links i {
margin-right: 12px;
font-size: 1.2rem;
width: 24px;
text-align: center;
}
.nav-links .badge {
margin-left: auto;
background: var(--primary);
color: white;
border-radius: 20px;
padding: 4px 10px;
font-size: 0.75rem;
font-weight: 600;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
margin-top: auto;
}
.user-info {
display: flex;
align-items: center;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
margin-right: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.user-details {
flex: 1;
}
.user-name {
font-weight: 600;
font-size: 0.9rem;
}
.user-role {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
}
/* Main Content */
.main-content {
flex: 1;
padding: 30px;
margin-left: var(--sidebar-width);
transition: var(--transition);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px 0;
}
.page-title {
color: var(--secondary);
font-weight: 700;
font-size: 1.8rem;
display: flex;
align-items: center;
}
.page-title i {
margin-right: 12px;
color: var(--primary);
background: rgba(99, 102, 241, 0.1);
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.search-box {
position: relative;
background: white;
border-radius: 10px;
padding: 10px 16px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
width: 300px;
}
.search-box i {
color: var(--gray);
margin-right: 10px;
}
.search-box input {
border: none;
outline: none;
background: transparent;
width: 100%;
font-size: 0.9rem;
}
/* Button Styles */
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: var(--transition);
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
font-size: 0.9rem;
}
.btn i {
margin-right: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(99, 102, 241, 0.3);
}
.btn-success {
background: linear-gradient(135deg, var(--success) 0%, #34d399 100%);
color: white;
}
.btn-warning {
background: linear-gradient(135deg, var(--warning) 0%, #fbbf24 100%);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, var(--danger) 0%, #f87171 100%);
color: white;
}
.btn-outline {
background: transparent;
border: 2px solid var(--primary);
color: var(--primary);
}
.btn-sm {
padding: 8px 16px;
font-size: 0.8rem;
}
/* Card Styles */
.dashboard-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
.card {
background: white;
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 24px;
transition: var(--transition);
border: 1px solid rgba(0,0,0,0.03);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
}
.card.primary::before {
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%);
}
.card.success::before {
background: linear-gradient(90deg, var(--success) 0%, #34d399 100%);
}
.card.warning::before {
background: linear-gradient(90deg, var(--warning) 0%, #fbbf24 100%);
}
.card.danger::before {
background: linear-gradient(90deg, var(--danger) 0%, #f87171 100%);
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.card-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-icon.primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
}
.card-icon.success {
background: linear-gradient(135deg, var(--success) 0%, #34d399 100%);
}
.card-icon.warning {
background: linear-gradient(135deg, var(--warning) 0%, #fbbf24 100%);
}
.card-icon.danger {
background: linear-gradient(135deg, var(--danger) 0%, #f87171 100%);
}
.card-value {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 5px;
color: var(--dark);
}
.card-title {
color: var(--gray);
font-size: 0.95rem;
font-weight: 500;
}
.card-trend {
display: flex;
align-items: center;
margin-top: 10px;
font-size: 0.85rem;
font-weight: 600;
}
.card-trend.up {
color: var(--success);
}
.card-trend.down {
color: var(--danger);
}
/* Section Title */
.section-title {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 20px;
color: var(--secondary);
display: flex;
align-items: center;
}
.section-title i {
margin-right: 10px;
color: var(--primary);
background: rgba(99, 102, 241, 0.1);
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
/* Table Styles */
.table-container {
background: white;
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 40px;
border: 1px solid rgba(0,0,0,0.03);
}
.table-header {
padding: 20px 24px;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.table-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 16px 24px;
text-align: left;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
th {
background-color: #f8fafc;
font-weight: 600;
color: var(--secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
tr:last-child td {
border-bottom: none;
}
tr:hover {
background-color: #f8fafc;
}
.status {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
display: inline-flex;
align-items: center;
}
.status i {
margin-right: 5px;
}
.status.pending {
background-color: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.status.completed {
background-color: rgba(16, 185, 129, 0.1);
color: var(--success);
}
.status.failed {
background-color: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
.actions {
display: flex;
gap: 8px;
}
.actions button {
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.actions .view {
background: rgba(99, 102, 241, 0.1);
color: var(--primary);
}
.actions .execute {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
}
.actions .cancel {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
.actions button:hover {
transform: scale(1.1);
}
/* Form Styles */
.form-container {
background: white;
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 30px;
margin-bottom: 40px;
border: 1px solid rgba(0,0,0,0.03);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(0,0,0,0.05);
flex-wrap: wrap;
gap: 16px;
}
.form-header h3 {
color: var(--secondary);
font-weight: 600;
display: flex;
align-items: center;
}
.form-header h3 i {
margin-right: 10px;
color: var(--primary);
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 10px;
font-weight: 600;
color: var(--secondary);
}
input, select, textarea {
width: 100%;
padding: 14px 16px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: var(--transition);
background: #f8fafc;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
background: white;
}
textarea {
min-height: 120px;
resize: vertical;
}
.form-row {
display: flex;
gap: 20px;
}
.form-row .form-group {
flex: 1;
}
/* Request Detail */
.detail-container {
background: white;
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 30px;
margin-bottom: 40px;
border: 1px solid rgba(0,0,0,0.03);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(0,0,0,0.05);
flex-wrap: wrap;
gap: 16px;
}
.detail-header h3 {
color: var(--secondary);
font-weight: 600;
display: flex;
align-items: center;
}
.detail-header h3 i {
margin-right: 10px;
color: var(--primary);
}
.detail-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.detail-section {
margin-bottom: 30px;
}
.detail-section h4 {
margin-bottom: 18px;
color: var(--secondary);
font-weight: 600;
display: flex;
align-items: center;
}
.detail-section h4 i {
margin-right: 8px;
color: var(--primary);
font-size: 0.9rem;
}
.detail-item {
display: flex;
margin-bottom: 12px;
padding: 10px 0;
}
.detail-label {
width: 180px;
font-weight: 600;
color: var(--dark);
}
.detail-value {
flex: 1;
}
.json-view {
background-color: #f8fafc;
padding: 18px;
border-radius: 8px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
border: 1px solid #e2e8f0;
font-size: 0.9rem;
}

274
src/styles/globals.css Normal file
View File

@@ -0,0 +1,274 @@
:root {
--primary: #6366f1;
--primary-light: #818cf8;
--primary-dark: #4f46e5;
--secondary: #1e293b;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
--light: #f8fafc;
--dark: #0f172a;
--gray: #64748b;
--gray-light: #cbd5e1;
--sidebar-width: 260px;
--header-height: 70px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', sans-serif;
}
body {
background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%);
color: var(--dark);
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
.container {
display: flex;
min-height: 100vh;
}
/* Responsive */
@media (max-width: 1200px) {
.sidebar {
width: 80px;
overflow: visible;
}
.logo-text, .nav-links span:not(.badge), .user-details {
display: none;
}
.logo-container {
justify-content: center;
padding: 20px 15px;
}
.logo-icon {
margin-right: 0;
}
.nav-links a {
justify-content: center;
padding: 16px;
}
.nav-links i {
margin-right: 0;
font-size: 1.4rem;
}
.nav-links .badge {
position: absolute;
top: 8px;
right: 8px;
font-size: 0.6rem;
padding: 2px 6px;
}
.main-content {
margin-left: 80px;
}
.nav-links li {
position: relative;
}
.nav-links li:hover::after {
content: attr(data-tooltip);
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
background: var(--dark);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.85rem;
white-space: nowrap;
z-index: 1000;
margin-left: 10px;
box-shadow: var(--shadow);
}
.sidebar-footer {
padding: 15px;
}
.user-info {
justify-content: center;
}
.user-avatar {
margin-right: 0;
}
}
@media (max-width: 992px) {
.dashboard-cards {
grid-template-columns: repeat(2, 1fr);
}
.form-row {
flex-direction: column;
gap: 0;
}
.search-box {
width: 250px;
}
}
@media (max-width: 768px) {
.dashboard-cards {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.header-actions {
width: 100%;
justify-content: space-between;
}
.search-box {
width: 100%;
max-width: 300px;
}
.table-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.table-actions {
width: 100%;
justify-content: space-between;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.detail-actions {
width: 100%;
justify-content: space-between;
}
.detail-item {
flex-direction: column;
gap: 5px;
}
.detail-label {
width: 100%;
}
}
@media (max-width: 576px) {
.main-content {
padding: 20px 15px;
margin-left: 0;
}
.sidebar {
width: 0;
transform: translateX(-100%);
}
.sidebar.active {
width: 280px;
transform: translateX(0);
}
.menu-toggle {
display: block;
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
background: var(--primary);
color: white;
border: none;
border-radius: 10px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
box-shadow: var(--shadow-lg);
transition: var(--transition);
}
.menu-toggle:hover {
transform: scale(1.05);
}
.header-actions {
flex-direction: column;
gap: 16px;
width: 100%;
}
.search-box {
max-width: 100%;
}
table {
display: block;
overflow-x: auto;
}
}
/* Toggle menu para móviles */
.menu-toggle {
display: none;
}
/* Animaciones */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card, .table-container, .form-container, .detail-container {
animation: fadeIn 0.5s ease-out;
}
/* Scrollbar personalizado */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-light);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary);
}