revamped: whole UI
This commit is contained in:
@@ -2,9 +2,8 @@
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jose.portfolio()</title>
|
||||
<title>Jose::portfolio()</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
71
package-lock.json
generated
71
package-lock.json
generated
@@ -17,7 +17,8 @@
|
||||
"framer-motion": "^12.4.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0"
|
||||
"react-router-dom": "^7.2.0",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
@@ -2726,6 +2727,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra/node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -2905,6 +2929,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||
@@ -3529,6 +3559,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz",
|
||||
"integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^0.1.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -4823,6 +4865,24 @@
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/twemoji": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/twemoji/-/twemoji-14.0.2.tgz",
|
||||
"integrity": "sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fs-extra": "^8.0.1",
|
||||
"jsonfile": "^5.0.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"universalify": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/twemoji-parser": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-14.0.0.tgz",
|
||||
"integrity": "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -4933,6 +4993,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"framer-motion": "^12.4.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0"
|
||||
"react-router-dom": "^7.2.0",
|
||||
"twemoji": "^14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB |
BIN
public/cat.gif
Normal file
BIN
public/cat.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
BIN
public/fonts/FiraCode.ttf
Normal file
BIN
public/fonts/FiraCode.ttf
Normal file
Binary file not shown.
BIN
public/fonts/OpenSans.ttf
Normal file
BIN
public/fonts/OpenSans.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ProductSansBold.ttf
Normal file
BIN
public/fonts/ProductSansBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ProductSansBoldItalic.ttf
Normal file
BIN
public/fonts/ProductSansBoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ProductSansItalic.ttf
Normal file
BIN
public/fonts/ProductSansItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/ProductSansRegular.ttf
Normal file
BIN
public/fonts/ProductSansRegular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
32
src/App.jsx
32
src/App.jsx
@@ -1,4 +1,4 @@
|
||||
import '@/css/App.css'
|
||||
import '@/css/index.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||
|
||||
@@ -10,6 +10,8 @@ import Link from '@/components/Link.jsx'
|
||||
import NavBar from '@/components/NavBar.jsx'
|
||||
import Footer from '@/components/Footer.jsx'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import HackerCat from '@/components/HackerCat'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const App = () => {
|
||||
|
||||
@@ -23,6 +25,32 @@ const App = () => {
|
||||
return delta + (septemberOrLater ? 1 : 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const setRandomFavicon = () => {
|
||||
const start = 0x1F300;
|
||||
const end = 0x1F64F;
|
||||
|
||||
const randomCode = Math.floor(Math.random() * (end - start + 1)) + start;
|
||||
|
||||
const hexCode = randomCode.toString(16);
|
||||
|
||||
const twemojiUrl = `https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/${hexCode}.svg`;
|
||||
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
link.href = twemojiUrl;
|
||||
|
||||
link.type = 'image/x-icon';
|
||||
};
|
||||
|
||||
setRandomFavicon();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Hola, soy Jose" subtitle={`Estudiante de Ingeniería de Computadores en la US (${getCurrentYear()}º año)`} />
|
||||
@@ -35,6 +63,8 @@ const App = () => {
|
||||
<Route path="/proyectos" element={<Projects />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
|
||||
<HackerCat />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link as RouterLink, useInRouterContext } from 'react-router-dom';
|
||||
|
||||
const AbstractLink = ({ to, children, className = '', ...props }) => {
|
||||
const isInternal = to.startsWith('/');
|
||||
const isRouterAvailable = useInRouterContext(); // verifica si estamos dentro de un Router
|
||||
const isRouterAvailable = useInRouterContext();
|
||||
|
||||
if (isInternal && isRouterAvailable) {
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import "../css/Card.css";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
import "@/css/Card.css";
|
||||
|
||||
const Card = ({ title, status, children, styleMode, className, titleIcon }) => {
|
||||
const cardRef = useRef(null);
|
||||
const [shortTitle, setShortTitle] = useState(title);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const checkSize = () => {
|
||||
if (cardRef.current) {
|
||||
const width = cardRef.current.offsetWidth;
|
||||
if (width < 300 && title.length > 15) {
|
||||
setShortTitle(title.slice(0, 10) + ".");
|
||||
setShortTitle(title.slice(0, 20) + ".");
|
||||
} else {
|
||||
setShortTitle(title);
|
||||
}
|
||||
@@ -31,7 +29,7 @@ const Card = ({ title, status, children, styleMode, className, titleIcon }) => {
|
||||
className={styleMode === "override" ? `${className}` :
|
||||
`col-xl-3 col-sm-6 d-flex flex-column align-items-center p-3 card-container ${className}`}
|
||||
>
|
||||
<div className={`card p-3 w-100 ${theme}`}>
|
||||
<div className={`card p-3 w-100`}>
|
||||
<h3 className="text-center">
|
||||
{titleIcon}
|
||||
{shortTitle}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import Card from "./Card.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CardContainer = ({ cards, className }) => {
|
||||
return (
|
||||
<div className={`row justify-content-center g-0 ${className}`}>
|
||||
{cards.map((card, index) => (
|
||||
<Card key={index} title={card.title} status={card.status} styleMode={card.styleMode} className={card.className} titleIcon={card.titleIcon}>
|
||||
<p className="card-text text-center">{card.content}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardContainer.propTypes = {
|
||||
cards: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CardContainer;
|
||||
@@ -1,16 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Container = ({ children, fluid }) => {
|
||||
return (
|
||||
<div className={`${fluid ? "container-fluid" : "container"}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Container.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
fluid: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default Container;
|
||||
@@ -1,37 +1,42 @@
|
||||
import Link from './Link';
|
||||
import '../css/Footer.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from '@/components/Link';
|
||||
import '@/css/Footer.css';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
const Footer = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const hearts = ["❤️", "💛", "🧡", "💚", "💙", "💜"];
|
||||
const [heart, setHeart] = useState("💜");
|
||||
|
||||
const randomHeart = useCallback(() => {
|
||||
return hearts[Math.floor(Math.random() * hearts.length)];
|
||||
}, [hearts]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setHeart(randomHeart());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const randomHeart = () => hearts[Math.floor(Math.random() * hearts.length)];
|
||||
}, [randomHeart]);
|
||||
|
||||
return (
|
||||
<footer className="py-4 d-flex text-center justify-content-center bg-dark text-white">
|
||||
<p className='m-0 p-0'>Dev'd with {heart} by Gallardo7761</p>
|
||||
<span className="mx-3">|</span>
|
||||
<div className="d-flex gap-3 justify-content-center">
|
||||
{[
|
||||
{ text: 'MiarmaGit', to: 'https://git.miarma.net/Gallardo7761' },
|
||||
{ text: 'Instagram', to: 'https://instagram.com/gallardoo7761' },
|
||||
{ text: 'Reddit', to: 'https://reddit.com/u/Gallardo7761' },
|
||||
].map((social) => (
|
||||
<Link key={social.text} to={social.to}>
|
||||
{social.text}
|
||||
</Link>
|
||||
))}
|
||||
<li className="nav-item" style={{ listStyleType: "none" }}>
|
||||
<a className='mastodon-verify' rel="me" href="https://mastodon.social/@gallardo7761">Mastodon</a>
|
||||
</li>
|
||||
<footer className="main-footer">
|
||||
<div className="container footer-container">
|
||||
<p className="footer-dev-text">
|
||||
Dev'd with <span className="heart-icon">{heart}</span> by Gallardo7761
|
||||
</p>
|
||||
|
||||
<span className="footer-divider">|</span>
|
||||
|
||||
<ul className="footer-links">
|
||||
<Link to="https://git.miarma.net/Gallardo7761">MiarmaGit</Link>
|
||||
<Link to="https://instagram.com/gallardoo7761">Instagram</Link>
|
||||
<Link to="https://reddit.com/u/Gallardo7761">Reddit</Link>
|
||||
<li className="nav-item">
|
||||
<a className="mastodon-verify" rel="noopener noreferer" target='_blank' href="https://masto.es/@gallardo7761">
|
||||
Mastodon
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
89
src/components/HackerCat.jsx
Normal file
89
src/components/HackerCat.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const HackerCat = () => {
|
||||
const [pos, setPos] = useState({ x: -100, y: 500 });
|
||||
const [command, setCommand] = useState("");
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const linuxCommands = [
|
||||
"sudo rm -rf /",
|
||||
"apt update",
|
||||
"git push --force",
|
||||
"kill -9 -1",
|
||||
"shutdown -h now",
|
||||
"ping 8.8.8.8",
|
||||
"whoami"
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const spawnCat = () => {
|
||||
if (Math.random() > 0.4) return;
|
||||
|
||||
const windowH = window.innerHeight;
|
||||
const randomY = Math.floor(Math.random() * (windowH - 200)) + 50;
|
||||
const randomCmd = linuxCommands[Math.floor(Math.random() * linuxCommands.length)];
|
||||
|
||||
setPos({ x: -200, y: randomY });
|
||||
setCommand(randomCmd);
|
||||
setIsVisible(true);
|
||||
|
||||
setTimeout(() => setIsVisible(false), 11000);
|
||||
};
|
||||
|
||||
const timer = setInterval(spawnCat, 300000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ x: pos.x, y: pos.y }}
|
||||
animate={{
|
||||
x: [pos.x, window.innerWidth + 200],
|
||||
y: [pos.y, pos.y - 50, pos.y + 50, pos.y]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
ease: "linear",
|
||||
y: { duration: 2, repeat: 5, ease: "easeInOut" }
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
background: '#1e1e1e',
|
||||
color: 'var(--text-color)',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
fontFamily: "'Fira Code', monospace",
|
||||
border: '1px solid var(--primary-color)',
|
||||
marginBottom: '5px',
|
||||
boxShadow: '0 0 10px var(--secondary-color)',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
<span style={{ color: '#ffffff' }}>$</span> {command}
|
||||
</div>
|
||||
|
||||
<img
|
||||
src="/cat.gif"
|
||||
alt="Hacker Cat"
|
||||
style={{ width: '100px', filter: 'drop-shadow(0 0 5px rgba(0,0,0,0.5))' }}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HackerCat;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import "../css/Header.css";
|
||||
import "@/css/Header.css";
|
||||
|
||||
const Header = ({ subtitle }) => {
|
||||
const names = useMemo(() => ["Jose", "Gallardo7761"], []);
|
||||
@@ -14,6 +14,7 @@ const Header = ({ subtitle }) => {
|
||||
const baseText = "Hola, soy ";
|
||||
const fullText = baseText + (isJose ? names[0] : names[1]);
|
||||
|
||||
// FSM ahh
|
||||
if (animationState === "writing") {
|
||||
if (text.length < fullText.length) {
|
||||
timeout = setTimeout(() => {
|
||||
@@ -36,15 +37,7 @@ const Header = ({ subtitle }) => {
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [text, animationState, isJose, names]); // -> Funciona !!! --> edit 08/25: fue mi primera pagina en react, se nota XD
|
||||
|
||||
const subtitleWithCode = (
|
||||
<code>
|
||||
printf("%s", "{subtitle}");
|
||||
</code>
|
||||
);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
}, [text, animationState, isJose, names]);
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
@@ -62,10 +55,8 @@ const Header = ({ subtitle }) => {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
>
|
||||
{isHovered ? subtitleWithCode : subtitle}
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import AbstractLink from './AbstractLink';
|
||||
import AbstractLink from '@/components/AbstractLink';
|
||||
|
||||
const Link = ({ to, children, isNavbar, className = '', ...props }) => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
import '../css/NavBar.css';
|
||||
import '@/css/NavBar.css';
|
||||
|
||||
const NavBar = ({ children }) => {
|
||||
const navVariants = {
|
||||
@@ -20,7 +20,9 @@ const NavBar = ({ children }) => {
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
<span className="navbar-toggler-icon">
|
||||
<span className="navbar-toggler-icon-custom-line"></span>
|
||||
</span>
|
||||
</button>
|
||||
<motion.div
|
||||
className="collapse navbar-collapse"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useTheme } from "../contexts/ThemeContext.jsx";
|
||||
import "../css/ThemeButton.css";
|
||||
|
||||
export default function ThemeButton() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button className="theme-toggle" onClick={toggleTheme}>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ConfigContext.jsx
|
||||
*
|
||||
* Este archivo define el contexto de configuración para la aplicación, permitiendo cargar y manejar la configuración desde un archivo externo.
|
||||
*
|
||||
* Importaciones:
|
||||
* - createContext, useContext, useState, useEffect: Funciones de React para crear y utilizar contextos, manejar estados y efectos secundarios.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - ConfigContext: Contexto que almacena la configuración cargada, el estado de carga y cualquier error ocurrido durante la carga de la configuración.
|
||||
* - ConfigProvider: Proveedor de contexto que maneja la carga de la configuración y proporciona el estado de la configuración a los componentes hijos.
|
||||
* - Utiliza `fetch` para cargar la configuración desde un archivo JSON.
|
||||
* - Maneja el estado de carga y errores durante la carga de la configuración.
|
||||
* - useConfig: Hook personalizado para acceder al contexto de configuración.
|
||||
*
|
||||
* PropTypes:
|
||||
* - ConfigProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
|
||||
*
|
||||
*/
|
||||
|
||||
const ConfigContext = createContext();
|
||||
|
||||
export const ConfigProvider = ({ children }) => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [configLoading, setLoading] = useState(true);
|
||||
const [configError, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await fetch("/config/settings.json");
|
||||
if (!response.ok) throw new Error("Error al cargar settings.json");
|
||||
const json = await response.json();
|
||||
setConfig(json);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, configLoading, configError }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export const useConfig = () => useContext(ConfigContext);
|
||||
@@ -1,67 +0,0 @@
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* DataContext.jsx
|
||||
*
|
||||
* Este archivo define el contexto de datos para la aplicación, permitiendo obtener y manejar datos de una fuente externa.
|
||||
*
|
||||
* Importaciones:
|
||||
* - createContext, useContext, useState, useEffect: Funciones de React para crear y utilizar contextos, manejar estados y efectos secundarios.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - DataContext: Contexto que almacena los datos obtenidos, el estado de carga y cualquier error ocurrido durante la obtención de datos.
|
||||
* - DataProvider: Proveedor de contexto que maneja la obtención de datos y proporciona el estado de los datos a los componentes hijos.
|
||||
* - Utiliza `fetch` para obtener datos de una URL construida a partir de la configuración proporcionada.
|
||||
* - Maneja el estado de carga y errores durante la obtención de datos.
|
||||
* - useData: Hook personalizado para acceder al contexto de datos.
|
||||
*
|
||||
* PropTypes:
|
||||
* - DataProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
|
||||
* - DataProvider también espera una configuración (`config`) que debe incluir `baseUrl` (string) y opcionalmente `params` (objeto).
|
||||
*
|
||||
*/
|
||||
|
||||
const DataContext = createContext();
|
||||
|
||||
export const DataProvider = ({ children, config }) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [dataLoading, setLoading] = useState(true);
|
||||
const [dataError, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams(config.params).toString();
|
||||
const url = `${config.baseUrl}?${queryParams}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Error al obtener datos");
|
||||
const result = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={{ data, dataLoading, dataError }}>
|
||||
{children}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
DataProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
config: PropTypes.shape({
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export const useData = () => useContext(DataContext);
|
||||
@@ -1,55 +0,0 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* ThemeContext.jsx
|
||||
*
|
||||
* Este archivo define el contexto de tema para la aplicación, permitiendo cambiar entre temas claro y oscuro.
|
||||
*
|
||||
* Importaciones:
|
||||
* - createContext, useContext, useEffect, useState: Funciones de React para crear y utilizar contextos, manejar efectos secundarios y estados.
|
||||
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
|
||||
*
|
||||
* Funcionalidad:
|
||||
* - ThemeContext: Contexto que almacena el tema actual y la función para cambiarlo.
|
||||
* - ThemeProvider: Proveedor de contexto que maneja el estado del tema y proporciona la función para alternar entre temas.
|
||||
* - Utiliza `localStorage` para persistir el tema seleccionado.
|
||||
* - Aplica la clase correspondiente al `body` del documento para reflejar el tema actual.
|
||||
* - useTheme: Hook personalizado para acceder al contexto del tema.
|
||||
*
|
||||
* PropTypes:
|
||||
* - ThemeProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
|
||||
*
|
||||
*/
|
||||
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
return localStorage.getItem("theme") || "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.remove("light", "dark");
|
||||
document.body.classList.add(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
ThemeProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body.light {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background: linear-gradient(135deg, var(--gradient-primary), var(--gradient-secondary));
|
||||
color: white;
|
||||
}
|
||||
@@ -1,61 +1,50 @@
|
||||
.card-container {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.card.light {
|
||||
background: linear-gradient(145deg, #eeeeee, #dadada);
|
||||
}
|
||||
|
||||
.card.light > div.card-content > p.card-text {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.card.light > span.status {
|
||||
background: #E0E0E0;
|
||||
}
|
||||
|
||||
.card.dark {
|
||||
background: linear-gradient(145deg, var(--card-gradient-primary), var(--card-gradient-secondary));
|
||||
}
|
||||
|
||||
.card.dark > div.card-content > p.card-text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card.dark > span.status {
|
||||
background: #505050;
|
||||
border-radius: 16px;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
border: 1px solid rgba(255, 63, 31, 0.3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
background: linear-gradient(145deg, #1e1e1e, #252525);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 10px 20px var(--box-shadow);
|
||||
transform: translateY(-10px) rotateX(2deg);
|
||||
box-shadow: 0 15px 30px rgba(255, 63, 31, 0.3);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card>h3 {
|
||||
font-size: 1.3em;
|
||||
.card h3 {
|
||||
font-size: 1.1em;
|
||||
font-family: 'Product Sans Bold', sans-serif;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
font-size: 0.95em;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card-text {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card > h3 > .svg-inline--fa {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
p.card-text {
|
||||
font-size: 2.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card>span.status {
|
||||
font-size: 0.9em;
|
||||
color: #A0A0A0;
|
||||
padding: 5px 10px;
|
||||
background: var(--card-background);
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
border: 1px solid rgba(255, 63, 31, 0.4);
|
||||
color: var(--text-color);
|
||||
}
|
||||
@@ -1,15 +1,57 @@
|
||||
footer > p > a {
|
||||
color: var(--primary-color);
|
||||
.main-footer {
|
||||
background-color: #141414;
|
||||
border-top: 1px solid #343a40;
|
||||
padding: 2rem 0;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
footer > p > a:hover {
|
||||
color: var(--secondary-color);
|
||||
.footer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
footer > div.d-flex > li.nav-item > a {
|
||||
color: var(--primary-color);
|
||||
@media (min-width: 768px) {
|
||||
.footer-container {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
footer > div.d-flex > li.nav-item > a:hover {
|
||||
color: var(--secondary-color);
|
||||
.footer-dev-text {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
color: #495057;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.footer-divider {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Estilo para todos los enlaces del footer */
|
||||
.footer-links a,
|
||||
.footer-links .link-component {
|
||||
color: var(--primary-color, #00d1b2);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.footer-links a:hover {
|
||||
color: var(--secondary-color, #fff);
|
||||
}
|
||||
@@ -1,20 +1,40 @@
|
||||
/* src/css/Header.css */
|
||||
.header {
|
||||
padding: 1rem 0; /* py-4 */
|
||||
background-color: #212529; /* bg-dark */
|
||||
color: #fff; /* text-white */
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* shadow-lg */
|
||||
padding: 4rem 0;
|
||||
background-color: #141414;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 2.5rem; /* display-5 */
|
||||
font-weight: 700; /* fw-bold */
|
||||
margin-bottom: 1rem; /* mb-4 */
|
||||
color: var(--primary-color); /* Color azul de Bootstrap */
|
||||
font-family: 'Fira Code';
|
||||
font-size: clamp(1.6rem, 7vw, 3.5rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--primary-color);
|
||||
white-space: nowrap;
|
||||
min-height: 1.2em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title::after {
|
||||
content: "_";
|
||||
margin-left: 5px;
|
||||
color: var(--primary-color);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 1.25rem; /* lead */
|
||||
color: #6c757d; /* text-muted */
|
||||
}
|
||||
|
||||
font-size: clamp(1rem, 3vw, 1.25rem);
|
||||
font-family: 'Fira Code';
|
||||
color: #adb5bd;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
57
src/css/Home.css
Normal file
57
src/css/Home.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.home-container {
|
||||
background: linear-gradient(135deg, var(--gradient-primary), var(--gradient-secondary));
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 63, 31, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 63, 31, 0.2);
|
||||
border-radius: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 8px 32px 0 rgba(255, 63, 31, 0.2);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.skill-badge {
|
||||
background: rgba(255, 63, 31, 0.1) !important;
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
color: white !important;
|
||||
font-family: "Fira Code", monospace;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: "Product Sans Bold", sans-serif;
|
||||
letter-spacing: 2px;
|
||||
color: var(--primary-color);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 0;
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.project-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.interest-item {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-left: 3px solid var(--primary-color);
|
||||
padding: 1rem;
|
||||
font-family: "Product Sans", sans-serif;
|
||||
}
|
||||
@@ -9,5 +9,40 @@
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
background-color: var(--gradient-primary);
|
||||
}
|
||||
background-color: rgb(40, 40, 40);
|
||||
border-top: 1px solid #343a40;
|
||||
border-bottom: 1px solid #343a40;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
border: none !important;
|
||||
padding: 0.5rem;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
background-image: none !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.navbar-toggler-icon::before,
|
||||
.navbar-toggler-icon::after,
|
||||
.navbar-toggler-icon-custom-line {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-toggler-icon::before { transform: translateY(-8px); }
|
||||
.navbar-toggler-icon::after { transform: translateY(8px); }
|
||||
5
src/css/Projects.css
Normal file
5
src/css/Projects.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.projects-container {
|
||||
background: linear-gradient(135deg, var(--gradient-primary), var(--gradient-secondary)) !important;
|
||||
color: white !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
@@ -4,66 +4,55 @@
|
||||
--text-shadow: #771e0e;
|
||||
--box-shadow: #a81900;
|
||||
|
||||
--text-color: white;
|
||||
|
||||
--gradient-primary: #1A1A1A;
|
||||
--gradient-secondary: #2A2A2A;
|
||||
|
||||
--card-background: #ff3f1f1a;
|
||||
--card-gradient-primary: #252525;
|
||||
--card-gradient-secondary: #353535;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--secondary-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
background-color: rgba(255, 63, 31, 0.1) !important;
|
||||
}
|
||||
|
||||
h2.h3 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mastodon-verify {
|
||||
.primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/poppins/v22/pxiEyp8kv8JHgFVrJJnecmNE.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;
|
||||
html,
|
||||
body {
|
||||
font-family: "Open Sans";
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/poppins/v22/pxiEyp8kv8JHgFVrJJfecg.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;
|
||||
.custom-hr {
|
||||
height: 1px;
|
||||
border: none;
|
||||
background: linear-gradient(to right, transparent, var(--primary-color), transparent);
|
||||
opacity: 0.4;
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/poppins/v22/pxiByp8kv8JHgFVrLEj6Z1JlFc-K.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;
|
||||
font-family: "Fira Code";
|
||||
src: url('/fonts/FiraCode.ttf');
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/poppins/v22/pxiByp8kv8JHgFVrLEj6Z1xlFQ.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;
|
||||
font-family: "Open Sans";
|
||||
src: url('/fonts/OpenSans.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Product Sans";
|
||||
src: url('/fonts/ProductSansRegular.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Product Sans Italic";
|
||||
src: url('/fonts/ProductSansItalic.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Product Sans Italic Bold";
|
||||
src: url('/fonts/ProductSansBoldItalic.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Product Sans Bold";
|
||||
src: url('/fonts/ProductSansBold.ttf');
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import '@/css/Home.css';
|
||||
import Card from '@/components/Card';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="min-vh-100" style={{ background: 'linear-gradient(to bottom right, #f8f9fa, #dee2e6)' }}>
|
||||
<main className="container-fluid">
|
||||
<div className="home-container">
|
||||
<main className="container py-5">
|
||||
<AboutSection />
|
||||
<hr className="custom-hr" />
|
||||
<ProjectsSection />
|
||||
<hr className="custom-hr" />
|
||||
<InterestsSection />
|
||||
</main>
|
||||
</div>
|
||||
@@ -14,111 +18,122 @@ const Home = () => {
|
||||
|
||||
const AboutSection = () => {
|
||||
return (
|
||||
<section className="py-5 bg-light">
|
||||
<div className="container">
|
||||
<div className="row align-items-center">
|
||||
<motion.div
|
||||
className="col-md-6 mb-4 mb-md-0"
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="rounded-circle overflow-hidden mx-auto shadow-lg" style={{ width: '300px', height: '300px' }}>
|
||||
<section className="py-4">
|
||||
<div className="row align-items-center">
|
||||
<motion.div
|
||||
className="col-md-5 mb-4 mb-md-0 d-flex justify-content-center"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="position-relative">
|
||||
<div className="position-absolute top-50 start-50 translate-middle rounded-circle"
|
||||
style={{ width: '300px', height: '300px', background: 'var(--primary-color)', filter: 'blur(60px)', opacity: 0.15 }}></div>
|
||||
|
||||
<div className="rounded-circle overflow-hidden shadow-lg border border-3 border-danger" style={{ width: '280px', height: '280px' }}>
|
||||
<motion.img
|
||||
src="https://git.miarma.net/avatars/2fba8e2d4e39fffec3bbfe128df0cb9934ccf9b49fd6d310244c6b6209739425?size=512"
|
||||
alt="Avatar"
|
||||
className="w-100 h-100 object-cover"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="col-md-6"
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h2 className="h3 fw-bold mb-3">Sobre Mí</h2>
|
||||
<p className="text-muted mb-4">
|
||||
Soy un estudiante de Ingeniería de Computadores en la Universidad de Sevilla, en mi cuarto año.
|
||||
Actualmente me dedico a linux, redes, microcontroladores y webdev. Por aquí abajo dejo con lo que suelo trastear:
|
||||
</p>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{['C', 'Rust', 'Java', 'JS (Vanilla + React)', 'Python', 'DB', 'Linux', 'HDL'].map((skill) => (
|
||||
<motion.span
|
||||
key={skill}
|
||||
className="badge px-3 py-2"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{skill}
|
||||
</motion.span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
const InterestsSection = () => {
|
||||
return (
|
||||
<section className="py-5 bg-light">
|
||||
<div className="container">
|
||||
<h2 className="h3 fw-bold mb-4 text-center">Intereses</h2>
|
||||
<div className="row g-3">
|
||||
{['Informática', 'Electrónica', 'Videojuegos', 'Música', 'Animanga', 'Ciberseguridad'].map((interest, index) => (
|
||||
<motion.div
|
||||
key={interest}
|
||||
className="col-md-4 col-6"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="text-center p-3 bg-white rounded shadow">
|
||||
{interest}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
<motion.div
|
||||
className="col-md-7 mt-4 mt-md-0"
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<h2 className="section-title">SOBRE MÍ</h2>
|
||||
<p className="lead opacity-75 mb-4">
|
||||
Estudiante de 4º de <strong>Ingeniería de Computadores</strong> en la US.
|
||||
Suelo trastear con Linux, redes, electrónica y webdev.
|
||||
</p>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{['C', 'Rust', 'Java', 'JS', 'React', 'Python', 'Linux', 'HDL'].map((skill) => (
|
||||
<motion.span
|
||||
key={skill}
|
||||
className="badge skill-badge px-3 py-2"
|
||||
whileHover={{ y: -3, backgroundColor: 'var(--primary-color)', color: '#000' }}
|
||||
>
|
||||
{skill}
|
||||
</motion.span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectsSection = () => {
|
||||
const projects = [
|
||||
{ title: 'miarma-backend', desc: 'Backend Spring que alimenta todos mis servicios', tech: 'Java/Spring' },
|
||||
{ title: 'riscv-ac', desc: 'Implementación HDL del procesador RISC-V. Arquitectura pura.', tech: 'Verilog/VHDL' },
|
||||
{ title: 'contaminus', desc: 'Proyecto Hack4Change 24/25. Fullstack + IoT.', tech: 'React/Python/IoT' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-5">
|
||||
<div className="container">
|
||||
<h2 className="h3 fw-bold mb-4 text-center">Mis Proyectos Favoritos</h2>
|
||||
<div className="row g-4">
|
||||
{[
|
||||
{ title: 'miarma-backend', desc: 'Backend Spring que alimenta todos mis servicios' },
|
||||
{ title: 'riscv-ac', desc: 'Implementación HDL del procesador RISC-V de la asignatura AC' },
|
||||
{ title: 'contaminus', desc: 'Proyecto presentado al hackathon Hack4Change 24/25 de la ETSII (aunque el jurado no nos echase mucha cuenta...). Mezcla frontend, backend e IoT.' },
|
||||
].map((project, index) => (
|
||||
<motion.div
|
||||
key={project.title}
|
||||
className="col-md-4"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
<section>
|
||||
<h2 className="section-title">MIS PROYECTOS FAVORITOS</h2>
|
||||
<div className="row g-4">
|
||||
{projects.map((project, index) => (
|
||||
<motion.div
|
||||
key={project.title}
|
||||
className="col-md-4 d-flex"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<a
|
||||
href={`https://git.miarma.net/Gallardo7761/${project.title}`}
|
||||
className="text-decoration-none w-100 d-flex"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="card h-100 shadow-sm">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{project.title}</h5>
|
||||
<p className="card-text">{project.desc}</p>
|
||||
<Card
|
||||
title={project.title}
|
||||
status={project.tech}
|
||||
styleMode="override"
|
||||
className="w-100"
|
||||
>
|
||||
<div className="card-content py-2">
|
||||
<p className="opacity-75" style={{ fontSize: '0.95rem' }}>
|
||||
{project.desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const InterestsSection = () => {
|
||||
const interests = ['Informática', 'Electrónica', 'Videojuegos', 'Música', 'Animanga', 'Ciberseguridad'];
|
||||
return (
|
||||
<section>
|
||||
<h2 className="section-title">INTERESES</h2>
|
||||
<div className="row g-3">
|
||||
{interests.map((interest, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="col-md-4 col-6"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
>
|
||||
<div className="interest-item rounded shadow-sm h-100 d-flex align-items-center">
|
||||
<span>{interest}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,49 +1,77 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Card from "@/components/Card";
|
||||
import { motion } from "framer-motion";
|
||||
import '@/css/Projects.css'
|
||||
|
||||
export default function ProjectsLanding() {
|
||||
const [repos, setRepos] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRepos = async () => {
|
||||
try {
|
||||
const userResponse = await fetch("https://api.github.com/users/Gallardo7761/repos");
|
||||
const userRepos = await userResponse.json();
|
||||
const GITEA_URL = "https://git.miarma.net";
|
||||
const USERNAME = "Gallardo7761";
|
||||
|
||||
setRepos(userRepos);
|
||||
try {
|
||||
const response = await fetch(`${GITEA_URL}/api/v1/users/${USERNAME}/repos?limit=100`);
|
||||
if (!response.ok) throw new Error("Fallo en la conexión");
|
||||
const data = await response.json();
|
||||
setRepos(data);
|
||||
} catch (error) {
|
||||
console.error("Error al obtener repos:", error);
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchRepos();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mt-5 mb-5 text-white">
|
||||
<h1 className="text-center text-dark mb-4">Mis Proyectos en GitHub</h1>
|
||||
<p className="text-center text-muted mb-4">
|
||||
(Copia temporal del mirror de GitHub, próximamente desde mi propia instancia Gitea!!!)
|
||||
</p>
|
||||
<div className="row">
|
||||
{repos.map((repo) => (
|
||||
<div key={repo.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<a
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<div className="card bg-dark text-white shadow-sm h-100">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{repo.name}</h5>
|
||||
<p className="card-text text-light">
|
||||
{repo.description || "Sin descripción"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div className="projects-container">
|
||||
<main className="container py-5">
|
||||
{loading ? (
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border primary" role="status"></div>
|
||||
<p className="mt-2">Cargando repos...</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-4">
|
||||
{repos.map((repo, index) => (
|
||||
<motion.div
|
||||
key={repo.id}
|
||||
className="col-md-6 col-lg-4 d-flex"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<a
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-decoration-none w-100"
|
||||
>
|
||||
<Card
|
||||
title={repo.name}
|
||||
status={repo.language || "Desconocido"}
|
||||
styleMode="override"
|
||||
className="h-100 w-100"
|
||||
>
|
||||
<div className="card-content">
|
||||
<p className="opacity-75" style={{ fontSize: '0.9em' }}>
|
||||
{repo.description || "Este repo es tan secreto que no tiene ni descripción."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 d-flex justify-content-between align-items-center">
|
||||
<small className="opacity-50">⭐ {repo.stars_count || 0}</small>
|
||||
{/*<small className="opacity-50">🍴 {repo.forks_count || 0}</small>*/}
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user