generated from Gallardo7761/miarma-template-full
Basic web structure
This commit is contained in:
415
index-orig.html
Normal file
415
index-orig.html
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Adeptus Miniaturium | Sanctus Painting</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Share Tech Mono';
|
||||||
|
src: url("https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Share+Tech+Mono&display=swap");
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--void-black: #050505;
|
||||||
|
--blood-god: #8a0b0b;
|
||||||
|
--plasma-glow: #ff3333;
|
||||||
|
--imperial-gold: #c6a34d;
|
||||||
|
--dirty-gold: #8c7335;
|
||||||
|
--plasteel: #2f353b;
|
||||||
|
--mechanicus-green: #23382c;
|
||||||
|
--parchment: #c2bbad;
|
||||||
|
--crt-line: rgba(18, 16, 16, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Share Tech Mono', monospace; /* Rollo pantalla de datos */
|
||||||
|
background-color: var(--void-black);
|
||||||
|
color: var(--parchment);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* EFECTO CRT / SCANLINES - Esto es bomba */
|
||||||
|
body::after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
||||||
|
z-index: 1000;
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background:
|
||||||
|
linear-gradient(to bottom, rgba(0,0,0,0.9), rgba(0,0,0,0.5)),
|
||||||
|
url('https://www.transparenttextures.com/patterns/dark-matter.png');
|
||||||
|
border-bottom: 4px double var(--imperial-gold);
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
text-shadow: 0 0 15px rgba(198, 163, 77, 0.6);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--blood-god);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 5px var(--blood-god);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NAVEGACION ESTILO PANEL DE CONTROL */
|
||||||
|
nav {
|
||||||
|
background-color: #0f1114;
|
||||||
|
border-bottom: 1px solid var(--dirty-gold);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
list-style: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
display: block;
|
||||||
|
padding: 15px 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #666;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-right: 1px solid #222;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover {
|
||||||
|
color: var(--parchment);
|
||||||
|
background-color: var(--blood-god);
|
||||||
|
box-shadow: inset 0 0 10px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a::before {
|
||||||
|
content: '[ ';
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.3s;
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
}
|
||||||
|
nav a::after {
|
||||||
|
content: ' ]';
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.3s;
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
}
|
||||||
|
nav a:hover::before, nav a:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 80px 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
section h2 {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
color: var(--parchment);
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
position: relative;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adornos a los lados del titulo */
|
||||||
|
section h2::before, section h2::after {
|
||||||
|
content: "+++";
|
||||||
|
color: var(--dirty-gold);
|
||||||
|
margin: 0 15px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TARJETAS TECH-HERESY - Con esquinas recortadas */
|
||||||
|
.card {
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid var(--dirty-gold);
|
||||||
|
padding: 40px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
position: relative;
|
||||||
|
/* Recorte estilo Sci-Fi */
|
||||||
|
clip-path: polygon(
|
||||||
|
20px 0, 100% 0,
|
||||||
|
100% calc(100% - 20px), calc(100% - 20px) 100%,
|
||||||
|
0 100%, 0 20px
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Linea decorativa interior */
|
||||||
|
.card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 5px; left: 5px; right: 5px; bottom: 5px;
|
||||||
|
border: 1px dashed #333;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
clip-path: polygon(
|
||||||
|
20px 0, 100% 0,
|
||||||
|
100% calc(100% - 20px), calc(100% - 20px) 100%,
|
||||||
|
0 100%, 0 20px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BOTONES PURITY SEAL */
|
||||||
|
.btn-imperial {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 40px;
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid var(--imperial-gold);
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.4s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-imperial:hover {
|
||||||
|
background: var(--blood-god);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--plasma-glow);
|
||||||
|
box-shadow: 0 0 20px var(--blood-god);
|
||||||
|
text-shadow: 0 0 5px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GRID DE FOTOS */
|
||||||
|
.grid-fotos {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-frame {
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid var(--plasteel);
|
||||||
|
padding: 5px;
|
||||||
|
background: #000;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-frame:hover {
|
||||||
|
border-color: var(--imperial-gold);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-placeholder {
|
||||||
|
height: 300px;
|
||||||
|
background: radial-gradient(circle, #222, #000);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #444;
|
||||||
|
border: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-placeholder span {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-caption {
|
||||||
|
background: var(--plasteel);
|
||||||
|
color: #aaa;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FORMULARIOS */
|
||||||
|
form input, form textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: rgba(0,0,0,0.7);
|
||||||
|
border: 1px solid var(--plasteel);
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:focus, form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--blood-god);
|
||||||
|
box-shadow: 0 0 10px rgba(138, 11, 11, 0.3);
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOOTER */
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #020202;
|
||||||
|
border-top: 1px solid var(--dirty-gold);
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skull-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--parchment);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMACIONES */
|
||||||
|
@keyframes textFlicker {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
95% { opacity: 1; }
|
||||||
|
96% { opacity: 0.8; }
|
||||||
|
97% { opacity: 1; }
|
||||||
|
98% { opacity: 0.5; }
|
||||||
|
99% { opacity: 1; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
|
animation: textFlicker 5s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Adeptus Miniaturium</h1>
|
||||||
|
<p>Sanctificando Plástico para la Gloria del Imperio</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#nexus">Nexus</a></li>
|
||||||
|
<li><a href="#pict-capturas">Pict-Capturas</a></li>
|
||||||
|
<li><a href="#vox-transmision">Vox-Comms</a></li>
|
||||||
|
<li><a href="#datos-biologis">Magos Marcos</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section id="nexus">
|
||||||
|
<h2>+++ Inicializando Lógica +++</h2>
|
||||||
|
<div class="card">
|
||||||
|
<p style="color: var(--imperial-gold); font-size: 0.9rem;">// PENSAMIENTO DEL DÍA: LA ESPERANZA ES EL PRIMER PASO HACIA LA DECEPCIÓN.</p>
|
||||||
|
<hr style="border: 0; border-top: 1px solid #333; margin: 20px 0;">
|
||||||
|
<p>Bienvenido al manufactorum personal del <strong>Artífice Marcos</strong>. Aquí, las miniaturas grises son purgadas de su falta de color y bendecidas con pigmentos sagrados, lavados de Nuln Oil y pincel seco ritual.</p>
|
||||||
|
<p>No pintamos juguetes. Forjamos veteranos de la Larga Guerra.</p>
|
||||||
|
<br>
|
||||||
|
<a href="#vox-transmision" class="btn-imperial">Iniciar Protocolo de Encargo</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="pict-capturas">
|
||||||
|
<h2>+++ Archivos de Batalla +++</h2>
|
||||||
|
<div class="grid-fotos">
|
||||||
|
<div class="foto-frame">
|
||||||
|
<div class="foto-placeholder">
|
||||||
|
<span>☠</span>
|
||||||
|
</div>
|
||||||
|
<div class="foto-caption">Muestra A-1: Astartes Pattern</div>
|
||||||
|
</div>
|
||||||
|
<div class="foto-frame">
|
||||||
|
<div class="foto-placeholder">
|
||||||
|
<span>⚙</span>
|
||||||
|
</div>
|
||||||
|
<div class="foto-caption">Muestra B-2: Engine War</div>
|
||||||
|
</div>
|
||||||
|
<div class="foto-frame">
|
||||||
|
<div class="foto-placeholder">
|
||||||
|
<span>⚔</span>
|
||||||
|
</div>
|
||||||
|
<div class="foto-caption">Muestra C-3: Xenos Filth</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="vox-transmision">
|
||||||
|
<h2>+++ Súplica al Manufactorum +++</h2>
|
||||||
|
<div class="card">
|
||||||
|
<p>Rellena los datos para solicitar la atención del Artífice. Sé preciso, el tiempo es un recurso limitado del Emperador.</p>
|
||||||
|
<form>
|
||||||
|
<input type="text" placeholder="Designación del Comandante (Nombre)">
|
||||||
|
<input type="email" placeholder="Frecuencia Vox (Email)">
|
||||||
|
<textarea rows="6" placeholder="Detalla los requerimientos de la misión: Esquema de color, Facción, Nivel de degradado..."></textarea>
|
||||||
|
<button type="submit" class="btn-imperial">Transmitir a la Noosphere</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="datos-biologis">
|
||||||
|
<h2>+++ Archivo: Magos Marcos +++</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
||||||
|
<div style="flex: 1; min-width: 200px;">
|
||||||
|
<p><strong>[ESTADO]</strong>: Operativo<br>
|
||||||
|
<strong>[UBICACIÓN]</strong>: Sector Baeticus (Andalucía)<br>
|
||||||
|
<strong>[ESPECIALIDAD]</strong>: Grimdark Realista, Weathering pesado, OSL.</p>
|
||||||
|
<p>Marcos no pinta para que queden bonitos en la estantería. Pinta para que parezca que tus muñecos han sobrevivido a un bombardeo orbital en Istvaan V. Aquí hay barro, sangre y oscuridad.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title></title>
|
<title>Adeptus Miniaturium</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" class="p-0 m-0"></div>
|
<div id="root" class="p-0 m-0"></div>
|
||||||
|
|||||||
30
src/App.jsx
30
src/App.jsx
@@ -1,28 +1,22 @@
|
|||||||
import Header from '@/components/Header.jsx';
|
import Footer from '@/components/Footer'
|
||||||
import NavBar from '@/components/NavBar.jsx';
|
import Header from '@/components/Header'
|
||||||
import Footer from '@/components/Footer.jsx';
|
import NavBar from '@/components/NavBar/NavBar'
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
import Home from '@/pages/Home'
|
||||||
import ProtectedRoute from '@/components/Auth/ProtectedRoute.jsx'
|
import { Route, Routes } from 'react-router-dom'
|
||||||
import useSessionRenewal from '@/hooks/useSessionRenewal'
|
import Login from './pages/Login'
|
||||||
import { CONSTANTS } from '@/util/constants'
|
|
||||||
|
|
||||||
import Home from '@/pages/Home.jsx'
|
|
||||||
import Building from '@/pages/Building.jsx'
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const routesWithFooter = ["/"];
|
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path='/' element={<Home />} />
|
||||||
<Route path="/*" element={<Building />} />
|
<Route path='/login' element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
{routesWithFooter.includes(useLocation().pathname) ? <Footer /> : null}
|
<Footer />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { useState, useRef, useEffect, cloneElement } from 'react';
|
|
||||||
import { Button } from 'react-bootstrap';
|
|
||||||
import { AnimatePresence, motion as _motion } from 'framer-motion';
|
|
||||||
import '@/css/AnimatedDropdown.css';
|
|
||||||
|
|
||||||
const AnimatedDropend = ({
|
|
||||||
trigger,
|
|
||||||
icon,
|
|
||||||
variant = "secondary",
|
|
||||||
className = "",
|
|
||||||
buttonStyle = "",
|
|
||||||
show,
|
|
||||||
onToggle,
|
|
||||||
onMouseEnter,
|
|
||||||
onMouseLeave,
|
|
||||||
children
|
|
||||||
}) => {
|
|
||||||
const isControlled = show !== undefined;
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const triggerRef = useRef(null);
|
|
||||||
const dropdownRef = useRef(null);
|
|
||||||
|
|
||||||
const actualOpen = isControlled ? show : open;
|
|
||||||
|
|
||||||
const toggle = (forceValue) => {
|
|
||||||
const newState = typeof forceValue === "boolean" ? forceValue : !actualOpen;
|
|
||||||
if (!isControlled) setOpen(newState);
|
|
||||||
onToggle?.(newState);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e) => {
|
|
||||||
if (
|
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(e.target) &&
|
|
||||||
!triggerRef.current?.contains(e.target)
|
|
||||||
) {
|
|
||||||
if (!isControlled) setOpen(false);
|
|
||||||
onToggle?.(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [isControlled, onToggle]);
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
if (!isControlled) setOpen(true);
|
|
||||||
onToggle?.(true);
|
|
||||||
onMouseEnter?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
if (!isControlled) setOpen(false);
|
|
||||||
onToggle?.(false);
|
|
||||||
onMouseLeave?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerElement = trigger
|
|
||||||
? (typeof trigger === "function"
|
|
||||||
? trigger({
|
|
||||||
onClick: e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggle();
|
|
||||||
},
|
|
||||||
ref: triggerRef
|
|
||||||
})
|
|
||||||
: cloneElement(trigger, {
|
|
||||||
onClick: e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggle();
|
|
||||||
},
|
|
||||||
ref: triggerRef
|
|
||||||
}))
|
|
||||||
: (
|
|
||||||
<Button
|
|
||||||
ref={triggerRef}
|
|
||||||
variant={variant}
|
|
||||||
className={`circle-btn ${buttonStyle}`}
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const dropdownClasses = `dropdown-menu show shadow rounded-4 px-2 py-2 ${className}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="position-relative d-inline-block dropend"
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
ref={triggerRef}
|
|
||||||
>
|
|
||||||
{triggerElement}
|
|
||||||
|
|
||||||
<AnimatePresence>
|
|
||||||
{actualOpen && (
|
|
||||||
<_motion.div
|
|
||||||
ref={dropdownRef}
|
|
||||||
initial={{ opacity: 0, x: -10 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -10 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
className={dropdownClasses}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
left: '100%',
|
|
||||||
zIndex: 1000,
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{typeof children === "function" ? children({ closeDropdown: () => toggle(false) }) : children}
|
|
||||||
</_motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AnimatedDropend;
|
|
||||||
@@ -54,7 +54,7 @@ const LoginForm = () => {
|
|||||||
return (
|
return (
|
||||||
<CustomContainer>
|
<CustomContainer>
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<div className="login-card card shadow p-5 rounded-5 mx-auto col-12 col-md-8 col-lg-6 col-xl-5 d-flex flex-column gap-4">
|
<div className="login-card card shadow rounded-0 mx-auto col-12 col-md-8 col-lg-6 col-xl-5 d-flex flex-column gap-4">
|
||||||
<h1 className="text-center">Inicio de sesión</h1>
|
<h1 className="text-center">Inicio de sesión</h1>
|
||||||
<Form className="d-flex flex-column gap-5" onSubmit={handleSubmit}>
|
<Form className="d-flex flex-column gap-5" onSubmit={handleSubmit}>
|
||||||
<div className="d-flex flex-column gap-3">
|
<div className="d-flex flex-column gap-3">
|
||||||
@@ -73,7 +73,7 @@ const LoginForm = () => {
|
|||||||
name="emailOrUserName"
|
name="emailOrUserName"
|
||||||
value={formState.emailOrUserName}
|
value={formState.emailOrUserName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="rounded-4"
|
className="rounded-0"
|
||||||
/>
|
/>
|
||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ const LoginForm = () => {
|
|||||||
name="password"
|
name="password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
|
{/*<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="keepLoggedIn"
|
name="keepLoggedIn"
|
||||||
@@ -92,10 +92,10 @@ const LoginForm = () => {
|
|||||||
value={formState.keepLoggedIn}
|
value={formState.keepLoggedIn}
|
||||||
onChange={(e) => { formState.keepLoggedIn = e.target.checked; setFormState({ ...formState }) }}
|
onChange={(e) => { formState.keepLoggedIn = e.target.checked; setFormState({ ...formState }) }}
|
||||||
/>
|
/>
|
||||||
{/*<Link disabled to="#" className="muted">
|
<Link disabled to="#" className="muted">
|
||||||
Olvidé mi contraseña
|
Olvidé mi contraseña
|
||||||
</Link>*/}
|
</Link>
|
||||||
</div>
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -105,7 +105,7 @@ const LoginForm = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Button type="submit" className="w-75 padding-4 rounded-4 border-0 shadow-sm login-button">
|
<Button type="submit" className="w-75 padding-4 rounded-0 border-0 shadow-sm login-button">
|
||||||
Iniciar sesión
|
Iniciar sesión
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
|
|||||||
value={value}
|
value={value}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="rounded-4 pe-5"
|
className="rounded-0 pe-5"
|
||||||
/>
|
/>
|
||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
const CustomContainer = ({ children }) => {
|
const CustomContainer = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<main className="px-4 py-5">
|
<main className="mx-4 my-5">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,56 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import '@/css/Footer.css'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import '@/css/Footer.css';
|
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => (
|
||||||
const [heart, setHeart] = useState('💜');
|
<footer className="text-center py-5">
|
||||||
|
<span className="skull-icon mb-2">☠</span>
|
||||||
useEffect(() => {
|
<p className="m-0">Adeptus Miniaturium © 41st Millennium</p>
|
||||||
const hearts = ["❤️", "💛", "🧡", "💚", "💙", "💜"];
|
</footer>
|
||||||
const randomHeart = () => hearts[Math.floor(Math.random() * hearts.length)];
|
);
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setHeart(randomHeart());
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<footer className="footer d-flex flex-column align-items-center gap-5 pt-5 px-4">
|
|
||||||
<div className="footer-columns w-100" style={{ maxWidth: '900px' }}>
|
|
||||||
<div className="footer-column">
|
|
||||||
<h4 className="footer-title">Contenido del footer</h4>
|
|
||||||
<div className="contact-info p-4">
|
|
||||||
<a
|
|
||||||
href="https://github.com/Gallardo7761"
|
|
||||||
target="_blank"
|
|
||||||
className='text-break d-block'
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faGithub} className="fa-icon me-2 " />
|
|
||||||
Gallardo7761
|
|
||||||
</a>
|
|
||||||
<a href="mailto:jose@miarma.net" className="text-break d-block">
|
|
||||||
<FontAwesomeIcon icon={faEnvelope} className="fa-icon me-2" />
|
|
||||||
jose@miarma.net
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="footer-bottom w-100 py-5 text-center">
|
|
||||||
<h6 id="devd" className='m-0'>
|
|
||||||
Hecho con <span className="heart-anim">{heart}</span> por{' '}
|
|
||||||
<a href="https://gallardo.dev" target="_blank" rel="noopener noreferrer">
|
|
||||||
Gallardo7761
|
|
||||||
</a>
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
import '@/css/Header.css';
|
import '@/css/Header.css';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => (
|
||||||
|
<header className="py-5 text-center position-relative">
|
||||||
return (
|
<h1 className="mb-2">Adeptus Miniaturium</h1>
|
||||||
<header className={`text-center bg-img`}>
|
<p className="m-0">SUBTITULO SUBTITULO SUBTITULO SUBTITULO</p>
|
||||||
<div className="m-0 p-5 mask">
|
</header>
|
||||||
<div className="d-flex flex-column justify-content-center align-items-center h-100">
|
);
|
||||||
<Link to='/' className='text-decoration-none'>
|
|
||||||
<h1 className='header-title m-0 text-white shadowed'>Tu página web</h1>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
23
src/components/Home/AboutMeSection.jsx
Normal file
23
src/components/Home/AboutMeSection.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import TechCard from "@/components/TechCard";
|
||||||
|
|
||||||
|
const AboutMeSection = () => (
|
||||||
|
<section id="datos-biologis" className="container py-5">
|
||||||
|
<h2 className="section-title text-center mb-5">+++ Archivo: Markcus +++</h2>
|
||||||
|
<TechCard>
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col-12">
|
||||||
|
<p>
|
||||||
|
<strong>[ESTADO]</strong>: Operativo<br />
|
||||||
|
<strong>[UBICACIÓN]</strong>: Andalucía<br />
|
||||||
|
<strong>[ESPECIALIDAD]</strong>: Grimdark Realista, Weathering pesado, OSL.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 m-0">
|
||||||
|
Markcus no pinta para que queden bonitos en la estantería. Pinta para que parezca que tus muñecos han sobrevivido a un bombardeo orbital en Istvaan V. Aquí hay barro, sangre y oscuridad.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TechCard>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AboutMeSection;
|
||||||
29
src/components/Home/SamplesSection.jsx
Normal file
29
src/components/Home/SamplesSection.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const SamplesSection = () => {
|
||||||
|
const muestras = [
|
||||||
|
{ icon: '☠', title: 'Muestra A-1: Astartes Pattern' },
|
||||||
|
{ icon: '⚙', title: 'Muestra B-2: Engine War' },
|
||||||
|
{ icon: '⚔', title: 'Muestra C-3: Xenos Filth' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="pict-capturas" className="container py-5">
|
||||||
|
<h2 className="section-title text-center mb-5">+++ Muestras +++</h2>
|
||||||
|
<div className="row g-4">
|
||||||
|
{muestras.map((m, i) => (
|
||||||
|
<div className="col-md-4" key={i}>
|
||||||
|
<div className="foto-frame d-flex flex-column">
|
||||||
|
<div className="foto-placeholder flex-grow-1">
|
||||||
|
<span>{m.icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="foto-caption">
|
||||||
|
{m.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SamplesSection;
|
||||||
22
src/components/Home/StartSection.jsx
Normal file
22
src/components/Home/StartSection.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import TechCard from "@/components/TechCard";
|
||||||
|
|
||||||
|
const StartSection = () => (
|
||||||
|
<section id="inicio" className="container py-5">
|
||||||
|
<h2 className="section-title text-center mb-5">+++ INICIALIZANDO +++</h2>
|
||||||
|
<TechCard>
|
||||||
|
<p style={{ color: 'var(--imperial-gold)', fontSize: '0.9rem' }}>
|
||||||
|
// PENSAMIENTO DEL DÍA: LA ESPERANZA ES EL PRIMER PASO HACIA LA DECEPCIÓN.
|
||||||
|
</p>
|
||||||
|
<hr style={{ border: 0, borderTop: '1px solid #333', margin: '20px 0' }} />
|
||||||
|
<p className="mb-4">
|
||||||
|
Bienvenido al manufactorum personal de <strong>Markcus</strong>. Aquí, las miniaturas grises son purgadas de su falta de color y bendecidas con pigmentos sagrados, lavados de Nuln Oil y pincel seco ritual.
|
||||||
|
</p>
|
||||||
|
<p className="mb-4">No pintamos juguetes. Forjamos veteranos de la Larga Guerra.</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<a href="#vox-transmision" className="btn-imperial">Iniciar Protocolo de Encargo</a>
|
||||||
|
</div>
|
||||||
|
</TechCard>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default StartSection;
|
||||||
24
src/components/Home/VoxSection.jsx
Normal file
24
src/components/Home/VoxSection.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import TechCard from "@/components/TechCard";
|
||||||
|
|
||||||
|
const VoxSection = () => (
|
||||||
|
<section id="vox-transmision" className="container py-5">
|
||||||
|
<h2 className="section-title text-center mb-5">+++ Súplica al Manufactorum +++</h2>
|
||||||
|
<TechCard>
|
||||||
|
<p className="mb-4">Rellena los datos para solicitar la atención del Artífice. Sé preciso, el tiempo es un recurso limitado del Emperador.</p>
|
||||||
|
<form>
|
||||||
|
<div className="mb-3">
|
||||||
|
<input type="text" className="tech-input" placeholder="Designación del Comandante (Nombre)" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<input type="email" className="tech-input" placeholder="Frecuencia Vox (Email)" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<textarea className="tech-textarea" rows="6" placeholder="Detalla los requerimientos de la misión: Esquema de color, Facción, Nivel de degradado..."></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="btn-imperial border-0">Transmitir a la Noosphere</button>
|
||||||
|
</form>
|
||||||
|
</TechCard>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default VoxSection;
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import {
|
|
||||||
faSignIn,
|
|
||||||
faUser,
|
|
||||||
faSignOut,
|
|
||||||
faHouse,
|
|
||||||
faList,
|
|
||||||
faBullhorn,
|
|
||||||
faFile
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
import '@/css/NavBar.css';
|
|
||||||
|
|
||||||
import ThemeButton from '@/components/ThemeButton.jsx';
|
|
||||||
|
|
||||||
import IfAuthenticated from '@/components/Auth/IfAuthenticated.jsx';
|
|
||||||
import IfNotAuthenticated from '@/components/Auth/IfNotAuthenticated.jsx';
|
|
||||||
import IfRole from '@/components/Auth/IfRole.jsx';
|
|
||||||
|
|
||||||
import { Navbar, Nav, Container } from 'react-bootstrap';
|
|
||||||
import AnimatedDropdown from '@/components/AnimatedDropdown.jsx';
|
|
||||||
|
|
||||||
import { CONSTANTS } from '@/util/constants.js';
|
|
||||||
|
|
||||||
const NavBar = () => {
|
|
||||||
const { user, logout } = useAuth();
|
|
||||||
const [showingUserDropdown, setShowingUserDropdown] = useState(false);
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const [isLg, setIsLg] = useState(window.innerWidth >= 992);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
setIsLg(window.innerWidth >= 992 && window.innerWidth < 1200);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleResize(); // inicializar
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
if (window.innerWidth >= 992) {
|
|
||||||
setExpanded(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Navbar expand="lg" sticky="top" expanded={expanded} onToggle={() => setExpanded(!expanded)}>
|
|
||||||
<Container fluid>
|
|
||||||
<Navbar.Toggle aria-controls="navbar" className="custom-toggler">
|
|
||||||
<svg width="30" height="30" viewBox="0 0 30 30">
|
|
||||||
<path
|
|
||||||
d="M4 7h22M4 15h22M4 23h22"
|
|
||||||
stroke="var(--navbar-link-color)"
|
|
||||||
strokeWidth="3"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeMiterlimit="10"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Navbar.Toggle>
|
|
||||||
|
|
||||||
<Navbar.Collapse id="main-navbar">
|
|
||||||
<Nav className="me-auto gap-2">
|
|
||||||
<Nav.Link
|
|
||||||
as={Link}
|
|
||||||
to="/"
|
|
||||||
title="Inicio"
|
|
||||||
href="/"
|
|
||||||
className={`text-truncate ${expanded ? "mt-3" : ""}`}
|
|
||||||
onClick={() => setExpanded(false)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faHouse} className="me-2" />
|
|
||||||
Inicio
|
|
||||||
</Nav.Link>
|
|
||||||
|
|
||||||
<div className="d-lg-none mt-2 ms-2">
|
|
||||||
<ThemeButton onlyIcon={isLg} />
|
|
||||||
</div>
|
|
||||||
</Nav>
|
|
||||||
</Navbar.Collapse>
|
|
||||||
|
|
||||||
<div className="d-none d-lg-block me-3">
|
|
||||||
<ThemeButton onlyIcon={isLg} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Nav className="d-flex flex-md-row flex-column gap-2 ms-auto align-items-center">
|
|
||||||
<IfAuthenticated>
|
|
||||||
<AnimatedDropdown
|
|
||||||
className='end-0 position-absolute'
|
|
||||||
show={showingUserDropdown}
|
|
||||||
onMouseEnter={() => setShowingUserDropdown(true)}
|
|
||||||
onMouseLeave={() => setShowingUserDropdown(false)}
|
|
||||||
onToggle={(isOpen) => setShowingUserDropdown(isOpen)}
|
|
||||||
trigger={
|
|
||||||
<Link className="nav-link dropdown-toggle fw-bold">
|
|
||||||
@{user?.user_name}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link to="/perfil" className="text-muted dropdown-item nav-link">
|
|
||||||
<FontAwesomeIcon icon={faUser} className="me-2" />
|
|
||||||
Mi perfil
|
|
||||||
</Link>
|
|
||||||
<hr className="dropdown-divider" />
|
|
||||||
<Link to="#" className="dropdown-item nav-link" onClick={logout}>
|
|
||||||
<FontAwesomeIcon icon={faSignOut} className="me-2" />
|
|
||||||
Cerrar sesión
|
|
||||||
</Link>
|
|
||||||
</AnimatedDropdown>
|
|
||||||
</IfAuthenticated>
|
|
||||||
|
|
||||||
<IfNotAuthenticated>
|
|
||||||
<Nav.Link as={Link} to="/login" title="Iniciar sesión">
|
|
||||||
<FontAwesomeIcon icon={faSignIn} className="me-2" />
|
|
||||||
Iniciar sesión
|
|
||||||
</Nav.Link>
|
|
||||||
</IfNotAuthenticated>
|
|
||||||
</Nav>
|
|
||||||
</Container>
|
|
||||||
</Navbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NavBar;
|
|
||||||
36
src/components/NavBar/NavBar.jsx
Normal file
36
src/components/NavBar/NavBar.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import '@/css/NavBar.css';
|
||||||
|
import NavItem from './NavItem';
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ label: 'Inicio', href: '/#inicio' },
|
||||||
|
{ label: 'Muestras', href: '/#muestras' },
|
||||||
|
{ label: 'Pedidos', href: '/#pedidos' },
|
||||||
|
{ label: 'Sobre Markcus', href: '/#sobre-mi' },
|
||||||
|
{ label: "🇪🇸", href: '#' },
|
||||||
|
{ label: "Login", href: '/login' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const NavBar = () => {
|
||||||
|
const centerItems = navItems.slice(0, navItems.length - 2);
|
||||||
|
const rightItems = navItems.slice(-2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="nav-container">
|
||||||
|
<ul className="d-flex align-items-center list-unstyled m-0 w-100 justify-content-center position-relative">
|
||||||
|
|
||||||
|
{centerItems.map((item, index) => (
|
||||||
|
<NavItem key={index} item={item} index={index} total={centerItems.length} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<li className="position-absolute end-0 d-flex">
|
||||||
|
{rightItems.map((item, index) => (
|
||||||
|
<NavItem key={index} item={item} index={index} total={rightItems.length} />
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBar;
|
||||||
15
src/components/NavBar/NavItem.jsx
Normal file
15
src/components/NavBar/NavItem.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const NavItem = ({ item, index, total }) => {
|
||||||
|
let borderClass = `${index === 0 ? "border-left border-right" : "border-right"}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={`mx-0 ${borderClass}`}>
|
||||||
|
<Link to={item.href} className="nav-link-custom">
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavItem;
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { faFilter, faFilePdf, faPlus } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import AnimatedDropdown from '@components/AnimatedDropdown';
|
|
||||||
import Button from 'react-bootstrap/Button';
|
|
||||||
import { CONSTANTS } from '@/util/constants';
|
|
||||||
import IfRole from '@/components/Auth/IfRole';
|
|
||||||
|
|
||||||
const SearchToolbar = ({ searchTerm, onSearchChange, filtersComponent, onCreate, onPDF }) => (
|
|
||||||
<div className="sticky-toolbar search-toolbar-wrapper">
|
|
||||||
<div className="search-toolbar">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="search-input"
|
|
||||||
placeholder="Buscar..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="toolbar-buttons">
|
|
||||||
{filtersComponent && (
|
|
||||||
<AnimatedDropdown variant="transparent" icon={<FontAwesomeIcon icon={faFilter} className='fa-md' />}>
|
|
||||||
{filtersComponent}
|
|
||||||
</AnimatedDropdown>
|
|
||||||
)}
|
|
||||||
{onPDF && (
|
|
||||||
<IfRole roles={[CONSTANTS.ROLE_ADMIN, CONSTANTS.ROLE_DEV]}>
|
|
||||||
<Button variant="transparent" onClick={onPDF}>
|
|
||||||
<FontAwesomeIcon icon={faFilePdf} className='fa-md' />
|
|
||||||
</Button>
|
|
||||||
</IfRole>
|
|
||||||
)}
|
|
||||||
{onCreate && (
|
|
||||||
<IfRole roles={[CONSTANTS.ROLE_ADMIN, CONSTANTS.ROLE_DEV]}>
|
|
||||||
<Button variant="transparent" onClick={onCreate}>
|
|
||||||
<FontAwesomeIcon icon={faPlus} className='fa-md' />
|
|
||||||
</Button>
|
|
||||||
</IfRole>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default SearchToolbar;
|
|
||||||
9
src/components/TechCard.jsx
Normal file
9
src/components/TechCard.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import '@/css/TechCard.css'
|
||||||
|
|
||||||
|
const TechCard = ({ children, className = '' }) => (
|
||||||
|
<div className={`tech-card ${className}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TechCard;
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { useTheme } from "@/hooks/useTheme.js";
|
|
||||||
import "@/css/ThemeButton.css";
|
|
||||||
|
|
||||||
export default function ThemeButton({ className, onlyIcon}) {
|
|
||||||
const { theme, toggleTheme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className={`theme-toggle ${className}`} onClick={toggleTheme}>
|
|
||||||
{
|
|
||||||
onlyIcon ? (
|
|
||||||
theme === "dark" ? ("🌞") : ("🌙")
|
|
||||||
) : (
|
|
||||||
theme === "dark" ? ("🌞 tema claro") : ("🌙 tema oscuro")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,12 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const axios = createAxiosInstance();
|
const axios = createAxiosInstance();
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
|
|
||||||
const [user, setUser] = useState(() => JSON.parse(localStorage.getItem("user")) || null);
|
|
||||||
const [token, setToken] = useState(() => localStorage.getItem("token"));
|
const [token, setToken] = useState(() => localStorage.getItem("token"));
|
||||||
|
const [identity, setIdentity] = useState(() => {
|
||||||
|
const stored = localStorage.getItem("identity");
|
||||||
|
return stored ? JSON.parse(stored) : null;
|
||||||
|
});
|
||||||
|
|
||||||
const [authStatus, setAuthStatus] = useState("checking");
|
const [authStatus, setAuthStatus] = useState("checking");
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const res = await axios.get(VALIDATE_URL, {
|
const res = await axios.get(VALIDATE_URL, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setAuthStatus("authenticated");
|
setAuthStatus("authenticated");
|
||||||
} else {
|
} else {
|
||||||
@@ -45,53 +50,121 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
const login = async (formData) => {
|
const login = async (formData) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const BASE_URL = config.apiConfig.baseUrl;
|
const BASE_URL = config.apiConfig.baseUrl;
|
||||||
const LOGIN_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.login}`;
|
const LOGIN_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.login}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(LOGIN_URL, formData);
|
const res = await axios.post(LOGIN_URL, formData);
|
||||||
const { token, member, tokenTime } = res.data.data;
|
|
||||||
|
const { token, user, account, metadata } = res.data;
|
||||||
|
|
||||||
|
const identity = {
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
localStorage.setItem("token", token);
|
localStorage.setItem("token", token);
|
||||||
localStorage.setItem("user", JSON.stringify(member));
|
localStorage.setItem("identity", JSON.stringify(identity));
|
||||||
localStorage.setItem("tokenTime", tokenTime);
|
|
||||||
|
|
||||||
setToken(token);
|
setToken(token);
|
||||||
setUser(member);
|
setIdentity(identity);
|
||||||
setAuthStatus("authenticated");
|
setAuthStatus("authenticated");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error al iniciar sesión:", err);
|
console.error("Error al iniciar sesión:", err);
|
||||||
|
|
||||||
let message = "Ha ocurrido un error inesperado.";
|
let message = "Ha ocurrido un error inesperado.";
|
||||||
|
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
const { status, data } = err.response;
|
const { status, data } = err.response;
|
||||||
|
|
||||||
if (status === 400) {
|
if (status === 400) {
|
||||||
message = "Usuario o contraseña incorrectos.";
|
message = "Usuario o contraseña incorrectos.";
|
||||||
} else if (status === 403) {
|
} else if (status === 403) {
|
||||||
message = "Tu cuenta está inactiva o ha sido suspendida.";
|
message = "Tu cuenta está inactiva o suspendida.";
|
||||||
} else if (status === 404) {
|
} else if (status === 404) {
|
||||||
message = "Usuario no encontrado.";
|
message = "Usuario no encontrado.";
|
||||||
} else if (data?.message) {
|
} else if (data?.message) {
|
||||||
message = data.message;
|
message = data.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setError(message);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (formData) => {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const BASE_URL = config.apiConfig.baseUrl;
|
||||||
|
const REGISTER_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.register}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post(REGISTER_URL, formData);
|
||||||
|
|
||||||
|
const { token, user, account, metadata } = res.data;
|
||||||
|
|
||||||
|
const identity = {
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
localStorage.setItem("identity", JSON.stringify(identity));
|
||||||
|
|
||||||
|
setToken(token);
|
||||||
|
setIdentity(identity);
|
||||||
|
setAuthStatus("authenticated");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error al registrarse:", err);
|
||||||
|
|
||||||
|
let message = "Ha ocurrido un error inesperado.";
|
||||||
|
|
||||||
|
if (err.response) {
|
||||||
|
const { status, data } = err.response;
|
||||||
|
|
||||||
|
if (status === 400) {
|
||||||
|
message = "Usuario o contraseña incorrectos.";
|
||||||
|
} else if (status === 403) {
|
||||||
|
message = "Tu cuenta está inactiva o suspendida.";
|
||||||
|
} else if (status === 404) {
|
||||||
|
message = "Usuario no encontrado.";
|
||||||
|
} else if (data?.message) {
|
||||||
|
message = data.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setError(message);
|
setError(message);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
localStorage.clear();
|
localStorage.removeItem("token");
|
||||||
setUser(null);
|
localStorage.removeItem("identity");
|
||||||
|
setIdentity(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setAuthStatus("unauthenticated");
|
setAuthStatus("unauthenticated");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearError = () => setError(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, token, authStatus, login, logout, error }}>
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
identity, // { user, account, metadata }
|
||||||
|
token,
|
||||||
|
authStatus,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
error,
|
||||||
|
clearError
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useData } from "../hooks/useData";
|
|||||||
|
|
||||||
export const DataContext = createContext();
|
export const DataContext = createContext();
|
||||||
|
|
||||||
export const DataProvider = ({ config, children }) => {
|
export const DataProvider = ({ config, onError, children }) => {
|
||||||
const data = useData(config);
|
const data = useData(config, onError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataContext.Provider value={data}>
|
<DataContext.Provider value={data}>
|
||||||
|
|||||||
40
src/context/ErrorContext.jsx
Normal file
40
src/context/ErrorContext.jsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { createContext, useState, useContext } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import NotificationModal from '@/components/NotificationModal';
|
||||||
|
|
||||||
|
const ErrorContext = createContext();
|
||||||
|
|
||||||
|
export const ErrorProvider = ({ children }) => {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const showError = (err) => {
|
||||||
|
setError({
|
||||||
|
title: err.status ? `Error ${err.status}` : "Error",
|
||||||
|
message: err.message,
|
||||||
|
variant: 'danger'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeError = () => setError(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorContext.Provider value={{ showError }}>
|
||||||
|
{children}
|
||||||
|
{error && (
|
||||||
|
<NotificationModal
|
||||||
|
show={true}
|
||||||
|
onClose={closeError}
|
||||||
|
title={error.title}
|
||||||
|
message={error.message}
|
||||||
|
variant='danger'
|
||||||
|
buttons={[{ label: "Aceptar", variant: "danger", onClick: closeError }]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ErrorContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ErrorProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useError = () => useContext(ErrorContext);
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { createContext, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export const ThemeContext = createContext();
|
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }) => {
|
|
||||||
const [theme, setTheme] = useState(() => {
|
|
||||||
return (
|
|
||||||
localStorage.getItem("theme") ||
|
|
||||||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = document.documentElement;
|
|
||||||
document.body.classList.remove("light", "dark");
|
|
||||||
document.body.classList.add(theme);
|
|
||||||
root.classList.remove("light", "dark");
|
|
||||||
root.classList.add(theme);
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
|
||||||
{children}
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +1,46 @@
|
|||||||
.dropdown-menu .dropdown-divider {
|
.dropdown-menu .dropdown-divider {
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid var(--dirty-gold);
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: var(--bg-color) !important;
|
background: #0f1114 !important;
|
||||||
color: var(--text-color) !important;
|
border: 1px solid var(--dirty-gold) !important;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.9);
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
clip-path: polygon(
|
||||||
|
10px 0, 100% 0,
|
||||||
|
100% calc(100% - 10px),
|
||||||
|
calc(100% - 10px) 100%,
|
||||||
|
0 100%, 0 10px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu.show {
|
.dropdown-menu.show {
|
||||||
background-color: var(--navbar-bg) !important;
|
background: #000 !important;
|
||||||
box-shadow: 0 5px 10px var(--shadow-color);
|
box-shadow: 0 0 25px rgba(138, 11, 11, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
background-color: var(--navbar-bg) !important;
|
background: transparent !important;
|
||||||
color: var(--navbar-dropdown-item-color);
|
color: var(--parchment) !important;
|
||||||
cursor: pointer;
|
font-family: 'Cinzel', serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-bottom: 1px solid #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item:hover {
|
.dropdown-item:hover {
|
||||||
background-color: var(--navbar-bg) !important;
|
background-color: var(--blood-god) !important;
|
||||||
color: var(--secondary-color) !important;
|
color: #fff !important;
|
||||||
|
text-shadow: 0 0 5px #fff;
|
||||||
|
box-shadow: inset 0 0 10px #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-item.disabled,
|
||||||
.disabled.text-muted {
|
.disabled.text-muted {
|
||||||
color: var(--muted-color) !important;
|
color: #444 !important;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/* ================================
|
|
||||||
BUILDING COMPONENT - VISUAL ONLY
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.building-container {
|
|
||||||
font-family: 'Product Sans', sans-serif;
|
|
||||||
color: var(--fg-color);
|
|
||||||
animation: fadeInScale 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.building-icon {
|
|
||||||
font-size: 4rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
animation: bounce 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.building-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.building-subtitle {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animaciones */
|
|
||||||
@keyframes fadeInScale {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateY(-8px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +1,12 @@
|
|||||||
.footer {
|
footer {
|
||||||
background-color: var(--navbar-bg); /* más similar al navbar */
|
background-color: #020202;
|
||||||
color: var(--fg-color);
|
border-top: 1px solid var(--dirty-gold);
|
||||||
border-top: 3px solid var(--border-color);
|
color: #555;
|
||||||
font-size: 1rem;
|
font-size: 0.9rem;
|
||||||
box-shadow: 0 -2px 8px var(--shadow-color);
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer::before {
|
.skull-icon {
|
||||||
content: "";
|
font-size: 2rem;
|
||||||
position: absolute;
|
color: var(--parchment);
|
||||||
top: 0;
|
display: block;
|
||||||
left: 0;
|
}
|
||||||
width: 100%;
|
|
||||||
height: 12px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
opacity: 0.25;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-title,
|
|
||||||
.footer h6#devd {
|
|
||||||
font-family: "Product Sans";
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-columns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
background-color: var(--bg-hover-color); /* sutil contraste */
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow: 0 4px 10px rgba(0,0,0,0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.footer-columns {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-column {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-column h5 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-column ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-column ul li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-column ul li a {
|
|
||||||
color: var(--fg-color);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-column ul li a:hover {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-shadow: 0 0 4px currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
opacity: 0.85;
|
|
||||||
text-align: center;
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom a {
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--fg-color);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom a:hover {
|
|
||||||
text-shadow: 0 0 5px currentColor;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
background-color: var(--contact-info-bg);
|
|
||||||
color: var(--primary-color);
|
|
||||||
box-shadow: 0 4px 10px var(--shadow-color);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info a {
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--primary-color);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info a:hover {
|
|
||||||
transform: translateX(8px);
|
|
||||||
color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info .fa-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heart-anim {
|
|
||||||
display: inline-block;
|
|
||||||
animation: heartbeat 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes heartbeat {
|
|
||||||
0%, 100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,26 @@
|
|||||||
/* ================================
|
header {
|
||||||
HEADER - ESTILO BASE
|
background:
|
||||||
================================== */
|
linear-gradient(to bottom, rgba(0,0,0,0.9), rgba(0,0,0,0.5)),
|
||||||
|
url('https://www.transparenttextures.com/patterns/dark-matter.png');
|
||||||
.bg-img {
|
border-bottom: 4px double var(--imperial-gold);
|
||||||
background-image: url('/images/bg.png');
|
box-shadow: 0 10px 30px rgba(0,0,0,0.9);
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*.mask {
|
header h1 {
|
||||||
background-color: var(--header-mask-color);
|
font-family: 'Cinzel', serif;
|
||||||
backdrop-filter: blur(4px);
|
font-size: 3.5rem;
|
||||||
-webkit-backdrop-filter: blur(4px);
|
font-weight: 900;
|
||||||
|
color: var(--imperial-gold);
|
||||||
}
|
text-transform: uppercase;
|
||||||
*/
|
letter-spacing: 5px;
|
||||||
|
text-shadow: 0 0 15px rgba(198, 163, 77, 0.6);
|
||||||
.header-title {
|
|
||||||
font-family: 'Product Sans';
|
|
||||||
font-size: 3em;
|
|
||||||
font-weight: bolder;
|
|
||||||
color: var(--text-color);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadowed {
|
header p {
|
||||||
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
|
font-size: 1.2rem;
|
||||||
}
|
color: var(--blood-god);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
/* ================================
|
font-weight: bold;
|
||||||
RESPONSIVE HEADER TITLE
|
text-shadow: 0 0 5px var(--blood-god);
|
||||||
================================== */
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header-title {
|
|
||||||
font-size: 2em;
|
|
||||||
padding: 0 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +1,76 @@
|
|||||||
/* ================================
|
|
||||||
LOGIN - CARD CONTAINER (VISUAL)
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
background-color: var(--login-bg) !important;
|
background: #111 !important;
|
||||||
color: var(--text-color);
|
border: 1px solid var(--dirty-gold);
|
||||||
box-shadow: 0 0 10px var(--shadow-color);
|
color: var(--parchment);
|
||||||
|
padding: 40px;
|
||||||
|
clip-path: polygon(
|
||||||
|
20px 0, 100% 0,
|
||||||
|
100% calc(100% - 20px),
|
||||||
|
calc(100% - 20px) 100%,
|
||||||
|
0 100%, 0 20px
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 25px rgba(0,0,0,0.9);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
.login-card::before {
|
||||||
INPUTS VISUALES
|
content: "";
|
||||||
================================== */
|
position: absolute;
|
||||||
|
inset: 6px;
|
||||||
|
border: 1px dashed #222;
|
||||||
|
pointer-events: none;
|
||||||
|
clip-path: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
input.form-control {
|
input.form-control {
|
||||||
background-color: var(--input-bg);
|
background-color: rgba(0,0,0,0.8) !important;
|
||||||
color: var(--input-text);
|
border: 1px solid var(--plasteel) !important;
|
||||||
border: 1px solid var(--input-border);
|
color: var(--imperial-gold) !important;
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
input.form-control:focus {
|
||||||
LABELS PERSONALIZADAS
|
background-color: #000 !important;
|
||||||
================================== */
|
border-color: var(--blood-god) !important;
|
||||||
|
box-shadow: 0 0 10px rgba(138, 11, 11, 0.4) !important;
|
||||||
.form-floating>label {
|
color: var(--imperial-gold) !important;
|
||||||
font-family: 'Product Sans';
|
|
||||||
font-size: 1.1em;
|
|
||||||
color: var(--label-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-floating>label::after {
|
.form-floating > label {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--dirty-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > label::after {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
|
||||||
BOTÓN VISUAL
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
font-family: 'Product Sans' !important;
|
font-family: 'Cinzel', serif !important;
|
||||||
font-size: 1.3em !important;
|
font-weight: 700 !important;
|
||||||
font-weight: bold !important;
|
letter-spacing: 2px !important;
|
||||||
background-color: var(--login-btn-bg) !important;
|
text-transform: uppercase;
|
||||||
color: var(--login-btn-text) !important;
|
background: #000 !important;
|
||||||
|
border: 1px solid var(--imperial-gold) !important;
|
||||||
|
color: var(--imperial-gold) !important;
|
||||||
|
padding: 15px 40px !important;
|
||||||
|
clip-path: polygon(
|
||||||
|
10px 0, 100% 0,
|
||||||
|
100% calc(100% - 10px),
|
||||||
|
calc(100% - 10px) 100%,
|
||||||
|
0 100%, 0 10px
|
||||||
|
);
|
||||||
|
transition: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-button:hover {
|
.login-button:hover {
|
||||||
background-color: var(--login-btn-hover) !important;
|
background: var(--blood-god) !important;
|
||||||
color: var(--login-btn-text-hover) !important;
|
border-color: var(--plasma-glow) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 0 20px var(--blood-god);
|
||||||
|
text-shadow: 0 0 5px #fff;
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,37 @@
|
|||||||
/* ================================
|
.nav-container {
|
||||||
NAVBAR - VISUAL + THEMING ONLY
|
background-color: #0f1114;
|
||||||
================================== */
|
border-bottom: 1px solid var(--dirty-gold);
|
||||||
|
position: sticky;
|
||||||
.navbar {
|
top: 0;
|
||||||
background-color: var(--navbar-bg) !important;
|
z-index: 900;
|
||||||
box-shadow: var(--navbar-shadow);
|
|
||||||
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.nav-link-custom {
|
||||||
color: var(--navbar-brand-color) !important;
|
display: block;
|
||||||
|
padding: 15px 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #666;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: 0.3s;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand:hover {
|
li.border-left .nav-link-custom {
|
||||||
color: var(--navbar-brand-hover) !important;
|
border-left: 1px solid #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nav-link,
|
li.border-right .nav-link-custom {
|
||||||
.nav-item > a.nav-link,
|
border-right: 1px solid #222;
|
||||||
.dropdown-item {
|
|
||||||
font-family: "Product Sans";
|
|
||||||
font-size: larger;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
|
|
||||||
color: var(--navbar-link-color) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:hover,
|
.nav-link-custom:hover {
|
||||||
.nav-link:focus {
|
color: var(--parchment);
|
||||||
background-color: var(--navbar-link-hover-bg) !important;
|
background-color: var(--blood-god);
|
||||||
color: var(--navbar-link-hover-color) !important;
|
box-shadow: inset 0 0 10px #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
.nav-link-custom::before { content: '[ '; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
|
||||||
border-top: 1px solid var(--navbar-divider-color);
|
.nav-link-custom::after { content: ' ]'; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
|
||||||
|
.nav-link-custom:hover::before, .nav-link-custom:hover::after { opacity: 1; }
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
ANIMACIONES
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
src/css/TechCard.css
Normal file
31
src/css/TechCard.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.tech-card {
|
||||||
|
background: #111 !important;
|
||||||
|
border: 1px solid var(--dirty-gold);
|
||||||
|
padding: 40px;
|
||||||
|
position: relative;
|
||||||
|
clip-path: polygon(
|
||||||
|
20px 0, 100% 0,
|
||||||
|
100% calc(100% - 20px), calc(100% - 20px) 100%,
|
||||||
|
0 100%, 0 20px
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 5px; left: 5px; right: 5px; bottom: 5px;
|
||||||
|
border: 1px dashed #333;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
clip-path: polygon(
|
||||||
|
20px 0, 100% 0,
|
||||||
|
100% calc(100% - 20px), calc(100% - 20px) 100%,
|
||||||
|
0 100%, 0 20px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-card p {
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* ================================
|
|
||||||
THEME TOGGLE - BASE
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.theme-toggle {
|
|
||||||
width: auto;
|
|
||||||
height: 40px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
display: flex;
|
|
||||||
padding: 0 1rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
background-color: var(--toggle-bg);
|
|
||||||
color: var(--toggle-fg);
|
|
||||||
font-family: 'Product Sans';
|
|
||||||
font-size: 1.2rem;
|
|
||||||
transition:
|
|
||||||
background-color 0.3s ease,
|
|
||||||
color 0.3s ease,
|
|
||||||
transform 0.2s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
HOVER / ACTIVE STATES
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.theme-toggle:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
LIGHT THEME
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.light {
|
|
||||||
--toggle-bg: #1e1e1e;
|
|
||||||
--toggle-fg: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .theme-toggle {
|
|
||||||
box-shadow: 0 0px 10px rgba(68, 7, 182, 0.808);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
DARK THEME
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--toggle-bg: #f0f0f0;
|
|
||||||
--toggle-fg: #1e1e1e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .theme-toggle {
|
|
||||||
box-shadow: 0 0px 10px rgba(206, 180, 36, 0.589);
|
|
||||||
}
|
|
||||||
@@ -1,230 +1,153 @@
|
|||||||
/* ================================
|
|
||||||
FUENTES PERSONALIZADAS
|
|
||||||
================================== */
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Share Tech Mono';
|
||||||
src: url('/fonts/OpenSans.ttf');
|
src: url("https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Share+Tech+Mono&display=swap");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Product Sans";
|
|
||||||
src: url('/fonts/ProductSansRegular.ttf');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Product Sans Italic";
|
|
||||||
src: url('/fonts/ProductSansItalic.ttf');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Product Sans Italic Bold";
|
|
||||||
src: url('/fonts/ProductSansBoldItalic.ttf');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Product Sans Bold";
|
|
||||||
src: url('/fonts/ProductSansBold.ttf');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
PALETA DE COLORES
|
|
||||||
================================== */
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--highlight-border: var(--accent-color);
|
--void-black: #050505;
|
||||||
--box-shadow-soft: 0 4px 6px var(--shadow-color);
|
--blood-god: #8a0b0b;
|
||||||
--alert-bg: #f8d7da;
|
--plasma-glow: #ff3333;
|
||||||
}
|
--imperial-gold: #c6a34d;
|
||||||
|
--dirty-gold: #8c7335;
|
||||||
.light {
|
--plasteel: #2f353b;
|
||||||
--primary-color: #333;
|
--mechanicus-green: #23382c;
|
||||||
--secondary-color: #555;
|
--parchment: #c2bbad;
|
||||||
--tertiary-color: #777;
|
--crt-line: rgba(18, 16, 16, 0.5);
|
||||||
--border-color: #ccc;
|
|
||||||
--divider-color: #ddd;
|
|
||||||
--bg-color: #fff;
|
|
||||||
--fg-color: #111;
|
|
||||||
--text-color: #111;
|
|
||||||
--muted-color: #666;
|
|
||||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
|
||||||
--bg-hover-color: #f0f0f0;
|
|
||||||
--bg-search-bar: rgba(0,0,0,0.05);
|
|
||||||
--input-bg: #fff;
|
|
||||||
--input-border: #ccc;
|
|
||||||
--placeholder-color: #999;
|
|
||||||
--input-text: var(--text-color);
|
|
||||||
--accent-color: #333;
|
|
||||||
--btn-bg: #333;
|
|
||||||
--btn-bg-hover: #555;
|
|
||||||
--btn-text: #fff;
|
|
||||||
--btn-text-hover: #fff;
|
|
||||||
--icon-color: var(--fg-color);
|
|
||||||
--highlight-border: #777;
|
|
||||||
--card-bg: #fff;
|
|
||||||
--card-button: #fff;
|
|
||||||
--card-border: #ccc;
|
|
||||||
--card-text: var(--text-color);
|
|
||||||
--card-text-secondary: #555;
|
|
||||||
--card-btn-hover: rgba(0,0,0,0.05);
|
|
||||||
--card-muted-text: #666;
|
|
||||||
--item-bg: #fff;
|
|
||||||
--item-text: var(--text-color);
|
|
||||||
--subtitle-color: #666;
|
|
||||||
--login-bg: #f9f9f9;
|
|
||||||
--label-color: var(--text-color);
|
|
||||||
--login-btn-bg: #333;
|
|
||||||
--login-btn-hover: #555;
|
|
||||||
--login-btn-text: #fff;
|
|
||||||
--login-btn-text-hover: #111;
|
|
||||||
--header-mask-color: rgba(0,0,0,0.1);
|
|
||||||
--navbar-bg: #fff;
|
|
||||||
--navbar-brand-color: #333;
|
|
||||||
--navbar-brand-hover: #555;
|
|
||||||
--navbar-link-color: #111;
|
|
||||||
--navbar-link-hover-bg: #f0f0f0;
|
|
||||||
--navbar-link-hover-color: #333;
|
|
||||||
--navbar-dropdown-bg: #fff;
|
|
||||||
--navbar-dropdown-item-color: #111;
|
|
||||||
--navbar-dropdown-item-hover-color: #333;
|
|
||||||
--navbar-divider-color: #ccc;
|
|
||||||
--hamburger-color: #333;
|
|
||||||
--navbar-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
|
||||||
--show-btn-color: #333;
|
|
||||||
--show-btn-hover: #555;
|
|
||||||
--header-btn-hover: rgba(0,0,0,0.05);
|
|
||||||
--list-hover-bg: rgba(0,0,0,0.03);
|
|
||||||
--list-hover-bg-light: #f5f5f5;
|
|
||||||
--list-active-bg-light: #e0e0e0;
|
|
||||||
--search-bg: rgba(255,255,255,0.6);
|
|
||||||
--search-border: #ccc;
|
|
||||||
--search-input-color: #111;
|
|
||||||
--search-placeholder: #999;
|
|
||||||
--toolbar-btn-color: #111;
|
|
||||||
--toolbar-btn-hover: rgba(0,0,0,0.07);
|
|
||||||
--modal-bg: #fff;
|
|
||||||
--modal-header-border: #ccc;
|
|
||||||
--modal-body-bg: #fff;
|
|
||||||
--modal-close-color: #111;
|
|
||||||
--contact-info-bg: #f5f5f5;
|
|
||||||
--balance-report-bg: #fff;
|
|
||||||
--file-card-bg: #fff;
|
|
||||||
--sidebar-bg: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--primary-color: #eee;
|
|
||||||
--secondary-color: #ccc;
|
|
||||||
--tertiary-color: #999;
|
|
||||||
--border-color: #444;
|
|
||||||
--divider-color: #555;
|
|
||||||
--bg-color: #111;
|
|
||||||
--fg-color: #fff;
|
|
||||||
--text-color: #fff;
|
|
||||||
--muted-color: #aaa;
|
|
||||||
--shadow-color: rgba(0,0,0,0.5);
|
|
||||||
--bg-hover-color: #222;
|
|
||||||
--bg-search-bar: rgba(255,255,255,0.05);
|
|
||||||
--input-bg: #222;
|
|
||||||
--input-border: #555;
|
|
||||||
--placeholder-color: #888;
|
|
||||||
--input-text: #fff;
|
|
||||||
--accent-color: #eee;
|
|
||||||
--btn-bg: #eee;
|
|
||||||
--btn-bg-hover: #ccc;
|
|
||||||
--btn-text: #111;
|
|
||||||
--btn-text-hover: #000;
|
|
||||||
--icon-color: #fff;
|
|
||||||
--highlight-border: #999;
|
|
||||||
--alert-bg: #500;
|
|
||||||
--card-bg: #222;
|
|
||||||
--card-button: #222;
|
|
||||||
--card-border: #555;
|
|
||||||
--card-text: #fff;
|
|
||||||
--card-text-secondary: #ccc;
|
|
||||||
--card-btn-hover: rgba(255,255,255,0.05);
|
|
||||||
--item-bg: #222;
|
|
||||||
--item-text: #fff;
|
|
||||||
--subtitle-color: #aaa;
|
|
||||||
--login-bg: #111;
|
|
||||||
--label-color: #fff;
|
|
||||||
--login-btn-bg: #eee;
|
|
||||||
--login-btn-hover: #ccc;
|
|
||||||
--login-btn-text: #111;
|
|
||||||
--login-btn-text-hover: #000;
|
|
||||||
--header-mask-color: rgba(0,0,0,0.3);
|
|
||||||
--navbar-bg: #111;
|
|
||||||
--navbar-brand-color: #eee;
|
|
||||||
--navbar-brand-hover: #ccc;
|
|
||||||
--navbar-link-color: #fff;
|
|
||||||
--navbar-link-hover-bg: #222;
|
|
||||||
--navbar-link-hover-color: #ccc;
|
|
||||||
--navbar-dropdown-bg: #222;
|
|
||||||
--navbar-dropdown-item-color: #fff;
|
|
||||||
--navbar-dropdown-item-hover-color: #ccc;
|
|
||||||
--navbar-divider-color: #555;
|
|
||||||
--hamburger-color: #eee;
|
|
||||||
--navbar-shadow: 0 2px 5px rgba(0,0,0,0.5);
|
|
||||||
--show-btn-color: #eee;
|
|
||||||
--show-btn-hover: #ccc;
|
|
||||||
--card-muted-text: #aaa;
|
|
||||||
--header-btn-hover: rgba(255,255,255,0.05);
|
|
||||||
--list-hover-bg: rgba(255,255,255,0.03);
|
|
||||||
--list-hover-bg-dark: #333;
|
|
||||||
--list-active-bg-dark: #444;
|
|
||||||
--search-bg: rgba(255,255,255,0.1);
|
|
||||||
--search-border: #555;
|
|
||||||
--search-input-color: #fff;
|
|
||||||
--search-placeholder: #888;
|
|
||||||
--toolbar-btn-color: #fff;
|
|
||||||
--toolbar-btn-hover: rgba(255,255,255,0.08);
|
|
||||||
--modal-bg: #222;
|
|
||||||
--modal-header-border: #555;
|
|
||||||
--modal-body-bg: #222;
|
|
||||||
--modal-close-color: #fff;
|
|
||||||
--contact-info-bg: #111;
|
|
||||||
--balance-report-bg: #222;
|
|
||||||
--file-card-bg: #222;
|
|
||||||
--sidebar-bg: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
|
||||||
ESTILOS BASE / RESET SUAVE
|
|
||||||
================================== */
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: transparent !important; /* compatibilidad navbar fija */
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
background-color: var(--void-black);
|
||||||
|
color: var(--parchment);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
/* CRT */
|
||||||
color: var(--text-color);
|
body::after {
|
||||||
background-color: var(--bg-color);
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
||||||
|
z-index: 1000;
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tipografía global */
|
/* SECCIONES Y TITULOS */
|
||||||
div,
|
.section-title {
|
||||||
label,
|
font-family: 'Cinzel', serif;
|
||||||
input,
|
font-size: 2.5rem;
|
||||||
p,
|
color: var(--parchment);
|
||||||
span,
|
text-transform: uppercase;
|
||||||
a,
|
position: relative;
|
||||||
button {
|
letter-spacing: 2px;
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
.section-title::before, .section-title::after {
|
||||||
h2,
|
content: "+++";
|
||||||
h3,
|
color: var(--dirty-gold);
|
||||||
h4,
|
margin: 0 15px;
|
||||||
h5,
|
font-size: 1.5rem;
|
||||||
h6 {
|
vertical-align: middle;
|
||||||
font-family: "Product Sans", sans-serif;
|
}
|
||||||
color: var(--text-color);
|
|
||||||
}
|
/* BOTONES */
|
||||||
|
.btn-imperial {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 40px;
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid var(--imperial-gold);
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.4s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-imperial:hover {
|
||||||
|
background: var(--blood-god);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--plasma-glow);
|
||||||
|
box-shadow: 0 0 20px var(--blood-god);
|
||||||
|
text-shadow: 0 0 5px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOTOS */
|
||||||
|
.foto-frame {
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid var(--plasteel);
|
||||||
|
padding: 5px;
|
||||||
|
background: #000;
|
||||||
|
transition: 0.3s;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-frame:hover {
|
||||||
|
border-color: var(--imperial-gold);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-placeholder {
|
||||||
|
height: 300px;
|
||||||
|
background: radial-gradient(circle, #222, #000);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #444;
|
||||||
|
border: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-placeholder span {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foto-caption {
|
||||||
|
background: var(--plasteel);
|
||||||
|
color: #aaa;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FORMULARIO */
|
||||||
|
.tech-input, .tech-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: rgba(0,0,0,0.7);
|
||||||
|
border: 1px solid var(--plasteel);
|
||||||
|
color: var(--imperial-gold);
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-input:focus, .tech-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--blood-god);
|
||||||
|
box-shadow: 0 0 10px rgba(138, 11, 11, 0.3);
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textFlicker {
|
||||||
|
0%, 95%, 97%, 99%, 100% { opacity: 1; }
|
||||||
|
96% { opacity: 0.8; }
|
||||||
|
98% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 { animation: textFlicker 5s infinite; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { AuthContext } from "@/context/AuthContext";
|
import { AuthContext } from "../context/AuthContext";
|
||||||
|
|
||||||
export const useAuth = () => useContext(AuthContext);
|
export const useAuth = () => useContext(AuthContext);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { ConfigContext } from "@/context/ConfigContext.jsx";
|
import { ConfigContext } from "../context/ConfigContext.jsx";
|
||||||
|
|
||||||
export const useConfig = () => useContext(ConfigContext);
|
export const useConfig = () => useContext(ConfigContext);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const useData = (config) => {
|
export const useData = (config, onError) => {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [dataLoading, setLoading] = useState(true);
|
const [dataLoading, setLoading] = useState(true);
|
||||||
const [dataError, setError] = useState(null);
|
const [dataError, setError] = useState(null);
|
||||||
@@ -13,10 +13,59 @@ export const useData = (config) => {
|
|||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const getAuthHeaders = () => ({
|
const getAuthHeaders = (isFormData = false) => {
|
||||||
"Content-Type": "application/json",
|
const token = localStorage.getItem("token");
|
||||||
"Authorization": `Bearer ${localStorage.getItem("token")}`,
|
|
||||||
});
|
const headers = {};
|
||||||
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
|
if (!isFormData) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAxiosError = (err) => {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
const data = err.response.data;
|
||||||
|
|
||||||
|
if (data.status === 422 && data.errors) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
errors: data.errors,
|
||||||
|
path: data.path ?? null,
|
||||||
|
timestamp: data.timestamp ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: data.status ?? err.response.status,
|
||||||
|
error: data.error ?? null,
|
||||||
|
message: data.message ?? err.response.statusText ?? "Error desconocido",
|
||||||
|
path: data.path ?? null,
|
||||||
|
timestamp: data.timestamp ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.request) {
|
||||||
|
return {
|
||||||
|
status: null,
|
||||||
|
error: "Network Error",
|
||||||
|
message: "No se pudo conectar al servidor",
|
||||||
|
path: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: null,
|
||||||
|
error: "Client Error",
|
||||||
|
message: err.message || "Error desconocido",
|
||||||
|
path: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
const current = configRef.current;
|
const current = configRef.current;
|
||||||
@@ -30,101 +79,62 @@ export const useData = (config) => {
|
|||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
params: current.params,
|
params: current.params,
|
||||||
});
|
});
|
||||||
setData(response.data.data);
|
setData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.message || err.message);
|
const error = handleAxiosError(err);
|
||||||
|
setError(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.baseUrl) {
|
if (config?.baseUrl) fetchData();
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
}, [config, fetchData]);
|
}, [config, fetchData]);
|
||||||
|
|
||||||
const getData = async (url, params = {}) => {
|
const requestWrapper = async (method, endpoint, payload = null, refresh = false) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, {
|
const isFormData = payload instanceof FormData;
|
||||||
headers: getAuthHeaders(),
|
const headers = getAuthHeaders(isFormData);
|
||||||
params,
|
const cfg = { headers };
|
||||||
});
|
let response;
|
||||||
return { data: response.data.data, error: null };
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
data: null,
|
|
||||||
error: err.response?.data?.message || err.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const postData = async (endpoint, payload) => {
|
if (method === "get") {
|
||||||
const headers = {
|
if (payload) cfg.params = payload;
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
response = await axios.get(endpoint, cfg);
|
||||||
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
|
} else if (method === "delete") {
|
||||||
};
|
if (payload) cfg.data = payload;
|
||||||
const response = await axios.post(endpoint, payload, { headers });
|
response = await axios.delete(endpoint, cfg);
|
||||||
await fetchData();
|
} else {
|
||||||
return response.data.data;
|
response = await axios[method](endpoint, payload, cfg);
|
||||||
};
|
|
||||||
|
|
||||||
const postDataValidated = async (endpoint, payload) => {
|
|
||||||
try {
|
|
||||||
const headers = {
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
|
||||||
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
|
|
||||||
};
|
|
||||||
const response = await axios.post(endpoint, payload, { headers });
|
|
||||||
return { data: response.data.data, errors: null };
|
|
||||||
} catch (err) {
|
|
||||||
const raw = err.response?.data?.message;
|
|
||||||
let parsed = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return { data: null, errors: { general: raw || err.message } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: null, errors: parsed };
|
if (refresh) await fetchData();
|
||||||
|
return response.data;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const error = handleAxiosError(err);
|
||||||
|
|
||||||
|
if (error.status !== 422 && onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const putData = async (endpoint, payload) => {
|
|
||||||
const response = await axios.put(endpoint, payload, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
return response.data.data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteData = async (endpoint) => {
|
const clearError = () => setError(null);
|
||||||
const response = await axios.delete(endpoint, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
return response.data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDataWithBody = async (endpoint, payload) => {
|
|
||||||
const response = await axios.delete(endpoint, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
return response.data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
dataLoading,
|
dataLoading,
|
||||||
dataError,
|
dataError,
|
||||||
getData,
|
clearError,
|
||||||
postData,
|
getData: (url, params, refresh = true) => requestWrapper("get", url, params, refresh),
|
||||||
postDataValidated,
|
postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh),
|
||||||
putData,
|
putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh),
|
||||||
deleteData,
|
deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh),
|
||||||
deleteDataWithBody,
|
deleteDataWithBody: (url, body, refresh = true) => requestWrapper("delete", url, body, refresh)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { DataContext } from "@/context/DataContext";
|
import { DataContext } from "../context/DataContext";
|
||||||
|
|
||||||
export const useDataContext = () => useContext(DataContext);
|
export const useDataContext = () => useContext(DataContext);
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { parseJwt } from "../util/tokenUtils.js";
|
|
||||||
import NotificationModal from "../components/NotificationModal.jsx";
|
|
||||||
import axios from "axios";
|
|
||||||
import { useAuth } from "./useAuth.js";
|
|
||||||
import { useConfig } from "./useConfig.js";
|
|
||||||
|
|
||||||
const useSessionRenewal = () => {
|
|
||||||
const { logout } = useAuth();
|
|
||||||
const { config } = useConfig();
|
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [alreadyWarned, setAlreadyWarned] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
const decoded = parseJwt(token);
|
|
||||||
|
|
||||||
if (!token || !decoded?.exp) return;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const expTime = decoded.exp * 1000;
|
|
||||||
const timeLeft = expTime - now;
|
|
||||||
|
|
||||||
if (timeLeft <= 60000 && timeLeft > 0 && !alreadyWarned) {
|
|
||||||
setShowModal(true);
|
|
||||||
setAlreadyWarned(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeLeft <= 0) {
|
|
||||||
clearInterval(interval);
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
}, 10000); // revisa cada 10 segundos
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [alreadyWarned, logout]);
|
|
||||||
|
|
||||||
const handleRenew = async () => {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
const decoded = parseJwt(token);
|
|
||||||
const now = Date.now();
|
|
||||||
const expTime = decoded?.exp * 1000;
|
|
||||||
|
|
||||||
if (!token || !decoded || now > expTime) {
|
|
||||||
logout();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.apiConfig.baseUrl}${config.apiConfig.endpoints.auth.refreshToken}`,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const newToken = response.data.data.token;
|
|
||||||
localStorage.setItem("token", newToken);
|
|
||||||
setShowModal(false);
|
|
||||||
setAlreadyWarned(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error renovando sesión:", err);
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const modal = showModal && (
|
|
||||||
<NotificationModal
|
|
||||||
show={true}
|
|
||||||
onClose={() => {
|
|
||||||
setShowModal(false);
|
|
||||||
logout();
|
|
||||||
}}
|
|
||||||
title="¿Quieres seguir conectado?"
|
|
||||||
message="Tu sesión está a punto de expirar. ¿Quieres renovarla 1 hora más?"
|
|
||||||
variant="info"
|
|
||||||
buttons={[
|
|
||||||
{
|
|
||||||
label: "Renovar sesión",
|
|
||||||
variant: "success",
|
|
||||||
onClick: handleRenew,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Cerrar sesión",
|
|
||||||
variant: "danger",
|
|
||||||
onClick: () => {
|
|
||||||
logout();
|
|
||||||
setShowModal(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return { modal };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSessionRenewal;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
import { ThemeContext } from "../context/ThemeContext";
|
|
||||||
|
|
||||||
export const useTheme = () => {
|
|
||||||
const context = useContext(ThemeContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useTheme debe usarse dentro de un <ThemeProvider>");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
15
src/main.jsx
15
src/main.jsx
@@ -4,27 +4,24 @@ import { createRoot } from 'react-dom/client'
|
|||||||
/* COMPONENTS */
|
/* COMPONENTS */
|
||||||
import App from '@/App.jsx'
|
import App from '@/App.jsx'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { ThemeProvider } from '@/context/ThemeContext'
|
|
||||||
import { AuthProvider } from '@/context/AuthContext'
|
import { AuthProvider } from '@/context/AuthContext'
|
||||||
import { ConfigProvider } from '@/context/ConfigContext.jsx'
|
import { ConfigProvider } from '@/context/ConfigContext.jsx'
|
||||||
|
|
||||||
/* CSS */
|
/* CSS */
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||||
import "slick-carousel/slick/slick.css";
|
import "slick-carousel/slick/slick.css";
|
||||||
import "slick-carousel/slick/slick-theme.css";
|
import "slick-carousel/slick/slick-theme.css";
|
||||||
import '@/css/index.css'
|
import '@/css/index.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<ThemeProvider>
|
<AuthProvider>
|
||||||
<AuthProvider>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<App />
|
||||||
<App />
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import '@/css/Building.css';
|
|
||||||
|
|
||||||
export default function Building() {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
if (location.pathname === '/') return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="building-container d-flex flex-column align-items-center justify-content-center text-center py-5 px-3">
|
|
||||||
<div className="building-icon">🚧</div>
|
|
||||||
<div className="building-title">Esta página está en construcción</div>
|
|
||||||
<div className="building-subtitle">Estamos trabajando para traértela pronto</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import React from 'react';
|
import StartSection from "@/components/Home/StartSection";
|
||||||
import { Link } from 'react-router-dom';
|
import SamplesSection from "@/components/Home/SamplesSection";
|
||||||
import '@/css/Home.css';
|
import VoxSection from "@/components/Home/VoxSection";
|
||||||
import CustomContainer from '@/components/CustomContainer';
|
import AboutMeSection from "@/components/Home/AboutMeSection";
|
||||||
import ContentWrapper from '@/components/ContentWrapper';
|
import ContentWrapper from "@/components/ContentWrapper";
|
||||||
import CustomCarousel from '@/components/CustomCarousel';
|
import CustomContainer from "@/components/CustomContainer";
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<CustomContainer>
|
<ContentWrapper>
|
||||||
<h1>Título aquí 🐧</h1>
|
<CustomContainer>
|
||||||
<p>
|
<StartSection />
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam eu tristique elit. Nullam lorem turpis, cursus in augue id, dignissim rhoncus dolor. Suspendisse vitae elit et orci auctor rutrum. Sed lobortis iaculis dapibus. Mauris vel dapibus elit. In risus est, lobortis ut scelerisque at, luctus pretium lorem. Vestibulum condimentum erat ut sagittis commodo. Nulla facilisi. Praesent vel dolor molestie, molestie ipsum ut, efficitur est. Nulla rutrum pulvinar eros, a faucibus orci vehicula sed. In tincidunt in sapien vel convallis. Aliquam nec leo sit amet libero efficitur imperdiet ac sit amet leo. Vivamus ex tellus, tempor aliquam orci eu, eleifend tempus odio.
|
<SamplesSection />
|
||||||
</p><p>
|
<VoxSection />
|
||||||
Aliquam mollis sollicitudin pharetra. Quisque malesuada, nulla nec sodales consequat, nulla felis imperdiet metus, auctor aliquet lacus urna vel neque. Cras cursus nisl eu erat vehicula, sed semper turpis porttitor. Fusce ut lectus a erat gravida ullamcorper ut ut neque. Pellentesque rutrum, nibh vitae egestas ullamcorper, justo dolor hendrerit magna, in rhoncus ante magna at velit. Fusce cursus, ante sed dictum gravida, ex lacus pulvinar libero, quis egestas sapien leo id nisi. In eget vestibulum ante. Vivamus venenatis eros lorem, ac tincidunt tortor elementum at.
|
<AboutMeSection />
|
||||||
</p>
|
</CustomContainer>
|
||||||
</CustomContainer>
|
</ContentWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
9
src/pages/Login.jsx
Normal file
9
src/pages/Login.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import LoginForm from "@/components/Auth/LoginForm";
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
return (
|
||||||
|
<LoginForm />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export const renderErrorAlert = (error, options = {}) => {
|
|
||||||
const { className = 'alert alert-danger py-1 px-2 small', role = 'alert' } = options;
|
|
||||||
|
|
||||||
if (!error) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className} role={role}>
|
|
||||||
{typeof error === 'string' ? error : 'An unexpected error occurred.'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resetErrorIfEditEnds = (editMode, setError) => {
|
|
||||||
if (!editMode) setError(null);
|
|
||||||
};
|
|
||||||
5
src/util/array.js
Normal file
5
src/util/array.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const random = (arr) => {
|
||||||
|
return arr[Math.floor(Math.random() * arr.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
export { random }
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
export const DateParser = {
|
|
||||||
sqlToString: (sqlDate) => {
|
|
||||||
const [datePart] = sqlDate.split('T');
|
|
||||||
const [year, month, day] = datePart.split('-');
|
|
||||||
return `${day}/${month}/${year}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
timestampToString: (timestamp) => {
|
|
||||||
const [datePart] = timestamp.split('T');
|
|
||||||
const [year, month, day] = datePart.split('-');
|
|
||||||
return `${day}/${month}/${year}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
isoToStringWithTime: (isoString) => {
|
|
||||||
if (!isoString) return '—';
|
|
||||||
|
|
||||||
const date = new Date(isoString);
|
|
||||||
if (isNaN(date)) return '—'; // Para proteger aún más por si llega basura
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat('es-ES', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
timeZone: 'Europe/Madrid'
|
|
||||||
}).format(date);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export const errorParser = (err) => {
|
|
||||||
const message = err.response?.data?.message;
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(message);
|
|
||||||
return Object.values(parsed)[0];
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
} catch (e) {
|
|
||||||
return message || err.message || "Unknown error";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export const generateSecurePassword = (length = 12) => {
|
|
||||||
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
const digits = '0123456789';
|
|
||||||
const symbols = '!@#$%^&*'; // <- compatibles con bcrypt
|
|
||||||
const all = upper + lower + digits + symbols;
|
|
||||||
|
|
||||||
if (length < 8) length = 8;
|
|
||||||
|
|
||||||
const getRand = (chars) => chars[Math.floor(Math.random() * chars.length)];
|
|
||||||
|
|
||||||
let password = [
|
|
||||||
getRand(upper),
|
|
||||||
getRand(lower),
|
|
||||||
getRand(digits),
|
|
||||||
getRand(symbols),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = password.length; i < length; i++) {
|
|
||||||
password.push(getRand(all));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = password.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[password[i], password[j]] = [password[j], password[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return password.join('');
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const parseJwt = (token) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(atob(token.split('.')[1]));
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user