Repository: Gothsec/Portfolio
Branch: main
Commit: 95f20bf31f37
Files: 23
Total size: 60.9 KB
Directory structure:
gitextract_3ahugtvb/
├── .github/
│ ├── FUNDING.yml
│ └── dependabot.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── astro.config.mjs
├── package.json
├── src/
│ ├── React/
│ │ ├── LetterGlitch.tsx
│ │ ├── LikeButton.tsx
│ │ └── SkillsList.tsx
│ ├── components/
│ │ ├── contact.astro
│ │ ├── footer.astro
│ │ ├── home.astro
│ │ ├── logoWall.astro
│ │ ├── nav.astro
│ │ └── projects.astro
│ ├── env.d.ts
│ ├── firebase.ts
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
├── tailwind.config.mjs
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: Gothsec
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
================================================
FILE: .gitignore
================================================
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
oscarandreshernandezpineda@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Oscar Hernandez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Portfolio


---
[Demo](https://oscarhernandez.vercel.app/)
[Astro Themes](https://astro.build/themes/details/dark-minimal/)
[ReactBits Showcase](https://www.reactbits.dev/showcase)
The component `<LetterGlitch \>` was taken from [ReactBits.dev](https://www.reactbits.dev/)
## **Stack**
### **Frontend**



### **Tools**



### **Show your favorite Spotify album (or your own)** 
1. Choose your Spotify album
2. Access the share options
3. Select 'copy embed code'
```
<iframe src="https://open.spotify.com/embed/album/YOUR_ALBUM_ID_HERE" style="border-radius:12px border:0;" class="w-full h-40" frameborder="0" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"></iframe>
```
4. Insert the embed code on footer.astro
That's it!
## **Project structure**
```
public/
└── svg/
src/
├── Components/
| ├── contact.astro
| ├── footer.astro
| ├── home.astro
| ├── logoWall.astro
| ├── nav.astro
| └── projects.astro
├── layouts/
| └── Layout.astro
├── React/
| ├── LetterGlitch.tsx
| ├── LikeButton.tsx
| └── SkillsList.tsx
└── pages/
└── index.astro
```
## **Local configuration**
1. Clone the repo:
```
git clone https://github.com/Gothsec/Astro-portfolio
```
2. Install dependencies:
```
npm install
```
3. Start the development server:
```
npm run dev
```
> **Important Notice:**
> This project is licensed under the [MIT License](https://opensource.org/licenses/mit).
> According to the license terms, any redistribution (including compiled or modified versions), you **must** retain the original copyright
> notice and the full license text. Copyright © 2026 Oscar Hernandez. All rights reserved.
================================================
FILE: astro.config.mjs
================================================
// @ts-check
import { defineConfig } from "astro/config";
import tailwind from "@astrojs/tailwind";
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind(), react()],
vite: {
resolve: {
alias: {
"@": "/src",
"@components": "/src/components",
},
},
},
output: "static",
build: {
inlineStylesheets: "auto",
},
server: {
host: true,
port: 4321,
},
});
================================================
FILE: package.json
================================================
{
"name": "portfolio",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/react": "^5.0.4",
"@astrojs/tailwind": "^6.0.2",
"@fontsource-variable/montserrat": "^5.2.8",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"astro": "^5.18.1",
"firebase": "^12.12.1",
"ogl": "^1.0.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sharp": "^0.34.5",
"typescript": "^5.9.3"
},
"devDependencies": {
"@types/node": "^25.6.0",
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1"
}
}
================================================
FILE: src/React/LetterGlitch.tsx
================================================
import { useRef, useEffect } from "react";
const LetterGlitch = ({
glitchColors = ["#5e4491", "#A476FF", "#241a38"],
glitchSpeed = 33,
centerVignette = false,
outerVignette = false,
smooth = true,
}: {
glitchColors: string[];
glitchSpeed: number;
centerVignette: boolean;
outerVignette: boolean;
smooth: boolean;
}) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationRef = useRef<number | null>(null);
const letters = useRef<
{
char: string;
color: string;
targetColor: string;
colorProgress: number;
}[]
>([]);
const grid = useRef({ columns: 0, rows: 0 });
const context = useRef<CanvasRenderingContext2D | null>(null);
const lastGlitchTime = useRef(Date.now());
const fontSize = 16;
const charWidth = 10;
const charHeight = 20;
const lettersAndSymbols = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"!",
"@",
"#",
"$",
"&",
"*",
"(",
")",
"-",
"_",
"+",
"=",
"/",
"[",
"]",
"{",
"}",
";",
":",
"<",
">",
",",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
];
const getRandomChar = () => {
return lettersAndSymbols[
Math.floor(Math.random() * lettersAndSymbols.length)
];
};
const getRandomColor = () => {
return glitchColors[Math.floor(Math.random() * glitchColors.length)];
};
const hexToRgb = (hex: string) => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
const interpolateColor = (
start: { r: number; g: number; b: number },
end: { r: number; g: number; b: number },
factor: number,
) => {
const result = {
r: Math.round(start.r + (end.r - start.r) * factor),
g: Math.round(start.g + (end.g - start.g) * factor),
b: Math.round(start.b + (end.b - start.b) * factor),
};
return `rgb(${result.r}, ${result.g}, ${result.b})`;
};
const calculateGrid = (width: number, height: number) => {
const columns = Math.ceil(width / charWidth);
const rows = Math.ceil(height / charHeight);
return { columns, rows };
};
const initializeLetters = (columns: number, rows: number) => {
grid.current = { columns, rows };
const totalLetters = columns * rows;
letters.current = Array.from({ length: totalLetters }, () => ({
char: getRandomChar(),
color: getRandomColor(),
targetColor: getRandomColor(),
colorProgress: 1,
}));
};
const resizeCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const rect = parent.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
if (context.current) {
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const { columns, rows } = calculateGrid(rect.width, rect.height);
initializeLetters(columns, rows);
drawLetters();
};
const drawLetters = () => {
if (!context.current || letters.current.length === 0) return;
const ctx = context.current;
const { width, height } = canvasRef.current!.getBoundingClientRect();
ctx.clearRect(0, 0, width, height);
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = "top";
letters.current.forEach((letter, index) => {
const x = (index % grid.current.columns) * charWidth;
const y = Math.floor(index / grid.current.columns) * charHeight;
ctx.fillStyle = letter.color;
ctx.fillText(letter.char, x, y);
});
};
const updateLetters = () => {
if (!letters.current || letters.current.length === 0) return; // Prevent accessing empty array
const updateCount = Math.max(1, Math.floor(letters.current.length * 0.05));
for (let i = 0; i < updateCount; i++) {
const index = Math.floor(Math.random() * letters.current.length);
if (!letters.current[index]) continue; // Skip if index is invalid
letters.current[index].char = getRandomChar();
letters.current[index].targetColor = getRandomColor();
if (!smooth) {
letters.current[index].color = letters.current[index].targetColor;
letters.current[index].colorProgress = 1;
} else {
letters.current[index].colorProgress = 0;
}
}
};
const handleSmoothTransitions = () => {
let needsRedraw = false;
letters.current.forEach((letter) => {
if (letter.colorProgress < 1) {
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
const startRgb = hexToRgb(letter.color);
const endRgb = hexToRgb(letter.targetColor);
if (startRgb && endRgb) {
letter.color = interpolateColor(
startRgb,
endRgb,
letter.colorProgress,
);
needsRedraw = true;
}
}
});
if (needsRedraw) {
drawLetters();
}
};
const animate = () => {
const now = Date.now();
if (now - lastGlitchTime.current >= glitchSpeed) {
updateLetters();
drawLetters();
lastGlitchTime.current = now;
}
if (smooth) {
handleSmoothTransitions();
}
animationRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
context.current = canvas.getContext("2d");
resizeCanvas();
animate();
let resizeTimeout: NodeJS.Timeout;
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
cancelAnimationFrame(animationRef.current as number);
resizeCanvas();
animate();
}, 100);
};
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(animationRef.current!);
window.removeEventListener("resize", handleResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [glitchSpeed, smooth]);
return (
<div className="relative w-full h-full bg-[#101010] overflow-hidden">
<canvas ref={canvasRef} className="block w-full h-full" />
{outerVignette && (
<div className="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(16,16,16,0)_60%,_rgba(16,16,16,1)_100%)]"></div>
)}
{centerVignette && (
<div className="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0.8)_0%,_rgba(0,0,0,0)_60%)]"></div>
)}
</div>
);
};
export default LetterGlitch;
================================================
FILE: src/React/LikeButton.tsx
================================================
import React, { useState, useEffect } from "react";
import { doc, onSnapshot, updateDoc, increment } from "firebase/firestore";
import { db } from "../firebase";
const LikeButton = () => {
const [likes, setLikes] = useState(0);
const [isLiked, setIsLiked] = useState(false);
const [isClient, setIsClient] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
setIsClient(true);
const storedIsLiked = localStorage.getItem("websiteIsLiked");
if (storedIsLiked) {
setIsLiked(storedIsLiked === "true");
}
// Listen for realtime updates from Firestore
const likeDocRef = doc(db, "likes", "counter");
const unsubscribe = onSnapshot(likeDocRef, (docSnap) => {
if (docSnap.exists()) {
const currentLikes = docSnap.data().likes;
// Only update if the server value is different (prevents overwrite during optimistic update)
setLikes((prev) => {
const newLikes = Math.max(0, currentLikes);
return newLikes;
});
}
});
return () => unsubscribe();
}, []);
const handleLike = async () => {
if (isProcessing || isLiked) return;
// Optimistic Update
const previousLikes = likes;
setLikes((prev) => prev + 1);
setIsLiked(true);
setIsAnimating(true);
localStorage.setItem("websiteIsLiked", "true");
// Reset animation after it finishes
setTimeout(() => setIsAnimating(false), 600);
try {
setIsProcessing(true);
const likeDocRef = doc(db, "likes", "counter");
await updateDoc(likeDocRef, {
likes: increment(1),
});
} catch (error) {
console.error("Error updating likes:", error);
// Rollback on error
setLikes(previousLikes);
setIsLiked(false);
localStorage.removeItem("websiteIsLiked");
} finally {
setIsProcessing(false);
}
};
if (!isClient) return null;
const borderColorClass = isLiked
? "border-[var(--sec)]"
: "border-[var(--white-icon)]";
return (
<div className="flex items-center">
<button
onClick={handleLike}
disabled={isProcessing || isLiked}
className={`
group relative w-40 h-10 flex items-center justify-center p-3
rounded-full transition-all duration-300 ease-in-out transform border-2 ${borderColorClass}
${!isLiked ? "hover:scale-105 hover:border-[var(--white)]" : "cursor-default"}
${isAnimating ? "animate-heart-pulse" : ""}
`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={`w-6 h-6 transition-all duration-300 ease-in-out
${isLiked ? "text-[var(--sec)] scale-110" : "text-[var(--white-icon)] group-hover:text-[var(--white)] group-hover:scale-105"}
`}
>
<path d="M16.5 3C19.5376 3 22 5.5 22 9C22 16 14.5 20 12 21.5C9.5 20 2 16 2 9C2 5.5 4.5 3 7.5 3C9.35997 3 11 4 12 5C13 4 14.64 3 16.5 3ZM12.9339 18.6038C13.8155 18.0485 14.61 17.4955 15.3549 16.9029C18.3337 14.533 20 11.9435 20 9C20 6.64076 18.463 5 16.5 5C15.4241 5 14.2593 5.56911 13.4142 6.41421L12 7.82843L10.5858 6.41421C9.74068 5.56911 8.5759 5 7.5 5C5.55906 5 4 6.6565 4 9C4 11.9435 5.66627 14.533 8.64514 16.9029C9.39 17.4955 10.1845 18.0485 11.0661 18.6038C11.3646 18.7919 11.6611 18.9729 12 19.1752C12.3389 18.9729 12.6354 18.7919 12.9339 18.6038Z"></path>
</svg>
<span className="text-sm pl-3 font-medium text-[var(--white)]">
{likes} Likes
</span>
</button>
</div>
);
};
export default LikeButton;
================================================
FILE: src/React/SkillsList.tsx
================================================
import React, { useState } from "react";
const CategoryIcons = {
"Web Development": (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6 text-[var(--sec)] opacity-70"
>
<path d="M21 3C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM20 11H4V19H20V11ZM20 5H4V9H20V5ZM11 6V8H9V6H11ZM7 6V8H5V6H7Z"></path>
</svg>
),
"Mobile Development": (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6 text-[var(--sec)] opacity-70"
>
<path d="M7 4V20H17V4H7ZM6 2H18C18.5523 2 19 2.44772 19 3V21C19 21.5523 18.5523 22 18 22H6C5.44772 22 5 21.5523 5 21V3C5 2.44772 5.44772 2 6 2ZM12 17C12.5523 17 13 17.4477 13 18C13 18.5523 12.5523 19 12 19C11.4477 19 11 18.5523 11 18C11 17.4477 11.4477 17 12 17Z"></path>
</svg>
),
"UI/UX Design & Prototyping": (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6 text-[var(--sec)] opacity-70"
>
<path d="M5.7646 7.99998L5.46944 7.26944C5.26255 6.75737 5.50995 6.17454 6.02202 5.96765L15.2939 2.22158C15.8059 2.01469 16.3888 2.26209 16.5956 2.77416L22.2147 16.6819C22.4216 17.194 22.1742 17.7768 21.6622 17.9837L12.3903 21.7298C11.8783 21.9367 11.2954 21.6893 11.0885 21.1772L11.0002 20.9586V21H7.00021C6.44792 21 6.00021 20.5523 6.00021 20V19.7303L2.65056 18.377C2.13849 18.1701 1.89109 17.5873 2.09798 17.0752L5.7646 7.99998ZM8.00021 19H10.2089L8.00021 13.5333V19ZM6.00021 12.7558L4.32696 16.8972L6.00021 17.6084V12.7558ZM7.69842 7.44741L12.5683 19.5008L19.9858 16.5039L15.1159 4.45055L7.69842 7.44741ZM10.6766 9.47974C10.1645 9.68663 9.5817 9.43924 9.37481 8.92717C9.16792 8.4151 9.41532 7.83227 9.92739 7.62538C10.4395 7.41849 11.0223 7.66588 11.2292 8.17795C11.4361 8.69002 11.1887 9.27286 10.6766 9.47974Z"></path>
</svg>
),
};
const SkillsList = () => {
const [openItem, setOpenItem] = useState<string | null>(null);
const skills = {
"Web Development": [
"Single Page Applications (SPAs)",
"Landing pages and business websites",
"Portfolio websites",
],
"Mobile Development": [
"Mobile-friendly web apps",
"React Native mobile apps",
],
"UI/UX Design & Prototyping": [
"UI design with Figma & Canva",
"UX research & improvements",
"Prototyping for websites & mobile apps",
],
};
const toggleItem = (item: string) => {
setOpenItem(openItem === item ? null : item);
};
return (
<div className="text-left pt-3 md:pt-9">
<h3 className="text-[var(--white)] text-3xl md:text-4xl font-semibold md:mb-6">
What I do?
</h3>
<ul className="space-y-4 mt-4 text-lg">
{Object.entries(skills).map(([category, items]) => (
<li key={category} className="w-full">
<div
onClick={() => toggleItem(category)}
className="md:w-[400px] w-full bg-[#1414149c] rounded-2xl text-left hover:bg-opacity-80 transition-all border border-[var(--white-icon-tr)] cursor-pointer overflow-hidden"
>
<div className="flex items-center gap-3 p-4">
{CategoryIcons[category]}
<div className="flex items-center gap-2 flex-grow justify-between">
<div className="min-w-0 max-w-[200px] md:max-w-none overflow-hidden">
<span className="block truncate text-[var(--white)] text-lg">
{category}
</span>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={`w-6 h-6 text-[var(--white)] transform transition-transform flex-shrink-0 ${
openItem === category ? "rotate-180" : ""
}`}
>
<path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path>
</svg>
</div>
</div>
<div
className={`transition-all duration-300 px-4 ${
openItem === category
? "max-h-[500px] pb-4 opacity-100"
: "max-h-0 opacity-0"
}`}
>
<ul className="space-y-2 text-[var(--white-icon)] text-sm">
{items.map((item, index) => (
<div key={index} className="flex items-center">
<span className="pl-1">•</span>
<li className="pl-3">{item}</li>
</div>
))}
</ul>
</div>
</div>
</li>
))}
</ul>
</div>
);
};
export default SkillsList;
================================================
FILE: src/components/contact.astro
================================================
<section id="contact" class="w-full py-12 border-t border-[#ffffff10]">
<div class="max-w-5xl mx-auto">
<h2 class="text-lg text-[var(--sec)] mb-2 shiny-sec">Let's talk</h2>
<h3 class="text-4xl md:text-5xl font-medium text-[var(--white)] mb-6">
Contact
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="text-[var(--white-icon)]">
<p class="mb-4">
Have a question or a project in mind? Feel free to reach out.
</p>
<div class="flex items-center gap-2">
<span>Location:</span>
<span class="text-[var(--white)]">Colombia, Valle del cauca</span>
</div>
</div>
<div>
<form
id="contact-form"
action="https://formspree.io/f/mnnjznkj"
method="POST"
class="flex flex-col gap-4"
>
<input
type="text"
name="from_name"
placeholder="Name"
required
class="px-4 py-2 bg-[#1414149c] text-[var(--white)] border border-[var(--white-icon-tr)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--sec)]"
/>
<input
type="email"
name="reply_to"
placeholder="Email"
required
class="px-4 py-2 bg-[#1414149c] text-[var(--white)] border border-[var(--white-icon-tr)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--sec)]"
/>
<textarea
name="message"
placeholder="Message"
rows="6"
required
class="px-4 py-2 bg-[#1414149c] text-[var(--white)] border border-[var(--white-icon-tr)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--sec)] resize-none"
></textarea>
<button
type="submit"
class="px-4 py-2 bg-[var(--white-icon-tr)] text-[var(--white)] rounded-lg opacity-60 transition-opacity border border-[var(--white-icon-tr)] hover:opacity-100 hover:bg-[var(--white-icon-tr)]"
>
Submit
</button>
</form>
<div
id="form-message"
class="hidden justify-center items-center mt-4 text-[var(--white)] text-lg"
>
✅ Thank you for your message!
</div>
</div>
</div>
</div>
</section>
<script type="module" is:inline>
const form = document.getElementById("contact-form");
const formMessage = document.getElementById("form-message");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
try {
const response = await fetch(form.action, {
method: "POST",
body: formData,
headers: { Accept: "application/json" },
});
if (response.ok) {
form.reset();
form.style.display = "none";
formMessage.classList.remove("hidden");
} else {
const data = await response.json();
console.error("Error response:", data);
alert("There was a problem sending your message.");
}
} catch (error) {
console.error("Error:", error);
alert("There was a problem sending your message.");
}
});
</script>
================================================
FILE: src/components/footer.astro
================================================
---
import LikeButton from "../React/LikeButton.tsx";
const currentYear = new Date().getFullYear();
---
<footer class="w-full py-12 border-t border-[#ffffff10]">
<div class="max-w-5xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-10">
<div class="flex flex-col lg:items-start items-center space-y-6 gap-9">
<div class="flex space-x-6 sm:space-x-8">
{
[
{
href: "https://github.com/gothsec",
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-8"><path d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"></path></svg>',
label: "GitHub",
},
{
href: "https://linkedin.com/in/hernandezoscar-dev",
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-8"><path d="M18.3362 18.339H15.6707V14.1622C15.6707 13.1662 15.6505 11.8845 14.2817 11.8845C12.892 11.8845 12.6797 12.9683 12.6797 14.0887V18.339H10.0142V9.75H12.5747V10.9207H12.6092C12.967 10.2457 13.837 9.53325 15.1367 9.53325C17.8375 9.53325 18.337 11.3108 18.337 13.6245V18.339H18.3362ZM7.00373 8.57475C6.14573 8.57475 5.45648 7.88025 5.45648 7.026C5.45648 6.1725 6.14648 5.47875 7.00373 5.47875C7.85873 5.47875 8.55173 6.1725 8.55173 7.026C8.55173 7.88025 7.85798 8.57475 7.00373 8.57475ZM8.34023 18.339H5.66723V9.75H8.34023V18.339ZM19.6697 3H4.32923C3.59498 3 3.00098 3.5805 3.00098 4.29675V19.7033C3.00098 20.4202 3.59498 21 4.32923 21H19.6675C20.401 21 21.001 20.4202 21.001 19.7033V4.29675C21.001 3.5805 20.401 3 19.6675 3H19.6697Z"></path></svg>',
label: "LinkedIn",
},
{
href: "https://mail.google.com/mail/?view=cm&fs=1&to=oscarandreshernandezpineda@gmail.com&su=Hey%20Oscar!",
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2.1em" height="2.1em" viewBox="0 0 24 24"><path fill="currentColor" d="m18.73 5.41l-1.28 1L12 10.46L6.55 6.37l-1.28-1A2 2 0 0 0 2 7.05v11.59A1.36 1.36 0 0 0 3.36 20h3.19v-7.72L12 16.37l5.45-4.09V20h3.19A1.36 1.36 0 0 0 22 18.64V7.05a2 2 0 0 0-3.27-1.64"/></svg>',
label: "Email",
},
].map((link) => (
<a
href={link.href}
target="_blank"
class="flex flex-col items-center group"
aria-label={link.label}
>
<div class="text-[var(--white-icon)] hover:text-[var(--white)] transition duration-300 ease-in-out">
<div set:html={link.icon} />
</div>
</a>
))
}
</div>
<LikeButton client:load />
</div>
<div class="flex flex-col items-center space-y-6">
<div class="grid grid-cols-1 gap-3 w-full max-w-xs py-6">
{
[
{
desc: "Built with",
name: "Astro",
icon: "/svg/astro.svg",
alt: "Astro Logo",
},
{
desc: "Styled with",
name: "TailwindCSS",
icon: "/svg/tailwindcss.svg",
alt: "TailwindCSS Logo",
},
{
desc: "Deployed on",
name: "Vercel",
icon: "/svg/vercel.svg",
alt: "Vercel Logo",
},
].map((tech) => (
<div class="flex items-center justify-center lg:justify-normal space-x-3">
<span class="text-[var(--white-icon)] text-sm">
{tech.desc}
</span>
<img
src={tech.icon}
alt={tech.alt}
class="h-5 w-5 object-contain filter brightness-0 invert opacity-50"
loading="lazy"
/>
<span class="text-[var(--white-icon)] text-sm">
{tech.name}
</span>
</div>
))
}
</div>
</div>
<div class="flex flex-col items-center lg:items-start space-y-6">
<div class="w-full max-w-xs">
<iframe
style="border-radius:12px; border:0;"
src="https://open.spotify.com/embed/playlist/2irOd49FRRLFWYccolQeea?utm_source=generator&theme=0&locale=en_US"
class="w-full h-40"
allowfullscreen=""
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"></iframe>
</div>
</div>
</div>
<div class="mt-12 pt-8 border-t border-[#ffffff10]">
<p class="text-center text-sm text-[var(--white-icon)] space-y-2">
<!-- If you are using this template, by MIT License you can't remove the copyright notice -->
<span class="block sm:inline"
>Copyright © {currentYear} <a href="https://github.com/Gothsec"
>Oscar Hernandez</a
>. All rights reserved.</span
>
</p>
</div>
</div>
</footer>
================================================
FILE: src/components/home.astro
================================================
---
import LetterGlitch from "../React/LetterGlitch.tsx";
import LogoWall from "../components/logoWall.astro";
import SkillsList from "../React/SkillsList.tsx";
---
<section class="text-[var(--white)] mt-12 md:mt-0" id="home">
<div class="max-w-5xl mx-auto space-y-8 md:py-36 pb-14">
<div class="text-left space-y-4">
<p class="text-md md:text-lg text-[var(--white-icon)] shiny-white">
Hi, I'm Oscar Hernandez
</p>
<div
class="flex flex-col lg:flex-row lg:items-center space-y-4 lg:space-y-0 lg:space-x-8 md:gap-4"
>
<h1
class="text-[var(--white)] text-5xl md:text-6xl font-medium text-pretty leading-none"
>
Software <br /> Developer
</h1>
<p class="text-md md:text-2xl text-[var(--white-icon)]">
Transforming ideas into interactive and seamless digital experiences
with cutting-edge <span class="text-[var(--sec)] shiny-sec"
>frontend</span
> development.
</p>
</div>
<div class="flex justify-start gap-2 pt-3 md:pt-6">
<a
target="_blank"
href="https://github.com/gothsec"
aria-label="GitHub"
class="text-[var(--white-icon)] hover:text-white transition duration-300 ease-in-out border border-1 border-[var(--white-icon-tr)] p-3 rounded-xl bg-[#1414149c] hover:bg-[var(--white-icon-tr)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-8"
>
<path
d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"
></path>
</svg>
</a>
<a
target="_blank"
href="https://linkedin.com/in/andresshernandez-eng"
aria-label="LinkedIn"
class="text-[var(--white-icon)] hover:text-white transition duration-300 ease-in-out border border-1 border-[var(--white-icon-tr)] p-3 rounded-xl bg-[#1414149c] hover:bg-[var(--white-icon-tr)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-8"
>
<path
d="M18.3362 18.339H15.6707V14.1622C15.6707 13.1662 15.6505 11.8845 14.2817 11.8845C12.892 11.8845 12.6797 12.9683 12.6797 14.0887V18.339H10.0142V9.75H12.5747V10.9207H12.6092C12.967 10.2457 13.837 9.53325 15.1367 9.53325C17.8375 9.53325 18.337 11.3108 18.337 13.6245V18.339H18.3362ZM7.00373 8.57475C6.14573 8.57475 5.45648 7.88025 5.45648 7.026C5.45648 6.1725 6.14648 5.47875 7.00373 5.47875C7.85873 5.47875 8.55173 6.1725 8.55173 7.026C8.55173 7.88025 7.85798 8.57475 7.00373 8.57475ZM8.34023 18.339H5.66723V9.75H8.34023V18.339ZM19.6697 3H4.32923C3.59498 3 3.00098 3.5805 3.00098 4.29675V19.7033C3.00098 20.4202 3.59498 21 4.32923 21H19.6675C20.401 21 21.001 20.4202 21.001 19.7033V4.29675C21.001 3.5805 20.401 3 19.6675 3H19.6697Z"
></path>
</svg>
</a>
<a
target="_blank"
href="https://mail.google.com/mail/?view=cm&fs=1&to=oscarandreshernandezpineda@gmail.com&su=Hey%20Oscar!"
aria-label="Email"
class="text-[var(--white-icon)] hover:text-white transition duration-300 ease-in-out border border-1 border-[var(--white-icon-tr)] p-3 rounded-xl bg-[#1414149c] hover:bg-[var(--white-icon-tr)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="2.1em"
height="2.1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m18.73 5.41l-1.28 1L12 10.46L6.55 6.37l-1.28-1A2 2 0 0 0 2 7.05v11.59A1.36 1.36 0 0 0 3.36 20h3.19v-7.72L12 16.37l5.45-4.09V20h3.19A1.36 1.36 0 0 0 22 18.64V7.05a2 2 0 0 0-3.27-1.64"
></path>
</svg>
</a>
</div>
</div>
<LogoWall />
<div class="flex flex-col lg:flex-row items-center gap-8">
<SkillsList client:load />
<div
class="flex justify-center md:w-full md:h-[292px] size-[290px] pt-3 md:pt-9 md:ml-16"
>
<LetterGlitch
client:load
glitchColors={["#5e4491", "#A476FF", "#241a38"]}
glitchSpeed={33}
centerVignette={false}
outerVignette={true}
smooth={true}
/>
</div>
</div>
</div>
</section>
<style is:global>
.shiny-sec {
background: linear-gradient(135deg, #a476ff 25%, #eee5ff 50%, #a476ff 75%);
background-size: 400% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: shine 3s linear infinite;
}
@keyframes shine {
0% {
background-position: 100% 50%;
}
30%,
70% {
background-position: 0% 50%;
}
}
</style>
================================================
FILE: src/components/logoWall.astro
================================================
---
const technologies = [
"astro",
"vue",
"react",
"typeScript",
"tailwindcss",
"next",
"nodejs",
"HTML5",
"CSS3",
"javaScript",
"git",
"supabase",
"mysql",
"bash",
];
---
<div class="relative overflow-x-hidden py-8">
<div class="pointer-events-none absolute inset-y-0 left-0 w-32 bg-gradient-to-r from-[var(--background)] to-transparent z-20"></div>
<div class="pointer-events-none absolute inset-y-0 right-0 w-32 bg-gradient-to-l from-[var(--background)] to-transparent z-20"></div>
<div class="flex animate-scroll w-max will-change-transform">
{
[...technologies, ...technologies].map((tech, index) => (
<div
class="flex items-center gap-2 pr-12 md:pr-20 group transition-all duration-300"
aria-hidden={index >= technologies.length ? "true" : "false"}
>
<img
src={`/svg/${tech}.svg`}
alt={tech}
class="h-7 w-auto object-contain transition-transform group-hover:scale-110 opacity-60"
width="30"
height="30"
loading={index < technologies.length ? "eager" : "lazy"}
decoding="async"
/>
<span class="text-lg font-medium text-[var(--white-icon)] whitespace-nowrap">
{tech.charAt(0).toUpperCase() + tech.slice(1)}
</span>
</div>
))
}
</div>
</div>
<style is:global>
@keyframes scroll {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-50%, 0, 0);
}
}
.animate-scroll {
animation: scroll 60s linear infinite;
}
@media (min-width: 768px) {
.animate-scroll {
animation-duration: 50s;
}
}
</style>
================================================
FILE: src/components/nav.astro
================================================
---
interface NavItem {
label: string;
href: string;
icon: string;
}
const navItems: NavItem[] = [
{
label: "Home",
href: "#home",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M21 20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V9.48907C3 9.18048 3.14247 8.88917 3.38606 8.69972L11.3861 2.47749C11.7472 2.19663 12.2528 2.19663 12.6139 2.47749L20.6139 8.69972C20.8575 8.88917 21 9.18048 21 9.48907V20ZM19 19V9.97815L12 4.53371L5 9.97815V19H19Z"></path></svg>`,
},
{
label: "Projects",
href: "#projects",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z"></path></svg>`,
},
{
label: "Contact",
href: "#contact",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M21.7267 2.95694L16.2734 22.0432C16.1225 22.5716 15.7979 22.5956 15.5563 22.1126L11 13L1.9229 9.36919C1.41322 9.16532 1.41953 8.86022 1.95695 8.68108L21.0432 2.31901C21.5716 2.14285 21.8747 2.43866 21.7267 2.95694ZM19.0353 5.09647L6.81221 9.17085L12.4488 11.4255L15.4895 17.5068L19.0353 5.09647Z"></path></svg>`,
},
];
---
<div class="flex justify-center w-full">
<nav
id="main-nav"
class="fixed left-1/2 -translate-x-1/2 z-[100] bg-[var(--background)] border border-1 border-transparent backdrop-blur-xl transition-all duration-500 ease-in-out md:top-6 md:bottom-auto bottom-0 w-[80%]"
>
<div class="container mx-auto flex justify-center items-center p-3">
<ul
class="flex w-full justify-between md:space-x-6 md:justify-center md:gap-12 gap-6"
>
{
navItems.map((item) => (
<li class="flex-1 md:flex-none">
<a
href={item.href}
class="flex flex-col items-center gap-1 text-[var(--white-icon)] transition-colors text-xs md:text-base relative group"
>
<div class="absolute -left-6 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full transition-all duration-300 scale-0 opacity-0 bg-[#A9FF5B] nav-indicator hidden md:block" />
<span class="md:hidden flex items-center justify-center w-6 h-6">
<fragment set:html={item.icon} />
</span>
<span class="hidden md:inline-block">{item.label}</span>
<span class="md:hidden">{item.label}</span>
</a>
</li>
))
}
</ul>
</div>
</nav>
</div>
<style>
nav {
transform: translateX(-50%);
background-color: var(--background);
transition:
background-color 0.3s ease,
border-radius 0.3s ease,
border-color 0.3s ease;
}
nav.scrolling {
background-color: var(--component-bg);
border-color: #ffffff10;
border-radius: 9999px;
}
nav a.active .nav-indicator {
transform: translateY(-50%) scale(1);
opacity: 1;
}
nav a.active {
color: white !important;
}
@media (max-width: 767px) {
nav {
width: 100% !important;
transform: translateX(-50%);
bottom: 0;
left: 50%;
position: fixed;
border-radius: 1rem 1rem 0 0;
border-color: #ffffff10;
}
nav.scrolling {
border-radius: 1rem 1rem 0 0;
background-color: var(--component-bg);
}
body {
padding-bottom: 70px;
}
}
</style>
<script>
const nav = document.getElementById("main-nav");
const maxScroll = 1000;
let rafId: number | null = null;
function updateNav() {
if (window.scrollY > 0) {
nav?.classList.add("scrolling");
const scrollProgress = Math.min(window.scrollY / maxScroll, 1);
const easeProgress = 1 - Math.pow(1 - scrollProgress, 4);
const minWidth = 528;
const maxWidth = window.innerWidth * 0.8;
const currentWidth = maxWidth - (maxWidth - minWidth) * easeProgress;
if (window.innerWidth >= 768) {
nav?.style.setProperty("width", `${currentWidth}px`);
}
} else {
nav?.classList.remove("scrolling");
nav?.style.setProperty("width", "80%");
}
rafId = null;
}
window.addEventListener(
"scroll",
() => {
if (!rafId) {
rafId = requestAnimationFrame(updateNav);
}
},
{ passive: true }
);
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
const target = e.currentTarget as HTMLAnchorElement;
const targetId = target.getAttribute("href")?.substring(1) || "";
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: "smooth",
});
}
});
});
document.addEventListener("DOMContentLoaded", () => {
const sections = document.querySelectorAll("section[id]");
const navLinks = document.querySelectorAll("nav a[href^='#']");
const observerOptions = { threshold: 0.6 };
const observerCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
navLinks.forEach((link) => link.classList.remove("active"));
const id = entry.target.getAttribute("id");
const activeLink = document.querySelector(`nav a[href="#${id}"]`);
if (activeLink) {
activeLink.classList.add("active");
}
}
});
};
const observer = new IntersectionObserver(
observerCallback,
observerOptions
);
sections.forEach((section) => observer.observe(section));
});
</script>
<style>
@media (max-width: 767px) {
body {
padding-bottom: 70px;
}
}
nav a.active {
color: white !important;
}
</style>
================================================
FILE: src/components/projects.astro
================================================
---
import { Image } from "astro:assets";
import svgl from "../../public/svgl.png";
import stockin from "../../public/stockin.png";
import moviesfordevs from "../../public/moviesfordevs.png";
import velez from "../../public/velez.png";
interface Project {
title: string;
image: ImageMetadata;
link: string;
preview: string;
status: string;
}
const projects: Project[] = [
{
title: "MoviesForDevs",
image: moviesfordevs as ImageMetadata,
link: "https://github.com/gothsec/MoviesForDevs",
preview: "https://movies-for-devs.vercel.app",
status: "Deployed",
},
{
title: "StockIn",
image: stockin as ImageMetadata,
link: "https://github.com/gothsec/stockin-demo",
preview: "https://stockin-demo.vercel.app",
status: "On Development",
},
{
title: "Svgl.app",
image: svgl as ImageMetadata,
link: "https://github.com/pheralb/svgl",
preview: "https://svgl.app",
status: "Contributor",
},
{
title: "Rifas Velez Web",
image: velez as ImageMetadata,
link: "https://github.com/Buga-Software/rifasvelez-web",
preview: "https://www.rifasvelez.com",
status: "Deployed",
},
];
---
<section
id="projects"
class="py-12 border-t border-[#ffffff10] text-[var(--white)]"
>
<div class="max-w-5xl mx-auto">
<h2 class="text-lg text-[var(--sec)] mb-2 shiny-sec">My work</h2>
<h3 class="text-4xl md:text-5xl font-medium mb-8">Projects</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
{
projects.map((project) => (
<div class="group">
<a
href={project.preview}
target="_blank"
rel="noopener noreferrer"
class="block"
>
<div class="rounded-2xl overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4">
<Image
src={project.image}
alt={project.title}
class="w-full h-48 md:h-72 object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="flex items-center px-3">
<div class="flex-grow">
<h4 class="text-2xl font-semibold">{project.title}</h4>
<span class="py-1 text-sm text-[var(--white-icon)]">
{project.status}
</span>
</div>
<div class="flex gap-2 ml-auto">
<a
target="_blank"
href={project.link}
aria-label="GitHub"
class="size-14 flex justify-center items-center text-[var(--white-icon)] hover:text-white transition duration-300 ease-in-out border border-1 border-[var(--white-icon-tr)] p-3 rounded-xl bg-[#1414149c] hover:bg-[var(--white-icon-tr)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-7"
>
<path d="M24 12L18.3431 17.6569L16.9289 16.2426L21.1716 12L16.9289 7.75736L18.3431 6.34315L24 12ZM2.82843 12L7.07107 16.2426L5.65685 17.6569L0 12L5.65685 6.34315L7.07107 7.75736L2.82843 12ZM9.78845 21H7.66009L14.2116 3H16.3399L9.78845 21Z" />
</svg>
</a>
<a
target="_blank"
href={project.preview}
aria-label="Preview"
class="size-14 flex justify-center items-center text-[var(--white-icon)] hover:text-white transition duration-300 ease-in-out border border-1 border-[var(--white-icon-tr)] p-3 rounded-xl bg-[#1414149c] hover:bg-[var(--white-icon-tr)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-7"
>
<path d="M16.0037 9.41421L7.39712 18.0208L5.98291 16.6066L14.5895 8H7.00373V6H18.0037V17H16.0037V9.41421Z" />
</svg>
</a>
</div>
</div>
</a>
</div>
))
}
</div>
<a
target="_blank"
href="https://github.com/Gothsec?tab=repositories"
aria-label="GitHub"
class="w-full flex items-center justify-center gap-2 mt-9 text-[var(--white-icon)] hover:text-white transition duration-300 ease-in-out border border-[var(--white-icon-tr)] p-3 rounded-full bg-[#1414149c] hover:bg-[var(--white-icon-tr)] hover:scale-105"
>
<span class="md:text-lg text-md">More projects on</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"
></path>
</svg>
</a>
</div>
</section>
================================================
FILE: src/env.d.ts
================================================
/// <reference path="../.astro/types.d.ts" />
================================================
FILE: src/firebase.ts
================================================
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: import.meta.env.FIREBASE_API_KEY,
authDomain: import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.PUBLIC_FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
================================================
FILE: src/layouts/Layout.astro
================================================
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{title} | Software Developer</title>
<meta
name="description"
content="Oscar Andres Hernandez Pineda - Software Developer building interactive and seamless digital experiences with cutting-edge frontend development."
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="canonical" href="https://oscarhernandez.vercel.app/" />
<link
rel="preload"
href="/fonts/montserrat-latin-wght-normal.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<meta
property="og:title"
content="Oscar Andres Hernandez Pineda | Software Developer"
/>
<meta
property="og:description"
content="Portfolio of Oscar Andres Hernandez Pineda, Software Developer specializing in frontend and interactive experiences."
/>
<meta
property="og:image"
content="https://oscarhernandez.vercel.app/og.image.png"
/>
<meta property="og:url" content="https://oscarhernandez.vercel.app" />
<meta property="og:type" content="website" />
<meta
property="og:site_name"
content="Oscar Andres Hernandez Pineda Portfolio"
/>
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Oscar Andres Hernandez Pineda",
"alternateName": "Oscar Hernandez",
"url": "https://oscarhernandez.vercel.app",
"image": "https://oscarhernandez.vercel.app/og.image.png",
"sameAs": [
"https://github.com/gothsec",
"https://linkedin.com/in/andresshernandez-eng"
],
"jobTitle": "Software Developer",
"description": "Software Developer building interactive and seamless digital experiences."
}
</script>
</head>
<body class="bg-[--background] sm:px-28 lg:px-20 px-9">
<slot />
</body>
</html>
<style is:global>
@font-face {
font-family: "Montserrat Variable";
src: url("/fonts/montserrat-latin-wght-normal.woff2")
format("woff2-variations");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
:root {
--background: #101010;
--sec: #a476ff;
--white: #dfdfdf;
--white-icon: #f3f3f398;
--white-icon-tr: #f3f3f310;
}
* {
font-family:
"Montserrat Variable",
-apple-system,
system-ui,
sans-serif;
box-sizing: border-box;
padding: 0;
margin: 0;
}
*::selection {
background-color: var(--sec);
color: var(--background);
}
/* Scrollbar styles */
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
background: var(--container);
border-radius: 30px;
}
::-webkit-scrollbar-thumb {
background: var(--background);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--pink);
}
/* Scrollbar styles for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--line) var(--container);
}
</style>
================================================
FILE: src/pages/index.astro
================================================
---
import Layout from "@/layouts/Layout.astro";
import Nav from "@/components/nav.astro";
import Home from "@/components/home.astro";
import Projects from "@/components/projects.astro";
import Contact from "@/components/contact.astro";
import Footer from "@/components/footer.astro";
---
<Layout title="Oscar Andres Hernandez Pineda">
<Nav />
<Home />
<Projects />
<Contact />
<Footer />
</Layout>
================================================
FILE: tailwind.config.mjs
================================================
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
keyframes: {
scaleAnim: {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.1)' },
'100%': { transform: 'scale(1)' },
},
'heart-pulse': {
'0%': { transform: 'scale(1)' },
'25%': { transform: 'scale(1.1)' },
'50%': { transform: 'scale(1)' },
'75%': { transform: 'scale(1.06)' },
'100%': { transform: 'scale(1)' },
},
},
animation: {
scale: 'scaleAnim 300ms ease-in-out',
'heart-pulse': 'heart-pulse 0.6s ease-in-out',
},
},
},
plugins: [],
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "ES2020",
"allowImportingTsExtensions": true,
"noEmit": true,
"types": [
"astro/client"
],
"moduleResolution": "node",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
"@components/*": [
"src/components/*"
]
},
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"include": [
"src/**/*",
"astro.config.ts"
]
}
gitextract_3ahugtvb/ ├── .github/ │ ├── FUNDING.yml │ └── dependabot.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── astro.config.mjs ├── package.json ├── src/ │ ├── React/ │ │ ├── LetterGlitch.tsx │ │ ├── LikeButton.tsx │ │ └── SkillsList.tsx │ ├── components/ │ │ ├── contact.astro │ │ ├── footer.astro │ │ ├── home.astro │ │ ├── logoWall.astro │ │ ├── nav.astro │ │ └── projects.astro │ ├── env.d.ts │ ├── firebase.ts │ ├── layouts/ │ │ └── Layout.astro │ └── pages/ │ └── index.astro ├── tailwind.config.mjs └── tsconfig.json
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (67K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 851,
"preview": "# These are supported funding model platforms\n\ngithub: Gothsec\npatreon: # Replace with a single Patreon username\nopen_co"
},
{
"path": ".github/dependabot.yml",
"chars": 524,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".gitignore",
"chars": 264,
"preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyar"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5238,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "LICENSE",
"chars": 1073,
"preview": "MIT License\n\nCopyright (c) 2025 Oscar Hernandez \n\nPermission is hereby granted, free of charge, to any person obtaining "
},
{
"path": "README.md",
"chars": 2486,
"preview": "# Portfolio\n"
},
{
"path": "astro.config.mjs",
"chars": 481,
"preview": "// @ts-check\nimport { defineConfig } from \"astro/config\";\nimport tailwind from \"@astrojs/tailwind\";\n\nimport react from \""
},
{
"path": "package.json",
"chars": 774,
"preview": "{\n \"name\": \"portfolio\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"scripts\": {\n \"dev\": \"astro dev\",\n \"start\": \""
},
{
"path": "src/React/LetterGlitch.tsx",
"chars": 7298,
"preview": "import { useRef, useEffect } from \"react\";\n\nconst LetterGlitch = ({\n glitchColors = [\"#5e4491\", \"#A476FF\", \"#241a38\"],\n"
},
{
"path": "src/React/LikeButton.tsx",
"chars": 3699,
"preview": "import React, { useState, useEffect } from \"react\";\nimport { doc, onSnapshot, updateDoc, increment } from \"firebase/fire"
},
{
"path": "src/React/SkillsList.tsx",
"chars": 5023,
"preview": "import React, { useState } from \"react\";\n\nconst CategoryIcons = {\n \"Web Development\": (\n <svg\n xmlns=\"http://ww"
},
{
"path": "src/components/contact.astro",
"chars": 3221,
"preview": "<section id=\"contact\" class=\"w-full py-12 border-t border-[#ffffff10]\">\n <div class=\"max-w-5xl mx-auto\">\n <h2 class="
},
{
"path": "src/components/footer.astro",
"chars": 6189,
"preview": "---\nimport LikeButton from \"../React/LikeButton.tsx\";\n\nconst currentYear = new Date().getFullYear();\n---\n\n<footer class="
},
{
"path": "src/components/home.astro",
"chars": 5889,
"preview": "---\nimport LetterGlitch from \"../React/LetterGlitch.tsx\";\nimport LogoWall from \"../components/logoWall.astro\";\nimport Sk"
},
{
"path": "src/components/logoWall.astro",
"chars": 1712,
"preview": "---\nconst technologies = [\n \"astro\",\n \"vue\",\n \"react\",\n \"typeScript\",\n \"tailwindcss\",\n \"next\",\n \"nodejs\",\n \"HTML"
},
{
"path": "src/components/nav.astro",
"chars": 6015,
"preview": "---\ninterface NavItem {\n label: string;\n href: string;\n icon: string;\n}\n\nconst navItems: NavItem[] = [\n {\n label:"
},
{
"path": "src/components/projects.astro",
"chars": 6122,
"preview": "---\nimport { Image } from \"astro:assets\";\nimport svgl from \"../../public/svgl.png\";\nimport stockin from \"../../public/st"
},
{
"path": "src/env.d.ts",
"chars": 46,
"preview": "/// <reference path=\"../.astro/types.d.ts\" />\n"
},
{
"path": "src/firebase.ts",
"chars": 554,
"preview": "import { initializeApp } from 'firebase/app';\nimport { getFirestore } from 'firebase/firestore';\n\nconst firebaseConfig ="
},
{
"path": "src/layouts/Layout.astro",
"chars": 3271,
"preview": "---\ninterface Props {\n title: string;\n}\n\nconst { title } = Astro.props;\n---\n\n<!doctype html>\n<html lang=\"en\">\n <head>\n"
},
{
"path": "src/pages/index.astro",
"chars": 410,
"preview": "---\nimport Layout from \"@/layouts/Layout.astro\";\nimport Nav from \"@/components/nav.astro\";\nimport Home from \"@/component"
},
{
"path": "tailwind.config.mjs",
"chars": 772,
"preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts"
},
{
"path": "tsconfig.json",
"chars": 473,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"ES2020\",\n \"allowImportingTsExtensions\": true,\n \"noEmit\": true,\n \"types\""
}
]
About this extraction
This page contains the full source code of the Gothsec/Portfolio GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (60.9 KB), approximately 19.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.