Add: translations. Fix: some css
@@ -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"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 340 KiB |
BIN
public/images/mini_1.jpeg
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
public/images/mini_2.jpeg
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
public/images/mini_3.jpeg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/images/mini_4.jpeg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
public/images/mini_5.jpeg
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
public/images/pfp.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
10
src/App.jsx
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,8 +2,10 @@ import PropTypes from 'prop-types';
|
||||
|
||||
const CustomContainer = ({ children }) => {
|
||||
return (
|
||||
<main className="mx-4 my-5">
|
||||
<main className="container my-5">
|
||||
<div className="d-flex flex-column gap-5">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import '@/css/Header.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const 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">SUBTITULO SUBTITULO SUBTITULO SUBTITULO</p>
|
||||
<p className="m-0">{t("header.subtitle")}</p>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
37
src/components/Home/ScrollBackgroundSpin.jsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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. 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;
|
||||
33
src/components/LanguageButton.jsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
{onClick || item.href === "#" ? (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="nav-button"
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</button>
|
||||
) : (
|
||||
<HashLink smooth to={item.href} className="nav-link-custom" onClick={handleClick}>
|
||||
{item.label}
|
||||
</HashLink>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ button.show-button {
|
||||
color: var(--show-btn-color);
|
||||
|
||||
}
|
||||
|
||||
button.show-button:hover {
|
||||
color: var(--show-btn-hover);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||