[REPO REFACTOR]: changed to a better git repository structure with branches
This commit is contained in:
38
eslint.config.js
Normal file
38
eslint.config.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5184
package-lock.json
generated
Normal file
5184
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "dotme-react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"framer-motion": "^12.4.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.3",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
BIN
public/binary.png
Executable file
BIN
public/binary.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/fonts/Ubuntu-Bold.ttf
Normal file
BIN
public/fonts/Ubuntu-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-BoldItalic.ttf
Normal file
BIN
public/fonts/Ubuntu-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-Italic.ttf
Normal file
BIN
public/fonts/Ubuntu-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-Light.ttf
Normal file
BIN
public/fonts/Ubuntu-Light.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-LightItalic.ttf
Normal file
BIN
public/fonts/Ubuntu-LightItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-Medium.ttf
Normal file
BIN
public/fonts/Ubuntu-Medium.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-MediumItalic.ttf
Normal file
BIN
public/fonts/Ubuntu-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Ubuntu-Regular.ttf
Normal file
BIN
public/fonts/Ubuntu-Regular.ttf
Normal file
Binary file not shown.
29
src/components/AbstractLink.jsx
Normal file
29
src/components/AbstractLink.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import PropTypes from 'prop-types';
|
||||
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
|
||||
|
||||
if (isInternal && isRouterAvailable) {
|
||||
return (
|
||||
<RouterLink className={className} to={to} {...props}>
|
||||
{children}
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a className={className} href={to} target="_blank" rel="noopener noreferrer" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
AbstractLink.propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AbstractLink;
|
||||
43
src/components/App.jsx
Normal file
43
src/components/App.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import '../css/App.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||
|
||||
import Home from '../pages/Home/Home.jsx'
|
||||
import Projects from '../pages/Projects/Projects.jsx'
|
||||
|
||||
import Header from './Header.jsx'
|
||||
import Link from './Link.jsx'
|
||||
import NavBar from './NavBar.jsx'
|
||||
import Footer from './Footer.jsx'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
|
||||
const App = () => {
|
||||
|
||||
const getCursoActualCarrera = () => {
|
||||
const inicio = new Date('2022-09-10');
|
||||
const ahora = new Date();
|
||||
|
||||
const añosPasados = ahora.getFullYear() - inicio.getFullYear();
|
||||
const haPasadoSeptiembre = ahora.getMonth() >= 8;
|
||||
|
||||
const cursoActual = añosPasados + (haPasadoSeptiembre ? 1 : 0);
|
||||
return cursoActual;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Hola, soy Jose" subtitle={`Estudiante de Ingeniería de Computadores en la US (${getCursoActualCarrera()}º año)`} />
|
||||
<NavBar>
|
||||
<Link to="/" isNavbar>Inicio</Link>
|
||||
<Link to="/proyectos" isNavbar>Proyectos</Link>
|
||||
</NavBar>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/proyectos" element={<Projects />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
59
src/components/Card.jsx
Normal file
59
src/components/Card.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import "../css/Card.css";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
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) + ".");
|
||||
} else {
|
||||
setShortTitle(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSize();
|
||||
window.addEventListener("resize", checkSize);
|
||||
return () => window.removeEventListener("resize", checkSize);
|
||||
}, [title]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
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}`}>
|
||||
<h3 className="text-center">
|
||||
{titleIcon}
|
||||
{shortTitle}
|
||||
</h3>
|
||||
<div className="card-content">{children}</div>
|
||||
{status ? <span className="status text-center mt-2">{status}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
styleMode: PropTypes.oneOf(["override", ""]),
|
||||
className: PropTypes.string,
|
||||
titleIcon: PropTypes.node,
|
||||
};
|
||||
|
||||
Card.defaultProps = {
|
||||
styleMode: "",
|
||||
};
|
||||
|
||||
export default Card;
|
||||
27
src/components/CardContainer.jsx
Normal file
27
src/components/CardContainer.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
16
src/components/Container.jsx
Normal file
16
src/components/Container.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
38
src/components/Footer.jsx
Normal file
38
src/components/Footer.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import Link from './Link';
|
||||
import '../css/Footer.css';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const Footer = () => {
|
||||
const hearts = ["❤️", "💛", "🧡", "💚", "💙", "💜"];
|
||||
const [heart, setHeart] = useState("💜");
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setHeart(randomHeart());
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const randomHeart = () => hearts[Math.floor(Math.random() * hearts.length)];
|
||||
|
||||
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: 'GitHub', to: 'https://github.com/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>
|
||||
))}
|
||||
<a className='mastodon-verify' rel="me" href="https://mastodon.social/@gallardo7761">Mastodon</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
79
src/components/Header.jsx
Normal file
79
src/components/Header.jsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { motion } from "framer-motion";
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import "../css/Header.css";
|
||||
|
||||
const Header = ({ subtitle }) => {
|
||||
const names = useMemo(() => ["Jose", "Gallardo7761"], []);
|
||||
const [isJose, setIsJose] = useState(true);
|
||||
const [text, setText] = useState("Hola, soy ");
|
||||
const [animationState, setAnimationState] = useState("writing");
|
||||
|
||||
useEffect(() => {
|
||||
let timeout;
|
||||
const baseText = "Hola, soy ";
|
||||
const fullText = baseText + (isJose ? names[0] : names[1]);
|
||||
|
||||
if (animationState === "writing") {
|
||||
if (text.length < fullText.length) {
|
||||
timeout = setTimeout(() => {
|
||||
setText(fullText.slice(0, text.length + 1));
|
||||
}, 150);
|
||||
} else {
|
||||
timeout = setTimeout(() => setAnimationState("deleting"), 3000);
|
||||
}
|
||||
} else if (animationState === "deleting") {
|
||||
if (text.length > baseText.length) {
|
||||
timeout = setTimeout(() => {
|
||||
setText(text.slice(0, -1));
|
||||
}, 100);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
setIsJose((prev) => !prev);
|
||||
setAnimationState("writing");
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="container d-flex flex-column align-items-center">
|
||||
<motion.h1
|
||||
className="header-title"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{text}
|
||||
</motion.h1>
|
||||
<motion.p id="subtitle"
|
||||
className="header-subtitle"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
>
|
||||
{isHovered ? subtitleWithCode : subtitle}
|
||||
</motion.p>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
21
src/components/Link.jsx
Normal file
21
src/components/Link.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import AbstractLink from './AbstractLink';
|
||||
|
||||
const Link = ({ to, children, isNavbar, className = '', ...props }) => {
|
||||
return (
|
||||
<li className="nav-item" style={{ listStyleType: 'none' }}>
|
||||
<AbstractLink to={to} className={`${isNavbar ? 'nav-link' : ''} ${className}`} {...props}>
|
||||
{children}
|
||||
</AbstractLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
isNavbar: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Link;
|
||||
45
src/components/NavBar.jsx
Normal file
45
src/components/NavBar.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
import '../css/NavBar.css';
|
||||
|
||||
const NavBar = ({ children }) => {
|
||||
const navVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg sticky-top navbar-dark shadow-sm">
|
||||
<div className="container">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarContent"
|
||||
aria-controls="navbarContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<motion.div
|
||||
className="collapse navbar-collapse"
|
||||
id="navbarContent"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={navVariants}
|
||||
>
|
||||
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{children}
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
NavBar.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
12
src/components/ThemeButton.jsx
Normal file
12
src/components/ThemeButton.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
60
src/contexts/ConfigContext.jsx
Normal file
60
src/contexts/ConfigContext.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
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);
|
||||
67
src/contexts/DataContext.jsx
Normal file
67
src/contexts/DataContext.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
55
src/contexts/ThemeContext.jsx
Normal file
55
src/contexts/ThemeContext.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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);
|
||||
}
|
||||
14
src/css/App.css
Normal file
14
src/css/App.css
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
61
src/css/Card.css
Normal file
61
src/css/Card.css
Normal file
@@ -0,0 +1,61 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 10px 20px var(--box-shadow);
|
||||
}
|
||||
|
||||
.card>h3 {
|
||||
font-size: 1.3em;
|
||||
color: var(--primary-color);
|
||||
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;
|
||||
}
|
||||
15
src/css/Footer.css
Normal file
15
src/css/Footer.css
Normal file
@@ -0,0 +1,15 @@
|
||||
footer > p > a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
footer > p > a:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
footer > div.d-flex > li.nav-item > a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
footer > div.d-flex > li.nav-item > a:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
20
src/css/Header.css
Normal file
20
src/css/Header.css
Normal file
@@ -0,0 +1,20 @@
|
||||
/* 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 */
|
||||
}
|
||||
|
||||
.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 */
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 1.25rem; /* lead */
|
||||
color: #6c757d; /* text-muted */
|
||||
}
|
||||
|
||||
13
src/css/NavBar.css
Normal file
13
src/css/NavBar.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.nav-link {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
background-color: var(--gradient-primary);
|
||||
}
|
||||
69
src/css/index.css
Normal file
69
src/css/index.css
Normal file
@@ -0,0 +1,69 @@
|
||||
:root {
|
||||
--primary-color: #ff3f1f;
|
||||
--secondary-color: #be3118;
|
||||
--text-shadow: #771e0e;
|
||||
--box-shadow: #a81900;
|
||||
|
||||
--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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
14
src/main.jsx
Normal file
14
src/main.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import App from './components/App.jsx';
|
||||
import './css/index.css';
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
);
|
||||
127
src/pages/Home/Home.jsx
Normal file
127
src/pages/Home/Home.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="min-vh-100" style={{ background: 'linear-gradient(to bottom right, #f8f9fa, #dee2e6)' }}>
|
||||
<main className="container-fluid">
|
||||
<AboutSection />
|
||||
<ProjectsSection />
|
||||
<InterestsSection />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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' }}>
|
||||
<motion.img
|
||||
src="https://avatars.githubusercontent.com/u/100301878?v=4"
|
||||
alt="Avatar"
|
||||
className="w-100 h-100 object-cover"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</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.
|
||||
</p>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
{['C/C++', 'Java', 'React', 'Vanilla JS', 'Python', 'MariaDB', 'Linux' ].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>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectsSection = () => {
|
||||
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: 'core', desc: 'Mega-backend que alimenta todos mis servicios' },
|
||||
{ title: 'mkernel', desc: 'Plugin que hace que mi servidor de Minecraft funcione' },
|
||||
{ title: 'amodgus', desc: 'Mod de Minecraft para introducir al Amongus (sus)' },
|
||||
].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 }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
49
src/pages/Projects/Projects.jsx
Normal file
49
src/pages/Projects/Projects.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ProjectsLanding() {
|
||||
const [repos, setRepos] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRepos = async () => {
|
||||
try {
|
||||
const userResponse = await fetch("https://api.github.com/users/Gallardo7761/repos");
|
||||
const userRepos = await userResponse.json();
|
||||
|
||||
setRepos(userRepos);
|
||||
} catch (error) {
|
||||
console.error("Error al obtener repos:", error);
|
||||
}
|
||||
};
|
||||
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">
|
||||
Aquí puedes ver algunos de los proyectos en los que he trabajado. 🚀
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
vite.config.js
Normal file
10
vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user