[REPO REFACTOR]: changed to a better git repository structure with branches

This commit is contained in:
2025-11-01 03:52:43 +01:00
parent 287a9f2b6a
commit 6952d5cfe1
37 changed files with 6216 additions and 0 deletions

38
eslint.config.js Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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.

View 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
View 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
View 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;

View 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;

View 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
View 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&apos;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
View 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(&quot;%s&quot;, &quot;{subtitle}&quot;);
</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
View 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
View 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;

View 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>
);
}

View 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);

View 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);

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 </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;

View 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
View 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'
}
})