Add: translations. Fix: some css

This commit is contained in:
Jose
2026-02-16 16:59:30 +01:00
parent 859d5b88bc
commit aec029b670
30 changed files with 499 additions and 171 deletions

View File

@@ -19,10 +19,13 @@
"bootstrap": "^5.3.7",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.12",
"i18next": "^25.8.10",
"react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0",
"react-i18next": "^16.5.4",
"react-router-dom": "^7.7.1",
"react-router-hash-link": "^2.4.3",
"react-slick": "^0.30.3",
"slick-carousel": "^1.8.1"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

BIN
public/images/mini_1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

BIN
public/images/mini_2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
public/images/mini_3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
public/images/mini_4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
public/images/mini_5.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

BIN
public/images/pfp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -2,19 +2,23 @@ import Footer from '@/components/Footer'
import Header from '@/components/Header'
import NavBar from '@/components/NavBar/NavBar'
import Home from '@/pages/Home'
import { Route, Routes } from 'react-router-dom'
import { Route, Routes, useLocation } from 'react-router-dom'
import Login from './pages/Login'
import "./i18n";
const App = () => {
const withoutFooter = ["/login"]
const location = useLocation();
return (
<>
<Header />
<Header profilePic={"/images/pfp.jpg"}/>
<NavBar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/login' element={<Login />} />
</Routes>
<Footer />
{!withoutFooter.includes(location.pathname) && <Footer />}
</>
);
}

View File

@@ -11,10 +11,12 @@ import CustomContainer from '@/components/CustomContainer.jsx';
import ContentWrapper from '@/components/ContentWrapper.jsx';
import '@/css/LoginForm.css';
import { useTranslation } from 'react-i18next';
const LoginForm = () => {
const { login, error } = useContext(AuthContext);
const navigate = useNavigate();
const { t } = useTranslation();
const [formState, setFormState] = useState({
emailOrUserName: "",
@@ -47,7 +49,7 @@ const LoginForm = () => {
await login(loginBody);
navigate("/");
} catch (err) {
console.error("Error de login:", err.message);
console.error("Error:", err.message);
}
};
@@ -55,16 +57,13 @@ const LoginForm = () => {
<CustomContainer>
<ContentWrapper>
<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">{t("login.title")}</h1>
<Form className="d-flex flex-column gap-5" onSubmit={handleSubmit}>
<div className="d-flex flex-column gap-3">
<FloatingLabel
controlId="floatingUsuario"
label={
<>
<FontAwesomeIcon icon={faUser} className="me-2" />
Usuario o Email
</>
t("login.user_label")
}
>
<Form.Control
@@ -82,20 +81,6 @@ const LoginForm = () => {
onChange={handleChange}
name="password"
/>
{/*<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
<Form.Check
type="checkbox"
name="keepLoggedIn"
label="Mantener sesión iniciada"
className="text-secondary"
value={formState.keepLoggedIn}
onChange={(e) => { formState.keepLoggedIn = e.target.checked; setFormState({ ...formState }) }}
/>
<Link disabled to="#" className="muted">
Olvidé mi contraseña
</Link>
</div>*/}
</div>
{error && (
@@ -105,8 +90,8 @@ const LoginForm = () => {
)}
<div className="text-center">
<Button type="submit" className="w-75 padding-4 rounded-0 border-0 shadow-sm login-button">
Iniciar sesión
<Button type="submit" className="border-0 w-75 padding-4 rounded-0 shadow-sm login-button">
{t("login.button")}
</Button>
</div>
</Form>

View File

@@ -4,9 +4,11 @@ import '@/css/PasswordInput.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'react-i18next';
const PasswordInput = ({ value, onChange, name = "password" }) => {
const [show, setShow] = useState(false);
const { t } = useTranslation();
const toggleShow = () => setShow(prev => !prev);
@@ -15,10 +17,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
<FloatingLabel
controlId="passwordInput"
label={
<>
<FontAwesomeIcon icon={faKey} className="me-2" />
Contraseña
</>
t("login.password_label")
}
>
<Form.Control

View File

@@ -2,8 +2,10 @@ import PropTypes from 'prop-types';
const CustomContainer = ({ children }) => {
return (
<main className="mx-4 my-5">
{children}
<main className="container my-5">
<div className="d-flex flex-column gap-5">
{children}
</div>
</main>
);
}

View File

@@ -1,10 +1,15 @@
import '@/css/Header.css';
import { useTranslation } from 'react-i18next';
const Header = () => (
<header className="py-5 text-center position-relative">
<h1 className="mb-2">Adeptus Miniaturium</h1>
<p className="m-0">SUBTITULO SUBTITULO SUBTITULO SUBTITULO</p>
</header>
);
const Header = () => {
const { t } = useTranslation();
return (
<header className="py-5 text-center position-relative">
<h1 className="mb-2">Adeptus Miniaturium</h1>
<p className="m-0">{t("header.subtitle")}</p>
</header>
);
};
export default Header;

View File

@@ -1,23 +0,0 @@
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;

View File

@@ -1,29 +0,0 @@
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;

View File

@@ -0,0 +1,37 @@
import { useEffect, useState } from "react";
const images = [
"/images/mini_1.jpeg",
"/images/mini_2.jpeg",
"/images/mini_3.jpeg",
"/images/mini_4.jpeg",
"/images/mini_5.jpeg",
];
const TOTAL_FRAMES = images.length;
const ScrollBackgroundSpin = () => {
const [frame, setFrame] = useState(0);
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY;
const maxScroll = document.body.scrollHeight - window.innerHeight;
const progress = scrollTop / maxScroll;
const currentFrame = Math.floor(progress * TOTAL_FRAMES);
setFrame(Math.min(currentFrame, TOTAL_FRAMES - 1));
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="spin-background">
<img src={images[frame]} alt="Miniatura fondo" />
</div>
);
};
export default ScrollBackgroundSpin;

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,24 +0,0 @@
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. 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;

View File

@@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next";
import NavItem from '@/components/NavBar/NavItem';
const LanguageButton = ({ as = "button", index = null }) => {
const { i18n } = useTranslation();
const toggleLanguage = () => {
console.log(i18n.language);
const nextLang = i18n.language === "es" ? "en" : "es";
i18n.changeLanguage(nextLang);
};
if (as === "navitem") {
return (
<NavItem
item={{
label: i18n.language === "es" ? "🇪🇸" : "🇺🇸",
href: "#"
}}
index={index}
onClick={toggleLanguage}
/>
);
}
return (
<button onClick={toggleLanguage} className="btn-imperial">
{i18n.language === "es" ? "🇪🇸" : "🇺🇸"}
</button>
);
};
export default LanguageButton;

View File

@@ -1,18 +1,21 @@
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' }
];
import LanguageButton from '../LanguageButton';
import { useTranslation } from 'react-i18next';
const NavBar = () => {
const centerItems = navItems.slice(0, navItems.length - 2);
const rightItems = navItems.slice(-2);
const { t } = useTranslation();
const navItems = [
{ label: t("nav.inicio.label"), href: `/#${t("nav.inicio.href")}` },
{ label: t("nav.muestras.label"), href: `/#${t("nav.muestras.href")}` },
{ label: t("nav.pedidos.label"), href: `/#${t("nav.pedidos.href")}` },
{ label: t("nav.sobre.label"), href: `/#${t("nav.sobre.href")}` },
{ label: t("nav.login.label"), href: `/${t("nav.login.href")}` }
];
const centerItems = navItems.slice(0, navItems.length - 1);
const rightItems = navItems.slice(-1);
return (
<nav className="nav-container">
@@ -23,8 +26,9 @@ const NavBar = () => {
))}
<li className="position-absolute end-0 d-flex">
<LanguageButton as='navitem' index={0} total={rightItems.length + 1} />
{rightItems.map((item, index) => (
<NavItem key={index} item={item} index={index} total={rightItems.length} />
<NavItem key={index} item={item} index={index + 1} total={rightItems.length + 1} />
))}
</li>

View File

@@ -1,13 +1,32 @@
import { Link } from "react-router-dom";
import { HashLink } from "react-router-hash-link/dist/react-router-hash-link.cjs.production";
const NavItem = ({ item, index, total }) => {
const NavItem = ({ item, index, onClick }) => {
let borderClass = `${index === 0 ? "border-left border-right" : "border-right"}`;
const handleClick = (e) => {
if (onClick) {
e.preventDefault();
onClick();
} else if (item.href === "#") {
e.preventDefault();
}
};
return (
<li className={`mx-0 ${borderClass}`}>
<Link to={item.href} className="nav-link-custom">
{item.label}
</Link>
{onClick || item.href === "#" ? (
<button
onClick={handleClick}
className="nav-button"
style={{ cursor: "pointer" }}
>
{item.label}
</button>
) : (
<HashLink smooth to={item.href} className="nav-link-custom" onClick={handleClick}>
{item.label}
</HashLink>
)}
</li>
);
};

View File

@@ -1,6 +1,6 @@
header {
background:
linear-gradient(to bottom, rgba(0,0,0,0.9), rgba(0,0,0,0.5)),
linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,1)),
url('https://www.transparenttextures.com/patterns/dark-matter.png');
border-bottom: 4px double var(--imperial-gold);
box-shadow: 0 10px 30px rgba(0,0,0,0.9);

View File

@@ -1,7 +1,7 @@
.login-card {
background: #111 !important;
border: 1px solid var(--dirty-gold);
color: var(--parchment);
border: 1px solid var(--dirty-gold) !important;
color: var(--parchment) !important;
padding: 40px;
clip-path: polygon(
20px 0, 100% 0,
@@ -42,7 +42,7 @@ input.form-control:focus {
font-family: 'Cinzel', serif;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--dirty-gold);
color: var(--dirty-gold) !important;
}
.form-floating > label::after {
@@ -51,9 +51,9 @@ input.form-control:focus {
.login-button {
font-family: 'Cinzel', serif !important;
font-weight: 700 !important;
font-weight: bold !important;
text-transform: uppercase !important;
letter-spacing: 2px !important;
text-transform: uppercase;
background: #000 !important;
border: 1px solid var(--imperial-gold) !important;
color: var(--imperial-gold) !important;
@@ -69,8 +69,8 @@ input.form-control:focus {
.login-button:hover {
background: var(--blood-god) !important;
border-color: var(--plasma-glow) !important;
color: #fff !important;
box-shadow: 0 0 20px var(--blood-god);
text-shadow: 0 0 5px #fff;
border-color: var(--plasma-glow) !important;
box-shadow: 0 0 20px var(--blood-god) !important;
text-shadow: 0 0 5px #fff !important;
}

View File

@@ -35,3 +35,38 @@ li.border-right .nav-link-custom {
.nav-link-custom::before { content: '[ '; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
.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; }
.nav-button {
all: unset;
display: block;
padding: 15px 30px;
color: #666;
font-family: 'Cinzel', serif;
font-weight: 700;
text-transform: uppercase;
transition: 0.3s;
position: relative;
cursor: pointer;
}
.border-left .nav-button {
border-left: 1px solid #222;
}
.border-right .nav-button {
border-right: 1px solid #222;
}
.nav-button:hover {
color: var(--parchment);
background-color: var(--blood-god);
box-shadow: inset 0 0 10px #000;
}
.nav-button::before { content: '[ '; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
.nav-button::after { content: ' ]'; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
.nav-button:hover::before,
.nav-button:hover::after {
opacity: 1;
}

View File

@@ -2,6 +2,7 @@ button.show-button {
color: var(--show-btn-color);
}
button.show-button:hover {
color: var(--show-btn-hover);
}

View File

@@ -1,6 +1,65 @@
/* latin-ext */
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin */
@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");
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v16/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root {
@@ -40,6 +99,13 @@ body::after {
}
/* SECCIONES Y TITULOS */
section {
padding: 80px 20px !important;
max-width: 1200px !important;
margin: auto !important;
scroll-margin-top: 40px;
}
.section-title {
font-family: 'Cinzel', serif;
font-size: 2.5rem;
@@ -57,6 +123,31 @@ body::after {
vertical-align: middle;
}
/* SPIN */
.spin-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
}
.spin-background img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.15;
transition: opacity 0.3s ease;
}
/* BOTONES */
.btn-imperial {
display: inline-block;

19
src/i18n.js Normal file
View File

@@ -0,0 +1,19 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "@/locales/en.json";
import es from "@/locales/es.json";
i18n
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
es: { translation: es },
},
lng: "en",
fallbackLng: "es",
interpolation: { escapeValue: false },
});
export default i18n;

60
src/locales/en.json Normal file
View File

@@ -0,0 +1,60 @@
{
"header": {
"subtitle": "Markcus Garklios The Executioner Crusader"
},
"nav": {
"inicio": { "label": "Home", "href": "home" },
"muestras": { "label": "Portfolio", "href": "portfolio" },
"pedidos": { "label": "Comissions", "href": "comissions" },
"sobre": { "label": "About Markcus", "href": "about-me" },
"login": { "label": "Login", "href": "login" }
},
"home": {
"inicio_title": "+++ INITIALIZING +++",
"inicio_thought": "// THOUGHT OF THE DAY: HOPE IS THE FIRST STEP TOWARD DISAPPOINTMENT.",
"inicio_welcome": "Welcome to Markcus' personal manufactorum. Here, grey miniatures are purged of their lack of color and blessed with sacred pigments, Nuln Oil washes, and ritual dry brushing.",
"inicio_note": "We don't paint toys. We forge veterans of the Long War.",
"inicio_button": "Start Order Protocol",
"muestras_title": "+++ Portfolio +++",
"muestras": [
{
"icon": "☠",
"title": "Sample A-1: Astartes Pattern"
},
{
"icon": "⚙",
"title": "Sample B-2: Engine War"
},
{
"icon": "⚔",
"title": "Sample C-3: Xenos Filth"
}
],
"pedidos_title": "+++ Plea to the Manufactorum +++",
"pedidos_text": "Fill in the details to request the attention of the Artificer. Be precise, time is an Emperor-limited resource.",
"pedidos_name": "Commander Designation (Name)",
"pedidos_email": "Vox Frequency (Email)",
"pedidos_requirements": "Detail mission requirements: Color scheme, Faction, Shading level...",
"pedidos_button": "Transmit",
"sobre_title": "+++ File: Markcus +++",
"sobre_status": {
"k": "[STATUS]:",
"v": "Operational"
},
"sobre_location": {
"k": "[LOCATION]:",
"v": "Andalusia"
},
"sobre_specialty": {
"k": "[SPECIALTY]:",
"v": "Acrilic painting, realistic details"
},
"sobre_text": "I´m Markcus, a loyal servant of the Emperor. My purpose is to expand the Emperor's domains, through wargaming and miniature modeling. Warhammer and miniature painting are my hobby and passion. That's why I put my skills as a painter at the service of all of you who want to increase your collection, even if you're a veteran or a beginner. I can paint anything: Space Marines, Imperial Guard, Necrons, Orks, etc. I can also paint miniatures that aren't from Warhammer."
},
"login": {
"title": "Authentication required",
"user_label": "Identifier",
"password_label": "Key",
"button": "Proceed"
}
}

54
src/locales/es.json Normal file
View File

@@ -0,0 +1,54 @@
{
"header": {
"subtitle": "Markcus Garklios The Executioner Crusader"
},
"nav": {
"inicio": { "label": "Inicio", "href": "inicio" },
"muestras": { "label": "Portafolio", "href": "portafolio" },
"pedidos": { "label": "Comisiones", "href": "comisiones" },
"sobre": { "label": "Sobre Markcus", "href": "sobre-mi" },
"login": { "label": "Login", "href": "login" }
},
"home": {
"inicio_title": "+++ INICIALIZANDO +++",
"inicio_thought": "// PENSAMIENTO DEL DÍA: LA ESPERANZA ES EL PRIMER PASO HACIA LA DECEPCIÓN.",
"inicio_welcome": "Bienvenido al manufactorum personal de Markcus. Aquí, las miniaturas grises son purgadas de su falta de color y bendecidas con pigmentos sagrados, lavados de Nuln Oil y pincel seco ritual.",
"inicio_note": "No pintamos juguetes. Forjamos veteranos de la Larga Guerra.",
"inicio_button": "Iniciar Protocolo de Encargo",
"muestras_title": "+++ Portafolio +++",
"muestras": [
{ "icon": "☠", "title": "Muestra A-1: Astartes Pattern" },
{ "icon": "⚙", "title": "Muestra B-2: Engine War" },
{ "icon": "⚔", "title": "Muestra C-3: Xenos Filth" }
],
"pedidos_title": "+++ Súplica al Manufactorum +++",
"pedidos_text": "Rellena los datos para solicitar la atención del Artífice. Sé preciso, el tiempo es un recurso limitado del Emperador.",
"pedidos_name": "Designación del Comandante (Nombre)",
"pedidos_email": "Frecuencia Vox (Email)",
"pedidos_requirements": "Detalla los requerimientos de la misión: Esquema de color, Facción, Nivel de degradado...",
"pedidos_button": "Transmitir",
"sobre_title": "+++ Archivo: Markcus +++",
"sobre_status": {
"k": "[ESTADO]:",
"v": "Operativo"
},
"sobre_location": {
"k": "[UBICACIÓN]:",
"v": "Andalucía"
},
"sobre_specialty": {
"k": "[ESPECIALIDAD]:",
"v": "Pintura acrílica, detalles realistas"
},
"sobre_text": "Soy Markcus, un leal servidor del Emperador. Mi propósito es expandir los dominios del Emperador, a través de wargames y modelado de miniaturas. Warhammer y la pintura de miniaturas son mi afición y pasión. Por eso pongo mis habilidades como pintor al servicio de todos vosotros que queréis ampliar vuestra colección, ya seáis veteranos o principiantes. Puedo pintar cualquier cosa: Marines Espaciales, Guardia Imperial, Necrones, Orkos, etc. También puedo pintar miniaturas que no sean de Warhammer."
},
"login": {
"title": "Autenticación requerida",
"user_label": "Identificador",
"password_label": "Clave",
"button": "Acceder"
}
}

View File

@@ -1,18 +1,93 @@
import StartSection from "@/components/Home/StartSection";
import SamplesSection from "@/components/Home/SamplesSection";
import VoxSection from "@/components/Home/VoxSection";
import AboutMeSection from "@/components/Home/AboutMeSection";
import ContentWrapper from "@/components/ContentWrapper";
import CustomContainer from "@/components/CustomContainer";
import ScrollBackgroundSpin from "@/components/Home/ScrollBackgroundSpin";
import TechCard from "@/components/TechCard";
import { HashLink } from "react-router-hash-link/dist/react-router-hash-link.cjs.production";
import { useTranslation } from "react-i18next";
const Home = () => {
const { t } = useTranslation();
const muestras = t("home.muestras", { returnObjects: true });
return (
<ContentWrapper>
<ScrollBackgroundSpin />
<CustomContainer>
<StartSection />
<SamplesSection />
<VoxSection />
<AboutMeSection />
<section id={t("nav.inicio.href")} className="container py-5">
<h2 className="section-title text-center mb-5">{t("home.inicio_title")}</h2>
<TechCard>
<p style={{ color: 'var(--imperial-gold)', fontSize: '0.9rem' }}>
{t("home.inicio_thought")}
</p>
<hr style={{ border: 0, borderTop: '1px solid #333', margin: '20px 0' }} />
<p className="mb-4">
{t("home.inicio_welcome")}
</p>
<p className="mb-4">{t("home.inicio_note")}</p>
<div className="mt-4">
<HashLink smooth to={"/#pedidos"} className="btn-imperial">
{t("home.inicio_button")}
</HashLink>
</div>
</TechCard>
</section>
<section id={t("nav.muestras.href")} className="container py-5">
<h2 className="section-title text-center mb-5">{t("home.muestras_title")}</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>
<section id={t("nav.pedidos.href")} className="container py-5">
<h2 className="section-title text-center mb-5">{t("home.pedidos_title")}</h2>
<TechCard>
<p className="mb-4">{t("home.pedidos_text")}</p>
<form>
<div className="mb-3">
<input type="text" className="tech-input" placeholder={t("home.pedidos_name")} />
</div>
<div className="mb-3">
<input type="email" className="tech-input" placeholder={t("home.pedidos_email")} />
</div>
<div className="mb-4">
<textarea className="tech-textarea" rows="6" placeholder={t("home.pedidos_requirements")}></textarea>
</div>
<button type="submit" className="btn-imperial border-0">{t("home.pedidos_button")}</button>
</form>
</TechCard>
</section>
<section id={t("nav.sobre.href")} className="container py-5">
<h2 className="section-title text-center mb-5">{t("home.sobre_title")}</h2>
<TechCard>
<div className="row align-items-center">
<div className="col-12">
<p>
<strong>{t("home.sobre_status.k")}</strong> {t("home.sobre_status.v")}<br />
<strong>{t("home.sobre_location.k")}</strong> {t("home.sobre_location.v")}<br />
<strong>{t("home.sobre_specialty.k")}</strong> {t("home.sobre_specialty.v")}
</p>
<p className="mt-3 m-0">
{t("home.sobre_text")}
</p>
</div>
</div>
</TechCard>
</section>
</CustomContainer>
</ContentWrapper>
);