Mejorar navegacion del header y evitar que Vite procese el Dockerfile.
Redisenar tabs Editor/Historial y CTA Acceder con estados activos claros. Acotar volumenes Docker y el escaneo de Tailwind/Vite para corregir el error intermitente de import-analysis en Firefox.
This commit is contained in:
+7
-1
@@ -18,8 +18,14 @@ services:
|
|||||||
container_name: frontend
|
container_name: frontend
|
||||||
ports:
|
ports:
|
||||||
- "8071:5173"
|
- "8071:5173"
|
||||||
|
# Montar solo fuentes de la app (no el Dockerfile ni ficheros de despliegue en /app)
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend/src:/app/src
|
||||||
|
- ./frontend/public:/app/public
|
||||||
|
- ./frontend/index.html:/app/index.html
|
||||||
|
- ./frontend/vite.config.js:/app/vite.config.js
|
||||||
|
- ./frontend/package.json:/app/package.json
|
||||||
|
- ./frontend/package-lock.json:/app/package-lock.json
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- VITE_API_URL=/api
|
- VITE_API_URL=/api
|
||||||
|
|||||||
@@ -1,7 +1,64 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { FiLogIn, FiLogOut } from 'react-icons/fi';
|
import { FiLogIn, FiLogOut, FiEdit3, FiClock } from 'react-icons/fi';
|
||||||
|
|
||||||
|
function NavTab({ to, isActive, icon: Icon, children }) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
className={[
|
||||||
|
'relative flex items-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold transition-all duration-200',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:ring-offset-2',
|
||||||
|
isActive
|
||||||
|
? 'bg-white text-slate-800 shadow-sm ring-1 ring-slate-200/80'
|
||||||
|
: 'text-slate-500 hover:bg-white/50 hover:text-slate-700',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={`h-4 w-4 shrink-0 ${isActive ? 'text-slate-600' : 'text-slate-400'}`}
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span>{children}</span>
|
||||||
|
{isActive && (
|
||||||
|
<span
|
||||||
|
className="absolute inset-x-3 -bottom-px h-[3px] rounded-full bg-blue-600"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccederButton({ isActive }) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
className={[
|
||||||
|
'group relative flex items-center gap-2.5 rounded-lg px-5 py-2.5 text-sm font-semibold text-white transition-all duration-200',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2',
|
||||||
|
'active:scale-[0.98]',
|
||||||
|
isActive
|
||||||
|
? 'bg-blue-700 shadow-md shadow-blue-700/35 ring-2 ring-blue-400/50 ring-offset-1 ring-offset-white'
|
||||||
|
: [
|
||||||
|
'bg-blue-600 shadow-md shadow-blue-600/30',
|
||||||
|
'hover:bg-blue-700 hover:shadow-lg hover:shadow-blue-600/35',
|
||||||
|
].join(' '),
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-md bg-white/15 text-white transition-colors duration-200 group-hover:bg-white/25"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
<FiLogIn className="h-4 w-4" strokeWidth={2.5} />
|
||||||
|
</span>
|
||||||
|
<span>Acceder</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
@@ -9,7 +66,7 @@ export default function Header() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, logout, isAuthenticated } = useAuth();
|
const { user, logout, isAuthenticated } = useAuth();
|
||||||
|
|
||||||
const userInitial = user?.username ? user.username[0].toUpperCase() : "U";
|
const userInitial = user?.username ? user.username[0].toUpperCase() : 'U';
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
@@ -17,56 +74,79 @@ export default function Header() {
|
|||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActive = (path) => {
|
const isEditorActive =
|
||||||
return location.pathname === path || (path === '/editor' && location.pathname === '/');
|
location.pathname === '/editor' || location.pathname === '/';
|
||||||
};
|
const isHistoryActive = location.pathname.startsWith('/history');
|
||||||
|
const isLoginActive = location.pathname === '/login';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white shadow-sm border-b border-slate-200 sticky top-0 z-50 h-16 shrink-0 w-full">
|
<header className="sticky top-0 z-50 h-16 w-full shrink-0 border-b border-slate-200 bg-white shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 h-full flex items-center justify-between">
|
<div className="mx-auto flex h-full max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
||||||
|
<Link
|
||||||
<Link to="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity whitespace-nowrap">
|
to="/"
|
||||||
<img src="/favicon.svg" alt="Deck of Cards Logo" className="w-10 h-10 shadow-sm rounded-xl object-contain" />
|
className="flex items-center gap-3 whitespace-nowrap transition-opacity hover:opacity-80"
|
||||||
<span className="text-2xl font-black bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-indigo-600 hidden sm:block">
|
>
|
||||||
|
<img
|
||||||
|
src="/favicon.svg"
|
||||||
|
alt="Deck of Cards Logo"
|
||||||
|
className="h-10 w-10 rounded-xl object-contain shadow-sm"
|
||||||
|
/>
|
||||||
|
<span className="hidden bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-2xl font-black text-transparent sm:block">
|
||||||
Deck of Cards
|
Deck of Cards
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 whitespace-nowrap">
|
<div className="flex items-center gap-4 whitespace-nowrap">
|
||||||
<div className="flex items-center gap-1 mr-2">
|
{/* Misma navegación con o sin sesión: evita estilos distintos al loguearse */}
|
||||||
<Link to="/editor" className={`text-sm font-bold px-4 py-2 rounded-lg transition-all ${isActive('/editor') ? 'text-blue-600' : 'text-slate-600 hover:text-blue-500'}`}>
|
<nav
|
||||||
|
className="flex items-center gap-0.5 rounded-xl bg-slate-100/90 p-1"
|
||||||
|
aria-label="Secciones principales"
|
||||||
|
>
|
||||||
|
<NavTab to="/editor" isActive={isEditorActive} icon={FiEdit3}>
|
||||||
Editor
|
Editor
|
||||||
</Link>
|
</NavTab>
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<Link to="/history" className={`text-sm font-bold px-4 py-2 rounded-lg transition-all ${isActive('/history') ? 'text-blue-600' : 'text-slate-600 hover:text-blue-500'}`}>
|
<NavTab to="/history" isActive={isHistoryActive} icon={FiClock}>
|
||||||
Historial
|
Historial
|
||||||
</Link>
|
</NavTab>
|
||||||
)}
|
)}
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<div className="relative border-l border-slate-200 pl-4">
|
<div className="relative border-l border-slate-200 pl-4">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
className="w-10 h-10 rounded-full bg-blue-100 text-blue-700 font-bold flex items-center justify-center border-2 border-blue-200 hover:bg-blue-200 transition-colors"
|
aria-expanded={isDropdownOpen}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="Menú de usuario"
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-blue-200 bg-blue-100 text-sm font-bold text-blue-700 transition-colors hover:bg-blue-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{userInitial}
|
{userInitial}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40" onClick={() => setIsDropdownOpen(false)}></div>
|
<div
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-lg border border-slate-100 py-2 z-50">
|
className="fixed inset-0 z-40"
|
||||||
<div className="px-4 py-2 border-b border-slate-50">
|
onClick={() => setIsDropdownOpen(false)}
|
||||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider">Usuario</p>
|
aria-hidden
|
||||||
<p className="text-sm font-bold text-slate-700 truncate">{user?.username}</p>
|
/>
|
||||||
|
<div className="absolute right-0 z-50 mt-2 w-48 rounded-xl border border-slate-100 bg-white py-2 shadow-lg">
|
||||||
|
<div className="border-b border-slate-50 px-4 py-2">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
||||||
|
Usuario
|
||||||
|
</p>
|
||||||
|
<p className="truncate text-sm font-bold text-slate-700">
|
||||||
|
{user?.username}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full flex items-center gap-2 px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 transition-colors"
|
className="flex w-full items-center gap-2 px-4 py-2 text-sm font-bold text-red-600 transition-colors hover:bg-red-50"
|
||||||
>
|
>
|
||||||
<FiLogOut className="w-5 h-5" strokeWidth={2.5} />
|
<FiLogOut className="h-5 w-5" strokeWidth={2.5} />
|
||||||
Cerrar Sesión
|
Cerrar Sesión
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,15 +154,8 @@ export default function Header() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center border-l border-slate-200 pl-4">
|
<div className="border-l border-slate-200 pl-4">
|
||||||
|
<AccederButton isActive={isLoginActive} />
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="flex items-center gap-2 text-sm font-bold bg-blue-600 text-white px-5 py-2.5 rounded-xl shadow-sm hover:bg-blue-700 transition-all active:scale-95"
|
|
||||||
>
|
|
||||||
<FiLogIn className="w-5 h-5" strokeWidth={2.5} />
|
|
||||||
Acceder
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Solo escanear código fuente; evita que Tailwind/Vite procesen Dockerfile u otros archivos en /app */
|
||||||
|
@source "./src/**/*.{js,jsx}";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,21 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
optimizeDeps: {
|
||||||
|
entries: ['index.html', 'src/**/*.{js,jsx}'],
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
watch: {
|
||||||
|
ignored: [
|
||||||
|
'**/Dockerfile',
|
||||||
|
'**/.dockerignore',
|
||||||
|
'**/docker-compose*.yml',
|
||||||
|
'**/docker-compose*.yaml',
|
||||||
|
'**/README.md',
|
||||||
|
],
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: process.env.BACKEND_URL || 'http://backend:8000',
|
target: process.env.BACKEND_URL || 'http://backend:8000',
|
||||||
|
|||||||
Reference in New Issue
Block a user