Proyecto inicial, con fallitos de CSS. (Gracias Karim.)

This commit is contained in:
2026-02-18 03:09:08 +01:00
commit c7b9f87ad2
20 changed files with 9062 additions and 0 deletions

223
src/App.css Normal file
View File

@@ -0,0 +1,223 @@
/* =========================================
1. VARIABLES Y RESET
========================================= */
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--dark: #0f172a;
--white: #ffffff;
--bg: #f8fafc;
--border: #e2e8f0;
--text: #1e293b;
--text-light: #64748b;
--danger: #ef4444;
--max-w: 1200px;
--radius: 12px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* =========================================
2. NAVBAR & BURGER ANIMADO
========================================= */
.navbar-container {
background: var(--white);
border-bottom: 1px solid var(--border);
height: 75px;
position: sticky;
top: 0;
z-index: 2000;
display: flex;
align-items: center;
}
.navbar {
width: 100%;
max-width: var(--max-w);
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.nav-logo {
font-size: 1.6rem;
font-weight: 800;
text-decoration: none;
color: var(--dark);
letter-spacing: -1px;
z-index: 2101;
}
/* BOTÓN BURGER */
.burger-btn {
background: var(--dark);
border: none;
width: 48px;
height: 48px;
border-radius: 10px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
z-index: 2101;
transition: transform 0.2s;
}
.burger-btn:active { transform: scale(0.9); }
.burger-line {
width: 24px;
height: 2px;
background-color: var(--white);
border-radius: 2px;
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ANIMACIÓN BURGER -> X */
.burger-btn.active .burger-line:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger-btn.active .burger-line:nth-child(2) { opacity: 0; transform: translateX(10px); }
.burger-btn.active .burger-line:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
/* MENÚ DRAWER */
.nav-links {
position: fixed;
top: 0;
right: -100%;
width: 320px;
height: 100vh;
background: var(--white);
display: flex;
flex-direction: column;
padding: 100px 40px;
gap: 15px;
box-shadow: -10px 0 40px rgba(0,0,0,0.1);
transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 2100;
}
.nav-links.open { right: 0; }
.nav-links a {
text-decoration: none;
color: var(--text);
font-size: 1.2rem;
font-weight: 600;
padding: 12px 15px;
border-radius: 8px;
transition: background 0.2s;
}
.nav-links a:hover { background: var(--bg); color: var(--primary); }
.admin-btn-nav { background: var(--primary) !important; color: white !important; text-align: center; }
.menu-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(15, 23, 42, 0.5);
backdrop-filter: blur(4px);
z-index: 2099;
}
/* =========================================
3. HOME LAYOUT
========================================= */
.home-container {
width: 100%;
max-width: var(--max-w);
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 350px;
gap: 40px;
padding: 40px 20px;
flex: 1;
}
.card, .admin-card {
background: var(--white);
padding: 30px;
border-radius: var(--radius);
border: 1px solid var(--border);
margin-bottom: 25px;
box-shadow: var(--shadow);
}
.sidebar-box {
background: var(--white);
padding: 25px;
border-radius: var(--radius);
border: 1px solid var(--border);
position: sticky;
top: 100px;
}
/* =========================================
4. ADMIN UI (MEJORADO)
========================================= */
.admin-container { max-width: 900px; margin: 40px auto; padding: 0 20px; flex: 1; }
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.admin-tabs { display: flex; gap: 8px; background: #eee; padding: 5px; border-radius: 10px; }
.tab-btn {
padding: 8px 16px; border: none; background: none; border-radius: 6px;
cursor: pointer; font-weight: 600; color: var(--text-light);
}
.tab-btn.active { background: var(--white); color: var(--dark); shadow: var(--shadow); }
.form-group { margin-bottom: 20px; display: flex; flex-direction: column; gap: 8px; }
.form-group label { font-weight: 700; font-size: 0.85rem; color: var(--text-light); text-transform: uppercase; }
.admin-input {
width: 100%; padding: 14px; border: 1px solid var(--border);
border-radius: 10px; font-size: 1rem; transition: border 0.2s;
}
.admin-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
.editor-frame { background: white; border: 1px solid var(--border); padding: 20px; min-height: 400px; border-radius: 10px; }
.submit-btn {
width: 100%; padding: 16px; background: var(--primary); color: white;
border: none; border-radius: 10px; font-weight: 700; font-size: 1rem;
cursor: pointer; margin-top: 10px; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
/* =========================================
5. FOOTER & RESPONSIVE
========================================= */
.footer { background: var(--dark); color: #94a3b8; padding: 60px 20px; margin-top: 60px; }
.footer-content { max-width: var(--max-w); margin: 0 auto; display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px; }
@media (max-width: 768px) {
.nav-logo { position: absolute; left: 50%; transform: translateX(-50%); }
.home-container { grid-template-columns: 1fr; padding: 15px; }
.nav-links { width: 100%; }
.footer-content { grid-template-columns: 1fr; text-align: center; }
.admin-header { flex-direction: column; gap: 20px; text-align: center; }
}

89
src/App.jsx Normal file
View File

@@ -0,0 +1,89 @@
import { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import Home from './home';
import Admin from './admin';
import Login from './login';
import PostDetail from './PostDetail';
function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => setIsOpen(!isOpen);
const closeMenu = () => setIsOpen(false);
return (
<div className="navbar-container">
<nav className="navbar">
{/* Logo */}
<Link to="/" className="nav-logo" onClick={closeMenu}>
Josemi Blog 🚀
</Link>
{/* Botón Burger Animado */}
{/* Si isOpen es true, se añade la clase 'active' para activar la animación CSS */}
<button
className={`burger-btn ${isOpen ? 'active' : ''}`}
onClick={toggleMenu}
aria-label="Toggle menu"
>
{/* Las 3 líneas que formarán el icono */}
<span className="burger-line"></span>
<span className="burger-line"></span>
<span className="burger-line"></span>
</button>
{/* Menú Lateral (Drawer) */}
<div className={`nav-links ${isOpen ? 'open' : ''}`}>
<Link to="/" onClick={closeMenu}>Inicio</Link>
<Link to="/login" onClick={closeMenu}>Login</Link>
<Link to="/admin" className="admin-btn-nav" onClick={closeMenu}>Admin</Link>
</div>
{/* Capa oscura al abrir el menú */}
{isOpen && <div className="menu-overlay" onClick={closeMenu}></div>}
</nav>
</div>
);
}
function Footer() {
return (
<footer className="footer">
<div className="footer-content">
<div>
<h3>Josemi Blog</h3>
<p>Recursos y artículos técnicos sobre desarrollo.</p>
</div>
<div>
<h3>Navegación</h3>
<Link to="/" style={{color:'#94a3b8', textDecoration:'none', display:'block', marginBottom:'10px'}}>Inicio</Link>
<Link to="/admin" style={{color:'#94a3b8', textDecoration:'none', display:'block'}}>Admin</Link>
</div>
<div>
<h3>Contacto</h3>
<p>Huelva, España 🇪🇸</p>
</div>
</div>
<div style={{textAlign:'center', marginTop:'40px', paddingTop:'20px', borderTop:'1px solid #1e293b'}}>
© 2026 Josemi Blog. Todos los derechos reservados.
</div>
</footer>
);
}
export default function App() {
return (
<Router>
<Navbar />
<main style={{ flex: 1 }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/admin" element={<Admin />} />
<Route path="/post/:slug" element={<PostDetail />} />
</Routes>
</main>
<Footer />
</Router>
);
}

24
src/PostDetail.jsx Normal file
View File

@@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
export default function PostDetail() {
const { slug } = useParams();
const [post, setPost] = useState(null);
useEffect(() => {
fetch('http://localhost:9002/posts').then(r => r.json()).then(d => {
setPost(d.find(p => p.slug === slug));
});
}, [slug]);
if (!post) return <div>Cargando...</div>;
return (
<div className="admin-container" style={{background:'white', padding:'40px', borderRadius:'12px'}}>
<Link to="/"> Volver</Link>
<h1 style={{fontSize:'3rem', marginTop:'20px'}}>{post.title}</h1>
<hr style={{margin:'20px 0'}}/>
<div dangerouslySetInnerHTML={{ __html: post.content }} style={{fontSize:'1.2rem'}} />
</div>
);
}

120
src/admin.jsx Normal file
View File

@@ -0,0 +1,120 @@
import { useState, useEffect } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
export default function Admin() {
const token = localStorage.getItem('blog_token');
const [tab, setTab] = useState('crear');
const [posts, setPosts] = useState([]);
const [form, setForm] = useState({ title: '', slug: '', tags: '', url: '', type: 'POST' });
const editor = useEditor({ extensions: [StarterKit], content: '' });
const load = () => fetch('http://localhost:9002/posts').then(r => r.json()).then(d => setPosts(d));
useEffect(() => { if (token) load(); }, [token]);
const save = async (e) => {
e.preventDefault();
await fetch('http://localhost:9002/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ ...form, content: editor.getHTML() })
});
alert('¡Publicado con éxito!');
load();
setTab('lista');
};
const del = async (id) => {
if (window.confirm('¿Estás seguro de que quieres eliminar este post?')) {
await fetch(`http://localhost:9002/posts/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
load();
}
};
if (!token) return <div className="admin-container"><h2>🔒 Acceso Restringido</h2><p>Por favor, inicia sesión.</p></div>;
return (
<div className="admin-container">
<header className="admin-header">
<h1>Panel de Gestión</h1>
<div className="admin-tabs">
<button onClick={() => setTab('crear')} className={`tab-btn ${tab === 'crear' ? 'active' : ''}`}>Nueva Entrada</button>
<button onClick={() => setTab('lista')} className={`tab-btn ${tab === 'lista' ? 'active' : ''}`}>Gestionar Todo</button>
</div>
</header>
{tab === 'crear' ? (
<form onSubmit={save} className="admin-form-card">
<div className="form-group">
<label>Tipo de Contenido</label>
<select value={form.type} onChange={e => setForm({...form, type: e.target.value})} className="admin-input">
<option value="POST">Artículo (Blog)</option>
<option value="LINK">Enlace Externo</option>
</select>
</div>
<div className="form-row">
<div className="form-group">
<label>Título</label>
<input placeholder="Ej: Mi primer post" onChange={e => setForm({...form, title: e.target.value})} className="admin-input" required />
</div>
<div className="form-group">
<label>Slug (URL única)</label>
<input placeholder="ej-mi-primer-post" onChange={e => setForm({...form, slug: e.target.value})} className="admin-input" required />
</div>
</div>
{form.type === 'LINK' && (
<div className="form-group">
<label>URL del Enlace</label>
<input placeholder="https://..." onChange={e => setForm({...form, url: e.target.value})} className="admin-input" />
</div>
)}
<div className="form-group">
<label>Etiquetas</label>
<input placeholder="React, JavaScript..." onChange={e => setForm({...form, tags: e.target.value})} className="admin-input" />
</div>
<div className="form-group">
<label>Contenido / Descripción</label>
<div className="editor-frame">
<EditorContent editor={editor} />
</div>
</div>
<button type="submit" className="submit-btn">Publicar Contenido</button>
</form>
) : (
<div className="admin-list-card">
{posts.length === 0 ? <p>No hay nada que mostrar.</p> : (
<table className="admin-table">
<thead>
<tr>
<th>Título</th>
<th>Tipo</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{posts.map(p => (
<tr key={p.id}>
<td>{p.title}</td>
<td><span className={`badge ${p.type}`}>{p.type}</span></td>
<td>
<button onClick={() => del(p.id)} className="delete-btn">Eliminar</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
);
}

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

47
src/home.jsx Normal file
View File

@@ -0,0 +1,47 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
export default function Home() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('http://localhost:9002/posts')
.then(res => res.json())
.then(data => setPosts(data))
.catch(err => console.error("Error:", err));
}, []);
const articles = posts.filter(p => p.type === 'POST' || !p.type);
const links = posts.filter(p => p.type === 'LINK');
return (
<div className="home-container">
<section>
<h2 style={{marginBottom:'30px'}}>Artículos</h2>
{articles.map(post => (
<article key={post.id} className="card">
<span style={{color:'var(--primary)', fontWeight:'bold'}}>#{post.tags}</span>
<h2 style={{margin:'10px 0'}}>{post.title}</h2>
<div dangerouslySetInnerHTML={{ __html: post.content.substring(0, 150) + '...' }} />
{/* Si no hay slug, usamos el ID para que no salga 'undefined' */}
<Link to={`/post/${post.slug || post.id}`} className="read-more" style={{color:'var(--primary)', textDecoration:'none', fontWeight:'bold', display:'block', marginTop:'15px'}}>
Leer artículo completo
</Link>
</article>
))}
</section>
<aside>
<div className="sidebar-box">
<h3>Enlaces Externos 🔗</h3>
{links.map(link => (
<div key={link.id} style={{padding:'15px 0', borderBottom:'1px solid #f1f5f9'}}>
<a href={link.url} target="_blank" style={{color:'var(--primary)', fontWeight:'bold', textDecoration:'none'}}>{link.title}</a>
<div style={{fontSize:'0.9rem', color:'#666'}} dangerouslySetInnerHTML={{ __html: link.content }} />
</div>
))}
</div>
</aside>
</div>
);
}

213
src/index.css Normal file
View File

@@ -0,0 +1,213 @@
/* =========================================
1. VARIABLES Y REINICIO
========================================= */
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--dark: #0f172a;
--white: #ffffff;
--gray-bg: #f8fafc;
--gray-border: #e2e8f0;
--text-main: #1e293b;
--text-muted: #64748b;
--danger: #ef4444;
--max-width: 1200px; /* Alineación de 1200px para todo */
--shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
--radius: 12px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: var(--gray-bg);
color: var(--text-main);
line-height: 1.6;
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* =========================================
2. NAVBAR (BURGER ALINEADO A 1200PX)
========================================= */
.navbar-container {
background: var(--white);
border-bottom: 1px solid var(--gray-border);
height: 80px;
position: sticky;
top: 0;
z-index: 2000;
display: flex;
justify-content: center;
}
.navbar {
width: 100%;
max-width: var(--max-width);
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.nav-logo {
font-size: 1.6rem;
font-weight: 800;
text-decoration: none;
color: var(--dark);
z-index: 2101;
}
.burger-btn {
background: var(--dark);
border: none;
width: 48px;
height: 48px;
border-radius: var(--radius);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
z-index: 2101;
}
.burger-line {
width: 24px;
height: 2px;
background-color: white;
transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Animación Burger -> X */
.burger-btn.active .burger-line:nth-child(1) { transform: translateY(8px) rotate(45deg); }
.burger-btn.active .burger-line:nth-child(2) { opacity: 0; }
.burger-btn.active .burger-line:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }
.nav-links {
position: fixed;
top: 0;
right: -100%;
width: 320px;
height: 100vh;
background: white;
display: flex;
flex-direction: column;
padding: 100px 40px;
gap: 15px;
box-shadow: -10px 0 30px rgba(0,0,0,0.1);
transition: 0.4s ease-in-out;
z-index: 2100;
}
.nav-links.open { right: 0; }
.nav-links a { text-decoration: none; color: var(--text-main); font-weight: 600; font-size: 1.2rem; padding: 10px; border-radius: 8px; }
.nav-links a:hover { background: var(--gray-bg); color: var(--primary); }
.menu-overlay {
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.4); z-index: 2099;
}
/* =========================================
3. HOME: ARTÍCULOS CON ESPACIO
========================================= */
.home-container {
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 350px;
gap: 50px; /* Espacio horizontal entre columnas */
padding: 50px 20px;
flex: 1;
}
.card {
background: white;
padding: 35px;
border-radius: var(--radius);
border: 1px solid var(--gray-border);
margin-bottom: 40px; /* AQUÍ ESTÁ EL ESPACIO QUE PEDÍAS */
box-shadow: var(--shadow);
transition: transform 0.2s;
}
.card:hover { transform: translateY(-4px); }
.tag {
color: var(--primary);
font-weight: 700;
font-size: 0.85rem;
text-transform: uppercase;
margin-bottom: 12px;
display: inline-block;
}
.sidebar-box {
background: white;
padding: 30px;
border-radius: var(--radius);
border: 1px solid var(--gray-border);
position: sticky;
top: 110px;
}
/* =========================================
4. ESTILOS DE BOTONES ADMIN (BACKEND)
========================================= */
.admin-container { max-width: 900px; margin: 40px auto; padding: 0 20px; }
.admin-tabs { display: inline-flex; background: #e5e7eb; padding: 4px; border-radius: 12px; margin-bottom: 30px; }
.tab-btn {
padding: 10px 25px; border: none; background: transparent; border-radius: 9px;
cursor: pointer; font-weight: 700; color: var(--text-muted); transition: 0.2s;
}
.tab-btn.active { background: white; color: var(--dark); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
.submit-btn {
width: 100%; padding: 18px; background: var(--primary); color: white;
border: none; border-radius: var(--radius); font-weight: 700; font-size: 1.1rem;
cursor: pointer; transition: 0.2s; box-shadow: 0 4px 15px rgba(37, 99, 235, 0.2);
}
.submit-btn:hover { background: var(--primary-hover); transform: translateY(-2px); }
.delete-btn {
padding: 8px 16px; background: white; color: var(--danger); border: 2px solid var(--danger);
border-radius: 8px; font-weight: 700; cursor: pointer; transition: 0.2s;
}
.delete-btn:hover { background: var(--danger); color: white; }
.admin-input { width: 100%; padding: 15px; border: 1px solid var(--gray-border); border-radius: 10px; margin-bottom: 20px; font-size: 1rem; }
.editor-frame { border: 1px solid var(--gray-border); padding: 25px; min-height: 400px; border-radius: 10px; background: white; }
/* =========================================
5. AJUSTES MÓVIL (CENTRADO TOTAL)
========================================= */
@media (max-width: 768px) {
.navbar { justify-content: flex-end; }
.nav-logo {
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
}
.home-container { grid-template-columns: 1fr; padding: 20px; }
.card { margin-bottom: 30px; }
.nav-links { width: 100%; }
}
/* =========================================
6. FOOTER
========================================= */
.footer { background: var(--dark); color: #94a3b8; padding: 80px 20px 40px; margin-top: auto; }
.footer-content { max-width: var(--max-width); margin: 0 auto; display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px; }

39
src/login.jsx Normal file
View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
const res = await fetch('http://localhost:9002/posts/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok) {
localStorage.setItem('blog_token', data.token);
alert('¡Acceso concedido!');
navigate('/admin');
window.location.reload();
} else {
alert('Error: ' + data.error);
}
};
return (
<div style={{ maxWidth: '400px', margin: '100px auto', padding: '40px', background: '#fff', borderRadius: '15px', boxShadow: '0 4px 15px rgba(0,0,0,0.1)', textAlign: 'center' }}>
<h2>Identifícate 🔐</h2>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input type="text" placeholder="Usuario" value={username} onChange={e => setUsername(e.target.value)} style={{ padding: '12px', borderRadius: '8px', border: '1px solid #ddd' }} />
<input type="password" placeholder="Contraseña" value={password} onChange={e => setPassword(e.target.value)} style={{ padding: '12px', borderRadius: '8px', border: '1px solid #ddd' }} />
<button type="submit" style={{ padding: '12px', background: '#000', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: 'bold' }}>Entrar al Panel</button>
</form>
<p style={{ marginTop: '20px', fontSize: '0.8rem', color: '#666' }}>Prueba con: josemi / josemivi</p>
</div>
);
}

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

13
src/navbar.jsx Normal file
View File

@@ -0,0 +1,13 @@
import { Link } from 'react-router-dom';
export default function Navbar() {
return (
<nav style={{ padding: '20px', background: '#fff', borderBottom: '1px solid #eee', display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '30px' }}>
<Link to="/" style={{ fontSize: '1.5rem', fontWeight: 'bold', textDecoration: 'none', color: '#000' }}>JosemiBlog 🚀</Link>
<div>
<Link to="/" style={{ marginRight: '20px', textDecoration: 'none', color: '#666' }}>Inicio</Link>
<Link to="/admin" style={{ padding: '8px 16px', background: '#000', color: '#fff', borderRadius: '6px', textDecoration: 'none' }}>Admin</Link>
</div>
</nav>
);
}