[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: Gothsec\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n\n# jetbrains setting folder\n.idea/\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\noscarandreshernandezpineda@gmail.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Oscar Hernandez \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Portfolio\n![OscarHernandez-portfolio](https://github.com/user-attachments/assets/e284a42b-15c5-495c-99c7-ad5c1eb3bbe7)\n![Deploy Status](https://img.shields.io/badge/Deploy-Vercel-black?style=flat&logo=vercel)\n\n---\n\n[Demo](https://oscarhernandez.vercel.app/)\n\n[Astro Themes](https://astro.build/themes/details/dark-minimal/)\n\n[ReactBits Showcase](https://www.reactbits.dev/showcase) \n\nThe component `<LetterGlitch \\>` was taken from [ReactBits.dev](https://www.reactbits.dev/)\n\n## **Stack**  \n### **Frontend**  \n![Astro](https://img.shields.io/badge/Astro-FF5D01?logo=astro&logoColor=white)\n![Tailwind](https://img.shields.io/badge/Tailwind_CSS-38B2AC?logo=tailwind-css&logoColor=white)\n![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)\n\n### **Tools**  \n![Figma](https://img.shields.io/badge/Figma-F24E1E?logo=figma&logoColor=white)\n![Prettier](https://img.shields.io/badge/Prettier-F7B93E?logo=prettier&logoColor=black)\n![Canva](https://img.shields.io/badge/Canva-c900c3?logo=canva&logoColor=white)\n\n### **Show your favorite Spotify album (or your own)** ![Spotify](https://img.shields.io/badge/Spotify-06cc1a?logo=spotify&logoColor=white)\n1. Choose your Spotify album\n2. Access the share options\n3. Select 'copy embed code'\n```\n<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>\n```\n4. Insert the embed code on footer.astro\n\nThat's it!\n\n## **Project structure**\n```\npublic/\n└── svg/\nsrc/\n├── Components/\n|    ├── contact.astro\n|    ├── footer.astro\n|    ├── home.astro\n|    ├── logoWall.astro\n|    ├── nav.astro\n|    └── projects.astro\n├── layouts/\n|    └── Layout.astro\n├── React/\n|    ├── LetterGlitch.tsx\n|    ├── LikeButton.tsx\n|    └── SkillsList.tsx\n└── pages/\n     └── index.astro\n```\n\n## **Local configuration** \n1. Clone the repo:  \n```\ngit clone https://github.com/Gothsec/Astro-portfolio\n```\n2. Install dependencies:\n```  \nnpm install\n```\n3. Start the development server:\n```  \nnpm run dev\n```\n\n> **Important Notice:**  \n> This project is licensed under the [MIT License](https://opensource.org/licenses/mit).  \n> According to the license terms, any redistribution (including compiled or modified versions), you **must** retain the original copyright \n> notice and the full license text. Copyright © 2026 Oscar Hernandez. All rights reserved.\n"
  },
  {
    "path": "astro.config.mjs",
    "content": "// @ts-check\nimport { defineConfig } from \"astro/config\";\nimport tailwind from \"@astrojs/tailwind\";\n\nimport react from \"@astrojs/react\";\n\n// https://astro.build/config\nexport default defineConfig({\n  integrations: [tailwind(), react()],\n  vite: {\n    resolve: {\n      alias: {\n        \"@\": \"/src\",\n        \"@components\": \"/src/components\",\n      },\n    },\n  },\n  output: \"static\",\n  build: {\n    inlineStylesheets: \"auto\",\n  },\n  server: {\n    host: true,\n    port: 4321,\n  },\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"portfolio\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro check && astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/check\": \"^0.9.9\",\n    \"@astrojs/react\": \"^5.0.4\",\n    \"@astrojs/tailwind\": \"^6.0.2\",\n    \"@fontsource-variable/montserrat\": \"^5.2.8\",\n    \"@types/react\": \"^19.1.16\",\n    \"@types/react-dom\": \"^19.1.9\",\n    \"astro\": \"^5.18.1\",\n    \"firebase\": \"^12.12.1\",\n    \"ogl\": \"^1.0.11\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"sharp\": \"^0.34.5\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^25.6.0\",\n    \"prettier\": \"^3.8.3\",\n    \"prettier-plugin-astro\": \"^0.14.1\"\n  }\n}\n"
  },
  {
    "path": "src/React/LetterGlitch.tsx",
    "content": "import { useRef, useEffect } from \"react\";\n\nconst LetterGlitch = ({\n  glitchColors = [\"#5e4491\", \"#A476FF\", \"#241a38\"],\n  glitchSpeed = 33,\n  centerVignette = false,\n  outerVignette = false,\n  smooth = true,\n}: {\n  glitchColors: string[];\n  glitchSpeed: number;\n  centerVignette: boolean;\n  outerVignette: boolean;\n  smooth: boolean;\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const animationRef = useRef<number | null>(null);\n  const letters = useRef<\n    {\n      char: string;\n      color: string;\n      targetColor: string;\n      colorProgress: number;\n    }[]\n  >([]);\n  const grid = useRef({ columns: 0, rows: 0 });\n  const context = useRef<CanvasRenderingContext2D | null>(null);\n  const lastGlitchTime = useRef(Date.now());\n\n  const fontSize = 16;\n  const charWidth = 10;\n  const charHeight = 20;\n\n  const lettersAndSymbols = [\n    \"A\",\n    \"B\",\n    \"C\",\n    \"D\",\n    \"E\",\n    \"F\",\n    \"G\",\n    \"H\",\n    \"I\",\n    \"J\",\n    \"K\",\n    \"L\",\n    \"M\",\n    \"N\",\n    \"O\",\n    \"P\",\n    \"Q\",\n    \"R\",\n    \"S\",\n    \"T\",\n    \"U\",\n    \"V\",\n    \"W\",\n    \"X\",\n    \"Y\",\n    \"Z\",\n    \"!\",\n    \"@\",\n    \"#\",\n    \"$\",\n    \"&\",\n    \"*\",\n    \"(\",\n    \")\",\n    \"-\",\n    \"_\",\n    \"+\",\n    \"=\",\n    \"/\",\n    \"[\",\n    \"]\",\n    \"{\",\n    \"}\",\n    \";\",\n    \":\",\n    \"<\",\n    \">\",\n    \",\",\n    \"0\",\n    \"1\",\n    \"2\",\n    \"3\",\n    \"4\",\n    \"5\",\n    \"6\",\n    \"7\",\n    \"8\",\n    \"9\",\n  ];\n\n  const getRandomChar = () => {\n    return lettersAndSymbols[\n      Math.floor(Math.random() * lettersAndSymbols.length)\n    ];\n  };\n\n  const getRandomColor = () => {\n    return glitchColors[Math.floor(Math.random() * glitchColors.length)];\n  };\n\n  const hexToRgb = (hex: string) => {\n    const shorthandRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;\n    hex = hex.replace(shorthandRegex, (m, r, g, b) => {\n      return r + r + g + g + b + b;\n    });\n\n    const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n    return result\n      ? {\n          r: parseInt(result[1], 16),\n          g: parseInt(result[2], 16),\n          b: parseInt(result[3], 16),\n        }\n      : null;\n  };\n\n  const interpolateColor = (\n    start: { r: number; g: number; b: number },\n    end: { r: number; g: number; b: number },\n    factor: number,\n  ) => {\n    const result = {\n      r: Math.round(start.r + (end.r - start.r) * factor),\n      g: Math.round(start.g + (end.g - start.g) * factor),\n      b: Math.round(start.b + (end.b - start.b) * factor),\n    };\n    return `rgb(${result.r}, ${result.g}, ${result.b})`;\n  };\n\n  const calculateGrid = (width: number, height: number) => {\n    const columns = Math.ceil(width / charWidth);\n    const rows = Math.ceil(height / charHeight);\n    return { columns, rows };\n  };\n\n  const initializeLetters = (columns: number, rows: number) => {\n    grid.current = { columns, rows };\n    const totalLetters = columns * rows;\n    letters.current = Array.from({ length: totalLetters }, () => ({\n      char: getRandomChar(),\n      color: getRandomColor(),\n      targetColor: getRandomColor(),\n      colorProgress: 1,\n    }));\n  };\n\n  const resizeCanvas = () => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const parent = canvas.parentElement;\n    if (!parent) return;\n\n    const dpr = window.devicePixelRatio || 1;\n    const rect = parent.getBoundingClientRect();\n\n    canvas.width = rect.width * dpr;\n    canvas.height = rect.height * dpr;\n\n    canvas.style.width = `${rect.width}px`;\n    canvas.style.height = `${rect.height}px`;\n\n    if (context.current) {\n      context.current.setTransform(dpr, 0, 0, dpr, 0, 0);\n    }\n\n    const { columns, rows } = calculateGrid(rect.width, rect.height);\n    initializeLetters(columns, rows);\n    drawLetters();\n  };\n\n  const drawLetters = () => {\n    if (!context.current || letters.current.length === 0) return;\n    const ctx = context.current;\n    const { width, height } = canvasRef.current!.getBoundingClientRect();\n    ctx.clearRect(0, 0, width, height);\n    ctx.font = `${fontSize}px monospace`;\n    ctx.textBaseline = \"top\";\n\n    letters.current.forEach((letter, index) => {\n      const x = (index % grid.current.columns) * charWidth;\n      const y = Math.floor(index / grid.current.columns) * charHeight;\n      ctx.fillStyle = letter.color;\n      ctx.fillText(letter.char, x, y);\n    });\n  };\n\n  const updateLetters = () => {\n    if (!letters.current || letters.current.length === 0) return; // Prevent accessing empty array\n\n    const updateCount = Math.max(1, Math.floor(letters.current.length * 0.05));\n\n    for (let i = 0; i < updateCount; i++) {\n      const index = Math.floor(Math.random() * letters.current.length);\n      if (!letters.current[index]) continue; // Skip if index is invalid\n\n      letters.current[index].char = getRandomChar();\n      letters.current[index].targetColor = getRandomColor();\n\n      if (!smooth) {\n        letters.current[index].color = letters.current[index].targetColor;\n        letters.current[index].colorProgress = 1;\n      } else {\n        letters.current[index].colorProgress = 0;\n      }\n    }\n  };\n\n  const handleSmoothTransitions = () => {\n    let needsRedraw = false;\n    letters.current.forEach((letter) => {\n      if (letter.colorProgress < 1) {\n        letter.colorProgress += 0.05;\n        if (letter.colorProgress > 1) letter.colorProgress = 1;\n\n        const startRgb = hexToRgb(letter.color);\n        const endRgb = hexToRgb(letter.targetColor);\n        if (startRgb && endRgb) {\n          letter.color = interpolateColor(\n            startRgb,\n            endRgb,\n            letter.colorProgress,\n          );\n          needsRedraw = true;\n        }\n      }\n    });\n\n    if (needsRedraw) {\n      drawLetters();\n    }\n  };\n\n  const animate = () => {\n    const now = Date.now();\n    if (now - lastGlitchTime.current >= glitchSpeed) {\n      updateLetters();\n      drawLetters();\n      lastGlitchTime.current = now;\n    }\n\n    if (smooth) {\n      handleSmoothTransitions();\n    }\n\n    animationRef.current = requestAnimationFrame(animate);\n  };\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    context.current = canvas.getContext(\"2d\");\n    resizeCanvas();\n    animate();\n\n    let resizeTimeout: NodeJS.Timeout;\n\n    const handleResize = () => {\n      clearTimeout(resizeTimeout);\n      resizeTimeout = setTimeout(() => {\n        cancelAnimationFrame(animationRef.current as number);\n        resizeCanvas();\n        animate();\n      }, 100);\n    };\n\n    window.addEventListener(\"resize\", handleResize);\n\n    return () => {\n      cancelAnimationFrame(animationRef.current!);\n      window.removeEventListener(\"resize\", handleResize);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [glitchSpeed, smooth]);\n\n  return (\n    <div className=\"relative w-full h-full bg-[#101010] overflow-hidden\">\n      <canvas ref={canvasRef} className=\"block w-full h-full\" />\n      {outerVignette && (\n        <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>\n      )}\n      {centerVignette && (\n        <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>\n      )}\n    </div>\n  );\n};\n\nexport default LetterGlitch;\n"
  },
  {
    "path": "src/React/LikeButton.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { doc, onSnapshot, updateDoc, increment } from \"firebase/firestore\";\nimport { db } from \"../firebase\";\n\nconst LikeButton = () => {\n  const [likes, setLikes] = useState(0);\n  const [isLiked, setIsLiked] = useState(false);\n  const [isClient, setIsClient] = useState(false);\n  const [isAnimating, setIsAnimating] = useState(false);\n  const [isProcessing, setIsProcessing] = useState(false);\n\n  useEffect(() => {\n    setIsClient(true);\n\n    const storedIsLiked = localStorage.getItem(\"websiteIsLiked\");\n    if (storedIsLiked) {\n      setIsLiked(storedIsLiked === \"true\");\n    }\n\n    // Listen for realtime updates from Firestore\n    const likeDocRef = doc(db, \"likes\", \"counter\");\n    const unsubscribe = onSnapshot(likeDocRef, (docSnap) => {\n      if (docSnap.exists()) {\n        const currentLikes = docSnap.data().likes;\n        // Only update if the server value is different (prevents overwrite during optimistic update)\n        setLikes((prev) => {\n          const newLikes = Math.max(0, currentLikes);\n          return newLikes;\n        });\n      }\n    });\n\n    return () => unsubscribe();\n  }, []);\n\n  const handleLike = async () => {\n    if (isProcessing || isLiked) return;\n\n    // Optimistic Update\n    const previousLikes = likes;\n    setLikes((prev) => prev + 1);\n    setIsLiked(true);\n    setIsAnimating(true);\n    localStorage.setItem(\"websiteIsLiked\", \"true\");\n\n    // Reset animation after it finishes\n    setTimeout(() => setIsAnimating(false), 600);\n\n    try {\n      setIsProcessing(true);\n      const likeDocRef = doc(db, \"likes\", \"counter\");\n      await updateDoc(likeDocRef, {\n        likes: increment(1),\n      });\n    } catch (error) {\n      console.error(\"Error updating likes:\", error);\n      // Rollback on error\n      setLikes(previousLikes);\n      setIsLiked(false);\n      localStorage.removeItem(\"websiteIsLiked\");\n    } finally {\n      setIsProcessing(false);\n    }\n  };\n\n  if (!isClient) return null;\n\n  const borderColorClass = isLiked\n    ? \"border-[var(--sec)]\"\n    : \"border-[var(--white-icon)]\";\n\n  return (\n    <div className=\"flex items-center\">\n      <button\n        onClick={handleLike}\n        disabled={isProcessing || isLiked}\n        className={`\n          group relative w-40 h-10 flex items-center justify-center p-3\n          rounded-full transition-all duration-300 ease-in-out transform border-2 ${borderColorClass}\n          ${!isLiked ? \"hover:scale-105 hover:border-[var(--white)]\" : \"cursor-default\"}\n          ${isAnimating ? \"animate-heart-pulse\" : \"\"}\n        `}\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 24 24\"\n          fill=\"currentColor\"\n          className={`w-6 h-6 transition-all duration-300 ease-in-out \n            ${isLiked ? \"text-[var(--sec)] scale-110\" : \"text-[var(--white-icon)] group-hover:text-[var(--white)] group-hover:scale-105\"}\n          `}\n        >\n          <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>\n        </svg>\n        <span className=\"text-sm pl-3 font-medium text-[var(--white)]\">\n          {likes} Likes\n        </span>\n      </button>\n    </div>\n  );\n};\n\nexport default LikeButton;\n"
  },
  {
    "path": "src/React/SkillsList.tsx",
    "content": "import React, { useState } from \"react\";\n\nconst CategoryIcons = {\n  \"Web Development\": (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      className=\"w-6 h-6 text-[var(--sec)] opacity-70\"\n    >\n      <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>\n    </svg>\n  ),\n  \"Mobile Development\": (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      className=\"w-6 h-6 text-[var(--sec)] opacity-70\"\n    >\n      <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>\n    </svg>\n  ),\n  \"UI/UX Design & Prototyping\": (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      className=\"w-6 h-6 text-[var(--sec)] opacity-70\"\n    >\n      <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>\n    </svg>\n  ),\n};\n\nconst SkillsList = () => {\n  const [openItem, setOpenItem] = useState<string | null>(null);\n\n  const skills = {\n    \"Web Development\": [\n      \"Single Page Applications (SPAs)\",\n      \"Landing pages and business websites\",\n      \"Portfolio websites\",\n    ],\n    \"Mobile Development\": [\n      \"Mobile-friendly web apps\",\n      \"React Native mobile apps\",\n    ],\n    \"UI/UX Design & Prototyping\": [\n      \"UI design with Figma & Canva\",\n      \"UX research & improvements\",\n      \"Prototyping for websites & mobile apps\",\n    ],\n  };\n\n  const toggleItem = (item: string) => {\n    setOpenItem(openItem === item ? null : item);\n  };\n\n  return (\n    <div className=\"text-left pt-3 md:pt-9\">\n      <h3 className=\"text-[var(--white)] text-3xl md:text-4xl font-semibold md:mb-6\">\n        What I do?\n      </h3>\n      <ul className=\"space-y-4 mt-4 text-lg\">\n        {Object.entries(skills).map(([category, items]) => (\n          <li key={category} className=\"w-full\">\n            <div\n              onClick={() => toggleItem(category)}\n              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\"\n            >\n              <div className=\"flex items-center gap-3 p-4\">\n                {CategoryIcons[category]}\n                <div className=\"flex items-center gap-2 flex-grow justify-between\">\n                  <div className=\"min-w-0 max-w-[200px] md:max-w-none overflow-hidden\">\n                    <span className=\"block truncate text-[var(--white)] text-lg\">\n                      {category}\n                    </span>\n                  </div>\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"currentColor\"\n                    className={`w-6 h-6 text-[var(--white)] transform transition-transform flex-shrink-0 ${\n                      openItem === category ? \"rotate-180\" : \"\"\n                    }`}\n                  >\n                    <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>\n                  </svg>\n                </div>\n              </div>\n\n              <div\n                className={`transition-all duration-300 px-4 ${\n                  openItem === category\n                    ? \"max-h-[500px] pb-4 opacity-100\"\n                    : \"max-h-0 opacity-0\"\n                }`}\n              >\n                <ul className=\"space-y-2 text-[var(--white-icon)] text-sm\">\n                  {items.map((item, index) => (\n                    <div key={index} className=\"flex items-center\">\n                      <span className=\"pl-1\">•</span>\n                      <li className=\"pl-3\">{item}</li>\n                    </div>\n                  ))}\n                </ul>\n              </div>\n            </div>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n};\n\nexport default SkillsList;\n"
  },
  {
    "path": "src/components/contact.astro",
    "content": "<section id=\"contact\" class=\"w-full py-12 border-t border-[#ffffff10]\">\n  <div class=\"max-w-5xl mx-auto\">\n    <h2 class=\"text-lg text-[var(--sec)] mb-2 shiny-sec\">Let's talk</h2>\n    <h3 class=\"text-4xl md:text-5xl font-medium text-[var(--white)] mb-6\">\n      Contact\n    </h3>\n    <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n      <div class=\"text-[var(--white-icon)]\">\n        <p class=\"mb-4\">\n          Have a question or a project in mind? Feel free to reach out.\n        </p>\n        <div class=\"flex items-center gap-2\">\n          <span>Location:</span>\n          <span class=\"text-[var(--white)]\">Colombia, Valle del cauca</span>\n        </div>\n      </div>\n\n      <div>\n        <form\n          id=\"contact-form\"\n          action=\"https://formspree.io/f/mnnjznkj\"\n          method=\"POST\"\n          class=\"flex flex-col gap-4\"\n        >\n          <input\n            type=\"text\"\n            name=\"from_name\"\n            placeholder=\"Name\"\n            required\n            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)]\"\n          />\n          <input\n            type=\"email\"\n            name=\"reply_to\"\n            placeholder=\"Email\"\n            required\n            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)]\"\n          />\n          <textarea\n            name=\"message\"\n            placeholder=\"Message\"\n            rows=\"6\"\n            required\n            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\"\n          ></textarea>\n          <button\n            type=\"submit\"\n            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)]\"\n          >\n            Submit\n          </button>\n        </form>\n        <div\n          id=\"form-message\"\n          class=\"hidden justify-center items-center mt-4 text-[var(--white)] text-lg\"\n        >\n          ✅ Thank you for your message!\n        </div>\n      </div>\n    </div>\n  </div>\n</section>\n\n<script type=\"module\" is:inline>\n  const form = document.getElementById(\"contact-form\");\n  const formMessage = document.getElementById(\"form-message\");\n\n  form.addEventListener(\"submit\", async (e) => {\n    e.preventDefault();\n    const formData = new FormData(form);\n\n    try {\n      const response = await fetch(form.action, {\n        method: \"POST\",\n        body: formData,\n        headers: { Accept: \"application/json\" },\n      });\n      if (response.ok) {\n        form.reset();\n        form.style.display = \"none\";\n        formMessage.classList.remove(\"hidden\");\n      } else {\n        const data = await response.json();\n        console.error(\"Error response:\", data);\n        alert(\"There was a problem sending your message.\");\n      }\n    } catch (error) {\n      console.error(\"Error:\", error);\n      alert(\"There was a problem sending your message.\");\n    }\n  });\n</script>\n"
  },
  {
    "path": "src/components/footer.astro",
    "content": "---\nimport LikeButton from \"../React/LikeButton.tsx\";\n\nconst currentYear = new Date().getFullYear();\n---\n\n<footer class=\"w-full py-12 border-t border-[#ffffff10]\">\n  <div class=\"max-w-5xl mx-auto\">\n    <div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-10\">\n      <div class=\"flex flex-col lg:items-start items-center space-y-6 gap-9\">\n        <div class=\"flex space-x-6 sm:space-x-8\">\n          {\n            [\n              {\n                href: \"https://github.com/gothsec\",\n                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>',\n                label: \"GitHub\",\n              },\n              {\n                href: \"https://linkedin.com/in/hernandezoscar-dev\",\n                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>',\n                label: \"LinkedIn\",\n              },\n              {\n                href: \"https://mail.google.com/mail/?view=cm&fs=1&to=oscarandreshernandezpineda@gmail.com&su=Hey%20Oscar!\",\n                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>',\n                label: \"Email\",\n              },\n            ].map((link) => (\n              <a\n                href={link.href}\n                target=\"_blank\"\n                class=\"flex flex-col items-center group\"\n                aria-label={link.label}\n              >\n                <div class=\"text-[var(--white-icon)] hover:text-[var(--white)] transition duration-300 ease-in-out\">\n                  <div set:html={link.icon} />\n                </div>\n              </a>\n            ))\n          }\n        </div>\n        <LikeButton client:load />\n      </div>\n\n      <div class=\"flex flex-col items-center space-y-6\">\n        <div class=\"grid grid-cols-1 gap-3 w-full max-w-xs py-6\">\n          {\n            [\n              {\n                desc: \"Built with\",\n                name: \"Astro\",\n                icon: \"/svg/astro.svg\",\n                alt: \"Astro Logo\",\n              },\n              {\n                desc: \"Styled with\",\n                name: \"TailwindCSS\",\n                icon: \"/svg/tailwindcss.svg\",\n                alt: \"TailwindCSS Logo\",\n              },\n              {\n                desc: \"Deployed on\",\n                name: \"Vercel\",\n                icon: \"/svg/vercel.svg\",\n                alt: \"Vercel Logo\",\n              },\n            ].map((tech) => (\n              <div class=\"flex items-center justify-center lg:justify-normal space-x-3\">\n                <span class=\"text-[var(--white-icon)] text-sm\">\n                  {tech.desc}\n                </span>\n                <img\n                  src={tech.icon}\n                  alt={tech.alt}\n                  class=\"h-5 w-5 object-contain filter brightness-0 invert opacity-50\"\n                  loading=\"lazy\"\n                />\n                <span class=\"text-[var(--white-icon)] text-sm\">\n                  {tech.name}\n                </span>\n              </div>\n            ))\n          }\n        </div>\n      </div>\n\n      <div class=\"flex flex-col items-center lg:items-start space-y-6\">\n        <div class=\"w-full max-w-xs\">\n          <iframe\n            style=\"border-radius:12px; border:0;\"\n            src=\"https://open.spotify.com/embed/playlist/2irOd49FRRLFWYccolQeea?utm_source=generator&theme=0&locale=en_US\"\n            class=\"w-full h-40\"\n            allowfullscreen=\"\"\n            allow=\"autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture\"\n            loading=\"lazy\"></iframe>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"mt-12 pt-8 border-t border-[#ffffff10]\">\n      <p class=\"text-center text-sm text-[var(--white-icon)] space-y-2\">\n        <!-- If you are using this template, by MIT License you can't remove the copyright notice -->\n        <span class=\"block sm:inline\"\n          >Copyright © {currentYear} <a href=\"https://github.com/Gothsec\"\n            >Oscar Hernandez</a\n          >. All rights reserved.</span\n        >\n      </p>\n    </div>\n  </div>\n</footer>\n"
  },
  {
    "path": "src/components/home.astro",
    "content": "---\nimport LetterGlitch from \"../React/LetterGlitch.tsx\";\nimport LogoWall from \"../components/logoWall.astro\";\nimport SkillsList from \"../React/SkillsList.tsx\";\n---\n\n<section class=\"text-[var(--white)] mt-12 md:mt-0\" id=\"home\">\n  <div class=\"max-w-5xl mx-auto space-y-8 md:py-36 pb-14\">\n    <div class=\"text-left space-y-4\">\n      <p class=\"text-md md:text-lg text-[var(--white-icon)] shiny-white\">\n        Hi, I'm Oscar Hernandez\n      </p>\n      <div\n        class=\"flex flex-col lg:flex-row lg:items-center space-y-4 lg:space-y-0 lg:space-x-8 md:gap-4\"\n      >\n        <h1\n          class=\"text-[var(--white)] text-5xl md:text-6xl font-medium text-pretty leading-none\"\n        >\n          Software <br /> Developer\n        </h1>\n        <p class=\"text-md md:text-2xl text-[var(--white-icon)]\">\n          Transforming ideas into interactive and seamless digital experiences\n          with cutting-edge <span class=\"text-[var(--sec)] shiny-sec\"\n            >frontend</span\n          > development.\n        </p>\n      </div>\n      <div class=\"flex justify-start gap-2 pt-3 md:pt-6\">\n        <a\n          target=\"_blank\"\n          href=\"https://github.com/gothsec\"\n          aria-label=\"GitHub\"\n          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)]\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            class=\"size-8\"\n          >\n            <path\n              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\"\n            ></path>\n          </svg>\n        </a>\n        <a\n          target=\"_blank\"\n          href=\"https://linkedin.com/in/andresshernandez-eng\"\n          aria-label=\"LinkedIn\"\n          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)]\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            class=\"size-8\"\n          >\n            <path\n              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\"\n            ></path>\n          </svg>\n        </a>\n        <a\n          target=\"_blank\"\n          href=\"https://mail.google.com/mail/?view=cm&fs=1&to=oscarandreshernandezpineda@gmail.com&su=Hey%20Oscar!\"\n          aria-label=\"Email\"\n          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)]\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"2.1em\"\n            height=\"2.1em\"\n            viewBox=\"0 0 24 24\"\n          >\n            <path\n              fill=\"currentColor\"\n              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\"\n            ></path>\n          </svg>\n        </a>\n      </div>\n    </div>\n\n    <LogoWall />\n\n    <div class=\"flex flex-col lg:flex-row items-center gap-8\">\n      <SkillsList client:load />\n      <div\n        class=\"flex justify-center md:w-full md:h-[292px] size-[290px] pt-3 md:pt-9 md:ml-16\"\n      >\n        <LetterGlitch\n          client:load\n          glitchColors={[\"#5e4491\", \"#A476FF\", \"#241a38\"]}\n          glitchSpeed={33}\n          centerVignette={false}\n          outerVignette={true}\n          smooth={true}\n        />\n      </div>\n    </div>\n  </div>\n</section>\n\n<style is:global>\n  .shiny-sec {\n    background: linear-gradient(135deg, #a476ff 25%, #eee5ff 50%, #a476ff 75%);\n    background-size: 400% 100%;\n    -webkit-background-clip: text;\n    background-clip: text;\n    color: transparent;\n    animation: shine 3s linear infinite;\n  }\n\n  @keyframes shine {\n    0% {\n      background-position: 100% 50%;\n    }\n    30%,\n    70% {\n      background-position: 0% 50%;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/logoWall.astro",
    "content": "---\nconst technologies = [\n  \"astro\",\n  \"vue\",\n  \"react\",\n  \"typeScript\",\n  \"tailwindcss\",\n  \"next\",\n  \"nodejs\",\n  \"HTML5\",\n  \"CSS3\",\n  \"javaScript\",\n  \"git\",\n  \"supabase\",\n  \"mysql\",\n  \"bash\",\n];\n---\n\n<div class=\"relative overflow-x-hidden py-8\">\n  <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>\n  <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>\n\n  <div class=\"flex animate-scroll w-max will-change-transform\">\n    {\n      [...technologies, ...technologies].map((tech, index) => (\n        <div \n          class=\"flex items-center gap-2 pr-12 md:pr-20 group transition-all duration-300\"\n          aria-hidden={index >= technologies.length ? \"true\" : \"false\"}\n        >\n          <img\n            src={`/svg/${tech}.svg`}\n            alt={tech}\n            class=\"h-7 w-auto object-contain transition-transform group-hover:scale-110 opacity-60\"\n            width=\"30\"\n            height=\"30\"\n            loading={index < technologies.length ? \"eager\" : \"lazy\"}\n            decoding=\"async\"\n          />\n          <span class=\"text-lg font-medium text-[var(--white-icon)] whitespace-nowrap\">\n            {tech.charAt(0).toUpperCase() + tech.slice(1)}\n          </span>\n        </div>\n      ))\n    }\n  </div>\n</div>\n\n<style is:global>\n  @keyframes scroll {\n    0% {\n      transform: translate3d(0, 0, 0);\n    }\n    100% {\n      transform: translate3d(-50%, 0, 0);\n    }\n  }\n  .animate-scroll {\n    animation: scroll 60s linear infinite;\n  }\n\n  @media (min-width: 768px) {\n    .animate-scroll {\n      animation-duration: 50s;\n    }\n  }\n</style>"
  },
  {
    "path": "src/components/nav.astro",
    "content": "---\ninterface NavItem {\n  label: string;\n  href: string;\n  icon: string;\n}\n\nconst navItems: NavItem[] = [\n  {\n    label: \"Home\",\n    href: \"#home\",\n    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>`,\n  },\n  {\n    label: \"Projects\",\n    href: \"#projects\",\n    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>`,\n  },\n  {\n    label: \"Contact\",\n    href: \"#contact\",\n    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>`,\n  },\n];\n---\n\n<div class=\"flex justify-center w-full\">\n  <nav\n    id=\"main-nav\"\n    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%]\"\n  >\n    <div class=\"container mx-auto flex justify-center items-center p-3\">\n      <ul\n        class=\"flex w-full justify-between md:space-x-6 md:justify-center md:gap-12 gap-6\"\n      >\n        {\n          navItems.map((item) => (\n            <li class=\"flex-1 md:flex-none\">\n              <a\n                href={item.href}\n                class=\"flex flex-col items-center gap-1 text-[var(--white-icon)] transition-colors text-xs md:text-base relative group\"\n              >\n                <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\" />\n                <span class=\"md:hidden flex items-center justify-center w-6 h-6\">\n                  <fragment set:html={item.icon} />\n                </span>\n                <span class=\"hidden md:inline-block\">{item.label}</span>\n                <span class=\"md:hidden\">{item.label}</span>\n              </a>\n            </li>\n          ))\n        }\n      </ul>\n    </div>\n  </nav>\n</div>\n<style>\n  nav {\n    transform: translateX(-50%);\n    background-color: var(--background);\n    transition:\n      background-color 0.3s ease,\n      border-radius 0.3s ease,\n      border-color 0.3s ease;\n  }\n\n  nav.scrolling {\n    background-color: var(--component-bg);\n    border-color: #ffffff10;\n    border-radius: 9999px;\n  }\n\n  nav a.active .nav-indicator {\n    transform: translateY(-50%) scale(1);\n    opacity: 1;\n  }\n\n  nav a.active {\n    color: white !important;\n  }\n\n  @media (max-width: 767px) {\n    nav {\n      width: 100% !important;\n      transform: translateX(-50%);\n      bottom: 0;\n      left: 50%;\n      position: fixed;\n      border-radius: 1rem 1rem 0 0;\n      border-color: #ffffff10;\n    }\n\n    nav.scrolling {\n      border-radius: 1rem 1rem 0 0;\n      background-color: var(--component-bg);\n    }\n\n    body {\n      padding-bottom: 70px;\n    }\n  }\n</style>\n\n<script>\n  const nav = document.getElementById(\"main-nav\");\n  const maxScroll = 1000;\n  let rafId: number | null = null;\n\n  function updateNav() {\n    if (window.scrollY > 0) {\n      nav?.classList.add(\"scrolling\");\n\n      const scrollProgress = Math.min(window.scrollY / maxScroll, 1);\n      const easeProgress = 1 - Math.pow(1 - scrollProgress, 4);\n\n      const minWidth = 528;\n      const maxWidth = window.innerWidth * 0.8;\n      const currentWidth = maxWidth - (maxWidth - minWidth) * easeProgress;\n\n      if (window.innerWidth >= 768) {\n        nav?.style.setProperty(\"width\", `${currentWidth}px`);\n      }\n    } else {\n      nav?.classList.remove(\"scrolling\");\n      nav?.style.setProperty(\"width\", \"80%\");\n    }\n    rafId = null;\n  }\n\n  window.addEventListener(\n    \"scroll\",\n    () => {\n      if (!rafId) {\n        rafId = requestAnimationFrame(updateNav);\n      }\n    },\n    { passive: true }\n  );\n\n  document.querySelectorAll('a[href^=\"#\"]').forEach((anchor) => {\n    anchor.addEventListener(\"click\", function (e) {\n      e.preventDefault();\n      const target = e.currentTarget as HTMLAnchorElement;\n      const targetId = target.getAttribute(\"href\")?.substring(1) || \"\";\n      const targetElement = document.getElementById(targetId);\n      if (targetElement) {\n        targetElement.scrollIntoView({\n          behavior: \"smooth\",\n        });\n      }\n    });\n  });\n\n  document.addEventListener(\"DOMContentLoaded\", () => {\n    const sections = document.querySelectorAll(\"section[id]\");\n    const navLinks = document.querySelectorAll(\"nav a[href^='#']\");\n    const observerOptions = { threshold: 0.6 };\n\n    const observerCallback = (entries) => {\n      entries.forEach((entry) => {\n        if (entry.isIntersecting) {\n          navLinks.forEach((link) => link.classList.remove(\"active\"));\n          const id = entry.target.getAttribute(\"id\");\n          const activeLink = document.querySelector(`nav a[href=\"#${id}\"]`);\n          if (activeLink) {\n            activeLink.classList.add(\"active\");\n          }\n        }\n      });\n    };\n\n    const observer = new IntersectionObserver(\n      observerCallback,\n      observerOptions\n    );\n    sections.forEach((section) => observer.observe(section));\n  });\n</script>\n\n<style>\n  @media (max-width: 767px) {\n    body {\n      padding-bottom: 70px;\n    }\n  }\n\n  nav a.active {\n    color: white !important;\n  }\n</style>\n"
  },
  {
    "path": "src/components/projects.astro",
    "content": "---\nimport { Image } from \"astro:assets\";\nimport svgl from \"../../public/svgl.png\";\nimport stockin from \"../../public/stockin.png\";\nimport moviesfordevs from \"../../public/moviesfordevs.png\";\nimport velez from \"../../public/velez.png\";\n\ninterface Project {\n  title: string;\n  image: ImageMetadata;\n  link: string;\n  preview: string;\n  status: string;\n}\n\nconst projects: Project[] = [\n  {\n    title: \"MoviesForDevs\",\n    image: moviesfordevs as ImageMetadata,\n    link: \"https://github.com/gothsec/MoviesForDevs\",\n    preview: \"https://movies-for-devs.vercel.app\",\n    status: \"Deployed\",\n  },\n  {\n    title: \"StockIn\",\n    image: stockin as ImageMetadata,\n    link: \"https://github.com/gothsec/stockin-demo\",\n    preview: \"https://stockin-demo.vercel.app\",\n    status: \"On Development\",\n  },\n  {\n    title: \"Svgl.app\",\n    image: svgl as ImageMetadata,\n    link: \"https://github.com/pheralb/svgl\",\n    preview: \"https://svgl.app\",\n    status: \"Contributor\",\n  },\n  {\n    title: \"Rifas Velez Web\",\n    image: velez as ImageMetadata,\n    link: \"https://github.com/Buga-Software/rifasvelez-web\",\n    preview: \"https://www.rifasvelez.com\",\n    status: \"Deployed\",\n  },\n];\n---\n\n<section\n  id=\"projects\"\n  class=\"py-12 border-t border-[#ffffff10] text-[var(--white)]\"\n>\n  <div class=\"max-w-5xl mx-auto\">\n    <h2 class=\"text-lg text-[var(--sec)] mb-2 shiny-sec\">My work</h2>\n    <h3 class=\"text-4xl md:text-5xl font-medium mb-8\">Projects</h3>\n\n    <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n      {\n        projects.map((project) => (\n          <div class=\"group\">\n            <a\n              href={project.preview}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              class=\"block\"\n            >\n              <div class=\"rounded-2xl overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4\">\n                <Image\n                  src={project.image}\n                  alt={project.title}\n                  class=\"w-full h-48 md:h-72 object-cover group-hover:scale-105 transition-transform duration-300\"\n                />\n              </div>\n              <div class=\"flex items-center px-3\">\n                <div class=\"flex-grow\">\n                  <h4 class=\"text-2xl font-semibold\">{project.title}</h4>\n                  <span class=\"py-1 text-sm text-[var(--white-icon)]\">\n                    {project.status}\n                  </span>\n                </div>\n                <div class=\"flex gap-2 ml-auto\">\n                  <a\n                    target=\"_blank\"\n                    href={project.link}\n                    aria-label=\"GitHub\"\n                    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)]\"\n                  >\n                    <svg\n                      xmlns=\"http://www.w3.org/2000/svg\"\n                      viewBox=\"0 0 24 24\"\n                      fill=\"currentColor\"\n                      class=\"size-7\"\n                    >\n                      <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\" />\n                    </svg>\n                  </a>\n                  <a\n                    target=\"_blank\"\n                    href={project.preview}\n                    aria-label=\"Preview\"\n                    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)]\"\n                  >\n                    <svg\n                      xmlns=\"http://www.w3.org/2000/svg\"\n                      viewBox=\"0 0 24 24\"\n                      fill=\"currentColor\"\n                      class=\"size-7\"\n                    >\n                      <path d=\"M16.0037 9.41421L7.39712 18.0208L5.98291 16.6066L14.5895 8H7.00373V6H18.0037V17H16.0037V9.41421Z\" />\n                    </svg>\n                  </a>\n                </div>\n              </div>\n            </a>\n          </div>\n        ))\n      }\n    </div>\n    <a\n      target=\"_blank\"\n      href=\"https://github.com/Gothsec?tab=repositories\"\n      aria-label=\"GitHub\"\n      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\"\n    >\n      <span class=\"md:text-lg text-md\">More projects on</span>\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n        class=\"size-6\"\n      >\n        <path\n          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\"\n        ></path>\n      </svg>\n    </a>\n  </div>\n</section>\n"
  },
  {
    "path": "src/env.d.ts",
    "content": "/// <reference path=\"../.astro/types.d.ts\" />\n"
  },
  {
    "path": "src/firebase.ts",
    "content": "import { initializeApp } from 'firebase/app';\nimport { getFirestore } from 'firebase/firestore';\n\nconst firebaseConfig = {\n  apiKey: import.meta.env.FIREBASE_API_KEY,\n  authDomain: import.meta.env.PUBLIC_FIREBASE_AUTH_DOMAIN,\n  projectId: import.meta.env.PUBLIC_FIREBASE_PROJECT_ID,\n  storageBucket: import.meta.env.PUBLIC_FIREBASE_STORAGE_BUCKET,\n  messagingSenderId: import.meta.env.PUBLIC_FIREBASE_MESSAGING_SENDER_ID,\n  appId: import.meta.env.PUBLIC_FIREBASE_APP_ID,\n};\n\nconst app = initializeApp(firebaseConfig);\nexport const db = getFirestore(app);"
  },
  {
    "path": "src/layouts/Layout.astro",
    "content": "---\ninterface Props {\n  title: string;\n}\n\nconst { title } = Astro.props;\n---\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>{title} | Software Developer</title>\n    <meta\n      name=\"description\"\n      content=\"Oscar Andres Hernandez Pineda - Software Developer building interactive and seamless digital experiences with cutting-edge frontend development.\"\n    />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\" />\n    <link rel=\"canonical\" href=\"https://oscarhernandez.vercel.app/\" />\n\n    <link\n      rel=\"preload\"\n      href=\"/fonts/montserrat-latin-wght-normal.woff2\"\n      as=\"font\"\n      type=\"font/woff2\"\n      crossorigin=\"anonymous\"\n    />\n\n    <meta\n      property=\"og:title\"\n      content=\"Oscar Andres Hernandez Pineda | Software Developer\"\n    />\n    <meta\n      property=\"og:description\"\n      content=\"Portfolio of Oscar Andres Hernandez Pineda, Software Developer specializing in frontend and interactive experiences.\"\n    />\n    <meta\n      property=\"og:image\"\n      content=\"https://oscarhernandez.vercel.app/og.image.png\"\n    />\n    <meta property=\"og:url\" content=\"https://oscarhernandez.vercel.app\" />\n    <meta property=\"og:type\" content=\"website\" />\n    <meta\n      property=\"og:site_name\"\n      content=\"Oscar Andres Hernandez Pineda Portfolio\"\n    />\n\n    <!-- Structured Data (JSON-LD) -->\n    <script type=\"application/ld+json\">\n      {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Person\",\n        \"name\": \"Oscar Andres Hernandez Pineda\",\n        \"alternateName\": \"Oscar Hernandez\",\n        \"url\": \"https://oscarhernandez.vercel.app\",\n        \"image\": \"https://oscarhernandez.vercel.app/og.image.png\",\n        \"sameAs\": [\n          \"https://github.com/gothsec\",\n          \"https://linkedin.com/in/andresshernandez-eng\"\n        ],\n        \"jobTitle\": \"Software Developer\",\n        \"description\": \"Software Developer building interactive and seamless digital experiences.\"\n      }\n    </script>\n  </head>\n  <body class=\"bg-[--background] sm:px-28 lg:px-20 px-9\">\n    <slot />\n  </body>\n</html>\n\n<style is:global>\n  @font-face {\n    font-family: \"Montserrat Variable\";\n    src: url(\"/fonts/montserrat-latin-wght-normal.woff2\")\n      format(\"woff2-variations\");\n    font-weight: 100 900;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  :root {\n    --background: #101010;\n    --sec: #a476ff;\n    --white: #dfdfdf;\n    --white-icon: #f3f3f398;\n    --white-icon-tr: #f3f3f310;\n  }\n\n  * {\n    font-family:\n      \"Montserrat Variable\",\n      -apple-system,\n      system-ui,\n      sans-serif;\n    box-sizing: border-box;\n    padding: 0;\n    margin: 0;\n  }\n\n  *::selection {\n    background-color: var(--sec);\n    color: var(--background);\n  }\n\n  /* Scrollbar styles */\n\n  ::-webkit-scrollbar {\n    width: 15px;\n  }\n\n  ::-webkit-scrollbar-track {\n    background: var(--container);\n    border-radius: 30px;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    background: var(--background);\n    border-radius: 10px;\n  }\n\n  ::-webkit-scrollbar-thumb:hover {\n    background: var(--pink);\n  }\n\n  /* Scrollbar styles for Firefox */\n  * {\n    scrollbar-width: thin;\n    scrollbar-color: var(--line) var(--container);\n  }\n</style>\n"
  },
  {
    "path": "src/pages/index.astro",
    "content": "---\nimport Layout from \"@/layouts/Layout.astro\";\nimport Nav from \"@/components/nav.astro\";\nimport Home from \"@/components/home.astro\";\nimport Projects from \"@/components/projects.astro\";\nimport Contact from \"@/components/contact.astro\";\nimport Footer from \"@/components/footer.astro\";\n---\n\n<Layout title=\"Oscar Andres Hernandez Pineda\">\n  <Nav />\n  <Home />\n  <Projects />\n  <Contact />\n  <Footer />\n</Layout>\n"
  },
  {
    "path": "tailwind.config.mjs",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}\"],\n  theme: {\n    extend: {\n      keyframes: {\n        scaleAnim: {\n          '0%': { transform: 'scale(1)' },\n          '50%': { transform: 'scale(1.1)' },\n          '100%': { transform: 'scale(1)' },\n        },\n        'heart-pulse': {\n          '0%': { transform: 'scale(1)' },\n          '25%': { transform: 'scale(1.1)' },\n          '50%': { transform: 'scale(1)' },\n          '75%': { transform: 'scale(1.06)' },\n          '100%': { transform: 'scale(1)' },\n        },\n      },\n      animation: {\n        scale: 'scaleAnim 300ms ease-in-out',\n        'heart-pulse': 'heart-pulse 0.6s ease-in-out',\n      },\n    },\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ES2020\",\n    \"allowImportingTsExtensions\": true,\n    \"noEmit\": true,\n    \"types\": [\n      \"astro/client\"\n    ],\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ],\n      \"@components/*\": [\n        \"src/components/*\"\n      ]\n    },\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\"\n  },\n  \"include\": [\n    \"src/**/*\",\n    \"astro.config.ts\"\n  ]\n}"
  }
]