main a40ef6ab8c30 cached
39 files
105.8 KB
29.5k tokens
1 symbols
1 requests
Download .txt
Repository: alexdeploy/developer-portfolio-v2
Branch: main
Commit: a40ef6ab8c30
Files: 39
Total size: 105.8 KB

Directory structure:
gitextract_xra8ecv9/

├── .gitignore
├── LICENSE
├── README.md
├── app.config.ts
├── app.vue
├── assets/
│   ├── README.md
│   └── tailwind.css
├── components/
│   ├── AppFooter.vue
│   ├── AppHeader.vue
│   ├── CommentedText.vue
│   ├── ContactForm.vue
│   ├── FormContentCode.vue
│   ├── GistSnippet.vue
│   ├── GithubCorner.vue
│   ├── MobileMenu.vue
│   ├── ProjectCard.vue
│   ├── README.md
│   └── SnakeGame.vue
├── content/
│   └── README.md
├── developer.json
├── layouts/
│   ├── README.md
│   └── default.vue
├── middleware/
│   └── README.md
├── nuxt.config.ts
├── package.json
├── pages/
│   ├── README.md
│   ├── about-me.vue
│   ├── contact-me.vue
│   ├── index.vue
│   └── projects.vue
├── public/
│   ├── README.md
│   ├── pwa/
│   │   └── manifest.json
│   └── worker.js
├── tailwind.config.js
├── test/
│   ├── github.test.js
│   └── global.test.js
├── tsconfig.json
└── utils/
    ├── README.md
    └── github-api.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
*.log*
.nuxt
.nitro
.cache
.output
.env
dist


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 Álex Rueda

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
================================================
<h1 align="center">
  developer-portfolio-v2
</h1>
<p align="center">
  The first open source version of <a href="https://www.figma.com/community/file/1100794861710979147" target="_blank"> Portfolio for Developers Concept V.2</a>, designed by <a href="https://www.behance.net/darelova" target="_blank">@darelova</a> and developed by <a href="https://github.com/alexdeploy">@alexdeploy</a>. Built with <a href="https://nuxt.com/" target="_blank">Nuxt.js 3.0</a> and hosted with <a href="https://www.netlify.com/" target="_blank">Netlify</a>.
</p>
<div align="center">

[![Netlify Status](https://api.netlify.com/api/v1/badges/6fa55804-6799-419f-9222-359ba49c5e4c/deploy-status)](https://app.netlify.com/sites/developer-portfolio-v2/deploys)

</div>

<p align="center">
  <a href="" target="_blank">
    <img src="./public/images/demo-share.png" />
  </a>
</p>

## 🚨 Forking this repo

Feel free to fork this repository and make it your own! You can use it as a starting point for your own portfolio website. However, please note that the effort and time deserves to be recognized and *plagiarism is a bad practice*. If you use this project, we would greatly appreciate it if you give credits to the designer <a href="https://www.behance.net/darelova" target="_blank">@darelova</a> and the developer <a href="https://github.com/alexdeploy">@alexdeploy</a>, or linking <a href="https://github.com/alexdeploy/developer-portfolio-v2">this repo</a>.

Thanks 🤘 and enjoy it!

## 🛠 Installation

1. Clone the project to your local machine.

```sh
git clone https://github.com/alexdeploy/developer-portfolio-v2.git
```

2. Navigate to the project directory

```sh
cd developer-portfolio-v2
```

3. Install the required dependencies

```sh
yarn
```

4. Start the development server

```sh
yarn dev
```

5. The development server should now be running on <a href="http://localhost:3000/">http://localhost:3000/</a>


## ✒️ Customization

The portfolio template includes some default content, but you can easily customize it to fit your needs. Here are some of the things you can change:

* Update the `developer.json` file on root directory, which contains all the text for the project and the portfolio "user" information, including *projects*, *about-me*, *gists* (Ids) and *contact* info.

* Update the `nuxt.config.ts` file for meta tags of website and some additional config.

* Update the `public/pwa/manifest.json` file for PWA config.

* Change the styling and design of the website to match your personal style.

## 🚀 Building and Running for Production

1. Generate a full static production build

```sh
yarn build
```

2. Preview the site as it will appear once deployed.

```sh
yarn preview
```

## Contributions

If you find any bugs or have any suggestions, you can open an <a href="https://github.com/alexdeploy/developer-portfolio-v2/issues">issue</a>.

## License

This project is licensed under the MIT License. See the <a href="https://github.com/alexdeploy/developer-portfolio-v2/blob/main/LICENSE">LICENSE</a> file for more information.

================================================
FILE: app.config.ts
================================================
/*
* Nuxt 3 Config File
* https://nuxt.com/docs/getting-started/configuration#app-configuration
*/
export default defineAppConfig({
    title: 'Hello Nuxt',
    blog:{
      enabled: true,
    },
    theme: {
      dark: true,
      colors: {
        primary: '#ff0000'
      }
    }
  })

================================================
FILE: app.vue
================================================
<template>
  <MobileMenu/>
  <AppHeader/>
  <NuxtPage data-aos="fade-in"/>
  <AppFooter/>
</template>

<script>
import AOS from 'aos';
import 'aos/dist/aos.css'; // You can also use <link> for styles
export default {
  /**
   * * Watch for route changes
   * This event is triggered when the route changes.
   * @param {Object} to - Route object
   * @param {Object} from - Route object
   */
  watch: {
    $route(to, from) {
      console.log('De', from.fullPath, 'a', to.fullPath);
    }
  },
  mounted() {
    AOS.init({
        // Global settings:
        disable: false, // accepts following values: 'phone', 'tablet', 'mobile', boolean, expression or function
        startEvent: 'DOMContentLoaded', // name of the event dispatched on the document, that AOS should initialize on
        initClassName: 'aos-init', // class applied after initialization
        animatedClassName: 'aos-animate', // class applied on animation
        useClassNames: false, // if true, will add content of `data-aos` as classes on scroll
        disableMutationObserver: false, // disables automatic mutations' detections (advanced)
        debounceDelay: 50, // the delay on debounce used while resizing window (advanced)
        throttleDelay: 99, // the delay on throttle used while scrolling the page (advanced)
        
        // Settings that can be overridden on per-element basis, by `data-aos-*` attributes:
        offset: 120, // offset (in px) from the original trigger point
        delay: 0, // values from 0 to 3000, with step 50ms
        duration: 400, // values from 0 to 3000, with step 50ms
        easing: 'ease', // default easing for AOS animations
        once: false, // whether animation should happen only once - while scrolling down
        mirror: false, // whether elements should animate out while scrolling past them
        anchorPlacement: 'top-bottom', // defines which position of the element regarding to window should trigger the animation
    });
  }
}
</script>

================================================
FILE: assets/README.md
================================================
# `assets/` Directory

Nuxt uses **Vite** or **webpack** to build and bundle your application. The main function of these build tools is to process JavaScript files, but they can be extended through plugins (for Vite) or loaders (for webpack) to process other kind of assets, like stylesheets, fonts or SVG. This step transforms the original file mainly for performance or caching purposes (such as stylesheets minification or browser cache invalidation).

By convention, Nuxt uses the `assets/` directory to store these files but there is no auto-scan functionality for this directory, and you can use any other name for it.

In your application's code, you can reference a file located in the `assets/` directory by using the `~/assets/ path`.

## Example

For example, referencing an image file that will be processed if a build tool is configured to handle this file extension:

*app.vue*
````html
<template>
  <img src="~/assets/img/nuxt.png" alt="Discover Nuxt 3" />
</template>
````

>> Nuxt won't serve files in the `assets/` directory at a static URL like `/assets/my-file.png`. If you need a static URL, use the `public/` directory.

================================================
FILE: assets/tailwind.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

h3, h2 {
 /*  @apply font-fira_retina; */
  font-family: 'Fira Code Retina';
  font-size: 15px;
}

html {
    @apply bg-dark-background;
    height: stretch;
    width: stretch;
    margin: 30px;
}

body {
    height: stretch;
    width: stretch;
}

main.page {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  overflow: hidden;
}

#__nuxt {
  @apply bg-blue-background flex flex-col justify-between;
  border-radius: 7px;
  border: 1px solid #1E2D3D;
  height: 100%;
  width: 100%;
}

#section-content-title {
  width: 100%;
  min-height: 35px;
  border-bottom: 1px solid #1E2D3D;
  }

#section-content-title:hover {
  cursor: pointer;
}

#section-content-title-contact {
  @apply cursor-pointer;
  border-bottom: 1px solid #1E2D3D;

  height: 35px;
}

/* Mobile */
#mobile-page-title {
  display: flex;
  font-size: 14px;
  height: 70px;
  color: white;
  padding: 0 25px;
  align-items: center;
}

.section-arrow {
  margin-right: 10px;
  transition: 0.1s;
}


/* MOBILE AND TABLET (SM - LG) */
@media (max-width: 1024px) {

  html {
    margin: 15px;
    min-height: 100%;
    height: stretch;
    width: stretch;
  }

  #__nuxt {
    @apply bg-blue-background flex flex-col;
    justify-content: space-between;
    height: auto;
    min-height: stretch; /* This allows the page view for mobile to be full height when the content is less than the screen height. */
    width: 100%;
    width: auto;
  }

  #page-menu, #nav-logo, #filter-menu {
    border: 0px;
  }

  #page-menu {
    width: 100% !important;
  }

  #section-content-title {
  width: 100%;
  height: 30px;
  background-color: #1E2D3D;
  align-items: center;
  padding: 0 25px;
  }

  .submenu .title {
    display:flex;
    align-items: center;
    padding: 0 25px;
    width: 100%;
    height: 35px;
    background-color: #1E2D3D;
    margin-bottom: 3px
  }

  #left, #contact-menu {
    border-right: 0px;
  }

    /* contact */
  #contact-me #left {
    padding: 35px 25px;
  }

  /* footer */
  footer {
    height: 50px;
    min-height: 50px !important;
    font-size: 14px !important;
  }

  #social-icons > a {
    width: 55px !important;
  }

}

/* LG - XL */
@media (min-width: 1024px) {
  #page-menu, #nav-logo, #filter-menu {
    min-width: 275px !important;
    max-width: 275px !important;
  }

  #page-menu, #filter-menu {
    font-size: 14px;
  }

  #commented-text {
    font-size: 14px;
  }

  #mobile-page-title {
    display: none;
  }
  /* contact */
  #contact-me #left {
    padding: 50px 25px 0px 25px;
  }

  #contact-form {
      max-width: 220px;
      width: 100%;
  }

  main.page {
    flex-direction: row;
  }
}

/* 2XL */
@media (min-width: 1536px) {

}

/* 2K */
@media (min-width: 1920px) {


  #page-menu, #nav-logo, #filter-menu {
    min-width: 310px !important;
    max-width: 310px !important;
  }

  /* header */
  #navbar > nav {
    height: 50px !important;
    font-size: 14px !important;
  }

  /* footer */
  footer {
    height: 50px;
    min-height: 50px !important;
    font-size: 14px !important;
  }

  #social-icons > a {
    width: 55px !important;
  }

  #social-icons > a > svg, footer > a > svg {
    width: 1.5rem !important; /* 20px */
    height: 1.5rem !important; /* 20px */
  }

  /* about */
  #commented-text {
    font-size: 16px !important;
  }
  #page-menu, #filter-menu {
    font-size: 16px !important;
  }

  /* contact */
  #contact-me #left {
    padding: 100px 25px 0px 25px !important;
  }

  .form-content {
    padding: 100px 100px !important;
    font-size: 16px !important;
  }
}


/* 
 * Mobile min view to tablet max view (width: 0 to 1020)
*/
@media screen (max-width: 1020px) {

}

/* 
 * Tablet min view to desktop medium view (width: 1020 to 1920) (height: 0 to 1080)
*/
@media screen (min-width: 1020px) and (max-width: 1920px) and (max-height: 1080px) {

}

/* 
 * Desktop medium view to desktop max view (width: 1920 to infinte) (height: 1080 to infinite)
*/
@media screen (min-width: 1920px) and (min-height: 1080px) {

}

/* Borders */

.border-top {
  border-top: 1px solid #1E2D3D;
}

.border-right {
  border-right: 1px solid #1E2D3D;
}

.border-bot {
  border-bottom: 1px solid #1E2D3D;
}

.border-left {
  border-left: 1px solid #1E2D3D;
}

/* Scroll bar */

/* width */
::-webkit-scrollbar {
  width: 20px;
  border-left: 1px solid #1E2D3D;
  display: none;
}

/* Track */
::-webkit-scrollbar-track {
  background: transparent;
}

/* Handle */
::-webkit-scrollbar-thumb {
  background: #607B96;
}

/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
  background: #7B9BBB;
}

/* Fonts */

@font-face {
    font-family: "Fira Code Light";
    src: url("./fonts/fira-code/FiraCode-Light.ttf") format('truetype');
}

@font-face {
  font-family: "Fira Code Regular";
  src: url("./fonts/fira-code/FiraCode-Regular.ttf") format('truetype');
}

@font-face {
  font-family: "Fira Code Retina";
  src: url("./fonts/fira-code/FiraCode-Retina.ttf") format('truetype');
}

@font-face {
  font-family: "Fira Code Medium";
  src: url("./fonts/fira-code/FiraCode-Medium.ttf") format('truetype');
}

@font-face {
  font-family: "Fira Code SemiBold";
  src: url("./fonts/fira-code/FiraCode-SemiBold.ttf") format('truetype');
}

@font-face {
  font-family: "Fira Code Bold";
  src: url("./fonts/fira-code/FiraCode-Bold.ttf") format('truetype');
}

@font-face {
  font-family: "Fira Code Variable";
  src: url("./fonts/fira-code/FiraCode-Variable.ttf") format('truetype');
}

================================================
FILE: components/AppFooter.vue
================================================
<template>
    <footer class='flex md:justify-between border-top text-menu-text font-fira_retina'>

        <!-- social icons -->
        <div class="w-full flex justify-between md:justify-start">
            <span id="social-title" class="h-full flex justify-center items-center border-right px-5">
                find me in:
            </span>
            <div id="social-icons" class="flex">
                <NuxtLink :to="social.twitter.url + social.twitter.user" target="_blank" class="flex justify-center items-center">
                    <img src="/icons/social/twitter.svg" alt="twitter"/>
                </NuxtLink>
                <NuxtLink :to="social.facebook.url + social.facebook.user" target="_blank" class="flex justify-center items-center">
                    <img src="/icons/social/facebook.svg" alt="facebook"/>
                </NuxtLink>
                <NuxtLink :to="social.github.url + social.github.user" target="_blank" class="flex md:hidden justify-center items-center">
                <img src="/icons/social/github.svg" alt="github"/>
            </NuxtLink>
            </div>
        </div>

        <!-- github user -->
        <NuxtLink :to="social.github.url + social.github.user" target="_blank" class="hidden md:flex items-center px-5 border-left">
            @{{ social.github.user }}
            <img src="/icons/social/github.svg" alt="github"/>
        </NuxtLink>

    </footer>
</template>

<script setup>
import config from '~/developer.json';

const social = ref(config.contacts.social);
</script>

<script>
export default {
    name: 'AppFooter',
}
</script>

<style>

footer {
    height: 40px;
    min-height: 40px;
    font-size: 13px;
}

footer a:hover {
    background-color: #1e2d3d74;
}

#social-icons > a {
    border-right: 1px solid #1E2D3D;
    height: 100%;
    width: 50px;
 }

#social-icons > a > img {
    width: 1.25rem; /* 20px */
    height: 1.25rem; /* 20px */
    margin: auto;
    opacity: 0.4;
}

footer > a > img {
    width: 1.25rem; /* 20px */
    height: 1.25rem; /* 20px */
    margin-left: 0.5rem; /* 8px */
  }

#social-icons > a:hover img {
    opacity: 1;
}

@media (max-width: 768px) {

    #social-title {
        border-right: none;
    }

    #social-icons > a {
        border-right: none;
        border-left: 1px solid #1E2D3D;
    }

    #social-icons > a > img {
        width: 1.5rem; /* 20px */
        height: 1.5rem; /* 20px */
  }
}

</style>

================================================
FILE: components/AppHeader.vue
================================================
<template>
    <header id="navbar" class="w-full hidden lg:flex flex-col">
      <nav class="w-full flex justify-between border-bot">
        <github-corner url="https://github.com/alexdeploy/developer-portfolio-v2" />
          <div class="flex">
            <NuxtLink id="nav-logo" to="/">
              {{ config.logo_name }}
            </NuxtLink>

            <NuxtLink id="nav-link" to="/" :class="{ active: isActive('/') }">
              _hello
            </NuxtLink>
  
            <NuxtLink id="nav-link" to="/about-me" :class="{ active: isActive('/about-me') }">
              _about-me
            </NuxtLink>
  
            <NuxtLink id="nav-link" to="/projects" :class="{ active: isActive('/projects') }">
              _projects
            </NuxtLink>
          </div>

          <NuxtLink id="nav-link-contact" to="/contact-me" :class="{ active: isActive('/contact-me')}">
            _contact-me
          </NuxtLink>
      
      </nav>

    </header>

</template>

<script setup>
import GithubCorner from './GithubCorner.vue';
import config from '~/developer.json';

const isActive = (route) => {
  return route === route;
}

</script>

<script>
export default {
  name: 'AppHeader',
};
</script>

<style>

#nav-link {
  border-right: 1px solid #1E2D3D;
  @apply text-menu-text font-fira_retina px-6 h-full flex items-center;
}

#nav-link-contact {
  border-left: 1px solid #1E2D3D;
  @apply text-menu-text font-fira_retina px-6 h-full flex items-center;
}

#nav-link:hover, #nav-link-contact:hover {
  background-color: #1e2d3d74;
  color: white;
}

#nav-logo {
  border-right: 1px solid #1E2D3D;
  @apply text-menu-text font-fira_retina px-6 h-full flex items-center;
}

#nav-logo:hover {
  background-color: #1e2d3d74;
  color: white;
}

#nav-link.router-link-active, #nav-link-contact.router-link-active {
  border-bottom: 2px solid #FEA55F;
  color: white;
}

#nav-logo.router-link-active {
  border-right: 1px solid #1E2D3D;
  border-bottom: none;
  @apply text-menu-text;
}

#navbar > nav {
  height: 45px;
  font-size: 13px;
}

</style>



================================================
FILE: components/CommentedText.vue
================================================
<template>
  <div class="code-container flex font-fira_retina text-menu-text">
    <div class="line-numbers lg:flex flex-col w-32 hidden">

      <!-- line numbers and asteriscs -->
      <div v-for="n in lines" class="grid grid-cols-2 justify-end" :key="n">
        <span class="col-span-1 mr-3">{{ n }}</span>
        <div v-if="n == 1" class="col-span-1 flex justify-center">/**</div>
        <div class="col-span-1 flex justify-center" v-if="n > 1 && n < lines">*</div>
        <div class="col-span-1 flex justify-center pl-2" v-if="n == lines">*/</div>
      </div>
    </div>

    <!-- text -->
    <div class="text-container">
      <p v-html="text"></p>
    </div>
  </div>
</template>

<script>

export default {
  props: {
    text: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      lines: 0
    };
  },
  mounted() {
    this.updateLines();
    window.addEventListener("resize", this.updateLines);
    window.addEventListener("click", this.updateLines);
  },
  beforeDestroy() {
    window.removeEventListener("resize", this.updateLines);
    window.removeEventListener("click", this.updateLines);
  },
  methods: {
    updateLines() {
      const textContainer = this.$el.querySelector(".text-container");
      const style = window.getComputedStyle(textContainer);
      const fontSize = parseInt(style.fontSize);
      const lineHeight = parseInt(style.lineHeight);
      const maxHeight = textContainer.offsetHeight;
      this.lines = Math.ceil(maxHeight / lineHeight) + 1;
    }
  }
};
</script>

<style>
.code-container {
  display: flex;
  align-items: flex-start;
}

.line-numbers {
  text-align: right;
}

.text-container {
  width: 100%;
  padding-left: 10px;
  word-wrap: break-word;
}
</style>

================================================
FILE: components/ContactForm.vue
================================================
<template>
    <form id="contact-form" class="text-sm">
        <div class="flex flex-col">
            <label for="name-input" class="mb-3">_name:</label>
            <input type="text" id="name-input" name="name" :placeholder="name" class="p-2 mb-5 placeholder-slate-600" required>
        </div>
        <div class="flex flex-col">
            <label for="email-input" class="mb-3">_email:</label>
            <input type="email" id="email-input" name="email" :placeholder="email" class="p-2 mb-5 placeholder-slate-600" required>
        </div>
        <div class="flex flex-col">
            <label for="message-input" class="mb-3">_message:</label>
            <textarea id="message-input" name="message" :placeholder="message" class="placeholder-slate-600" required></textarea>
        </div>
        <button id="submit-button" type="submit" class="py-2 px-4">submit-message</button>
    </form>
</template>

<script>


export default {
    name: 'ContactForm',
    props: {
        name: {
            type: String,
            required: true
        },
        email: {
            type: String,
            required: true
        },
        message: {
            type: String,
            required: true
        }
    },
    mounted() {
        document.getElementById("contact-form").addEventListener("submit", function(event) {
            event.preventDefault();
            const name = document.querySelector('input[name="name"]').value;
            const email = document.querySelector('input[name="email"]').value;
            const message = document.querySelector('textarea[name="message"]').value;
            
            // Here the code to send the email
            
        });
    }
}
</script>

<style>

form {
    @apply font-fira_retina text-menu-text
}
input {
    background-color: #011221;
    border: 2px solid #1E2D3D;
    border-radius: 7px;
    
}
/* Change Autocomplete styles in Chrome*/
input:-webkit-autofill,
input:-webkit-autofill:hover, 
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
  -webkit-text-fill-color: rgb(190, 190, 190);
  transition: background-color 5000s ease-in-out 0s;
  border: 2px solid #607b96;
}

#message-input {
    background-color: #011221;
    border: 2px solid #1E2D3D;
    border-radius: 7px;
    resize: none;
    height: 150px;
    padding: 10px;
}

#submit-button {
    @apply font-fira_retina text-white text-sm;
    background-color: #1E2D3D;
    border-radius: 7px;
    margin-top: 20px;
    cursor: pointer;
}

#submit-button:hover {
    background-color: #263B50;
}

input:focus, #message-input:focus {
    outline: none;
    transition: none;
    border: 2px solid #607b96;
    box-shadow: #607b9669 0px 0px 0px 2px;
  }

#contact-form {
    max-width: 370px;
    width: 100%;
}

@media (max-width: 1920px) {
    #contact-form {
        max-width: 320px;
        max-height: 400px;
    }
    #submit-button {
        /* width: 100%; */
        font-size: 12px;
    }
    textarea {
        font-size: 13px;
        max-height: 130px !important;
    }
    input {
        font-size: 13px;
    }
}
</style>

================================================
FILE: components/FormContentCode.vue
================================================
<template>
    <div class="code-container flex font-fira_retina text-menu-text">
        <div class="line-numbers lg:flex flex-col w-16 hidden">

            <!-- line numbers and asteriscs -->
            <div v-for="n in lines" class="grid grid-cols-2 justify-end" :key="n">
                <span class="col-span-1 mr-3">{{ n }}</span>
            </div>
        </div>
        <div class="font-fira_retina text-white text-container">
            <p>
                <span class="tag">
                    const
                </span>
                <span class="tag-name">
                    button
                </span>
                =
                <span class="tag-name">
                    document.querySelector
                    <span class="text-menu-text">
                        (
                        <span class="text-codeline-link">
                            '#sendBtn'
                        </span>
                        );
                    </span>
                </span>

            </p>
            <br>
            <p class="text-menu-text">
                <span class="tag">
                    const
                </span>
                <span class="tag-name">
                    message
                </span>
                = {
                <br> &nbsp;&nbsp;
                <span id="name" class="tag-name">
                    name
                </span>
                :
                <span class="text-codeline-link">"</span>
                <span id="name-value" class="text-codeline-link">
                    {{ name }}
                </span>
                <span class="text-codeline-link">"</span>
                , <br> &nbsp;&nbsp;
                <span id="email" class="tag-name">
                    email
                </span>
                :
                <span class="text-codeline-link">"</span>
                <span id="email-value" class="text-codeline-link">
                    {{ email }}
                </span>
                <span class="text-codeline-link">"</span>
                , <br> &nbsp;&nbsp;
                <span id="message" class="tag-name">
                    message
                </span>
                :
                <span class="text-codeline-link">"</span>
                <span id="message-value" class="text-codeline-link">
                    {{ message }}
                </span>
                <span class="text-codeline-link">"</span>
                , <br> &nbsp;&nbsp;
                date:
                <span class="text-codeline-link">
                    "{{ date }}"
                </span>
                <br>
                }
            </p>
            <br>
            <p>
                <span class="tag-name">
                    button.addEventListener

                    <span class="text-menu-text">
                        (
                        <span class="text-codeline-link">
                            'click'
                        </span>
                        ), ()
                        <span class="tag">
                            =>
                        </span>
                        {
                        <br>
                    </span>
                    &nbsp;&nbsp;form.send
                    <span class="text-menu-text">(</span>
                    message
                    <span class="text-menu-text">); <br> })</span>
                </span>

            </p>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            date: new Date().toDateString(),
            lines: 0
        }
    },
    props: {
        name: String,
        email: String,
        message: String,
    },
    mounted() {
        this.updateLines();
        window.addEventListener("resize", this.updateLines);
        window.addEventListener("input", this.updateLines);
        window.addEventListener("click", this.updateLines);
    },
    beforeDestroy() {
        window.removeEventListener("resize", this.updateLines);
        window.removeEventListener("click", this.updateLines);
        window.addEventListener("input", this.updateLines);
    },
    methods: {
        updateLines() {
            const textContainer = this.$el.querySelector(".text-container");
            const style = window.getComputedStyle(textContainer);
            const fontSize = parseInt(style.fontSize);
            const lineHeight = parseInt(style.lineHeight);
            const maxHeight = textContainer.offsetHeight;
            this.lines = Math.ceil(maxHeight / lineHeight);
        }
    }
}
</script>

<style>
.tag {
    color: #C98BDF;
}

.tag-name {
    color: #819bff;
}

.arrow {
    color: #F8F8F8;
}

.code-container {
    display: flex;
    align-items: flex-start;
}

.line-numbers {
    text-align: right;
}

.text-container {
    width: 100%;
    padding-left: 0px;
    word-wrap: break-word;
}
</style>

================================================
FILE: components/GistSnippet.vue
================================================
<template>
    <div class="gist mb-5" v-if="dataFetched">
        
        <!-- head info -->
        <div class="flex justify-between my-2">

            <div class="flex">
                <!-- avatar -->
                <img :src="gist.owner.avatar_url" alt="" class="w-8 h-8 rounded-full mr-2">
    
                <!-- username & gist date info -->
                <div class="flex flex-col">
                    <a id="username" :href="'https://github.com/' + gist.owner.login" target="_blank" class="font-fira_bold text-purple-text text-xs pb-1 hover:cursor-pointer">
                        @{{ gist.owner.login }}
                    </a>
                    <p class="font-fira_retina text-xs text-menu-text">Created {{ monthsAgo }} months ago</p>
                </div>
            </div>

            <!-- details and stars -->
            <div class="flex text-menu-text font-fira_retina text-xs justify-self-end lg:mx-2">
                <div class="flex lg:mx-2 hover:cursor-pointer hover:text-white">
                    <img src="/icons/gist/comments.svg" alt="" class="w-4 h-4 mr-2">
                    <span @click="showComment(gist.id)">details</span>
                </div>
                <div class="hidden lg:flex hover:cursor-pointer hover:text-white">
                    <img src="/icons/gist/star.svg" alt="" class="w-4 h-4 mx-2">
                    <span class="">stars</span>
                </div>
            </div>
            
        </div>

        <highlightjs class="snippet-container" :code="content"/>
        <div :id="'comment' + gist.id" class="flex hidden justify-between text-menu-text font-fira_retina mt-4 pt-4 border-top">
            <p id="comment" v-if="comment" class="w-5/6">{{ comment }}</p>
            <p v-else class="w-5/6">No comments.</p>
            <img src="/icons/close.svg" alt="" class="hover:cursor-pointer" @click="showComment(gist.id)">
        </div>
    </div>
</template>

<style>
.snippet-container {
    background-color: #011221;
    padding: 5px;
    border-radius: 15px;
    border: 1px solid #1E2D3D;
    font-size: 12px;
    overflow-y: scroll;
    overflow-x: scroll;
    max-height: 220px;
}

.snippet-container pre {
    margin: 0;
    overflow: hidden;
    width: 100%;
    max-height: 220px;
}

.snippet-container code {
    white-space: pre-wrap;
    max-height: 220px;
    width: max-content;
    overflow: hidden;

}

.snippet-container::-webkit-scrollbar {
    display: none;  /* Safari and Chrome */
}

pre code.hljs{
    display:block;
    /* overflow-x:auto; */
    padding:1.5em
}

code.hljs{
    padding:3px 5px
}

#comment {
    font-size: 14px;
}

#username:hover {
    color: #5e6ef2;
}

/* #comment {
    
} */

.hljs{color:#85a9ce;background:#011221}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}

</style>

<script>

import hljsVuePlugin from "@highlightjs/vue-plugin";
import 'highlight.js/lib/common';

export default {
    name: 'GistSnippet',
    props: {
        id: {
            type: String,
            required: true
        }
    },
    data(){
        return {
            gist: null,
            monthsAgo: null,
            content: null,
            language: null,
            dataFetched: false,
            comment: null
        }
    },
    mounted(){
        fetch(`https://api.github.com/gists/${this.id}`)
            .then(response => response.json())
            .then(data => this.setValues(data))
            
    },
    methods: {
        async setValues(gist) {
        this.gist = gist
        this.monthsAgo = this.setMonths(gist.created_at)
        this.content = this.setSnippet(gist)
        this.language = Object.values(gist.files)[0].language
        this.dataFetched = true
        this.comment = await this.setComments(gist.comments_url)
        },
        setMonths(date) {
            let now = new Date()
            let gistDate = new Date(date)
            let diff = now.getTime() - gistDate.getTime()
            let days = Math.floor(diff / (1000 * 3600 * 24))
            let months = Math.floor(days / 30)
            return months
        },
        setSnippet(gist) {
            let snippet = Object.values(gist.files)[0].content // Object.values(gist.files)[0].filename.content
            return snippet
        },
        async setComments(comments_url){
            let response = await fetch(comments_url)
            let data = await response.json()
            try{
                let body = data[0].body
                return body
            } catch {
                console.log(`no comments found on ${comments_url}`)
            }
        },
        showComment(id) {
            let comment = document.getElementById('comment' + id)
            comment.classList.toggle('hidden')
        }
    },
    components: {
        highlightjs: hljsVuePlugin.component
    }
}
</script>

================================================
FILE: components/GithubCorner.vue
================================================
<template>
    <a :href="url" class="github-corner" target="_blank" aria-label="View source on Github">
        <svg width="80" height="80" viewBox="0 0 250 250" aria-hidden="true">
            <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
            <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
                fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
            <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
                fill="currentColor" class="octo-body"></path>
        </svg>
    </a>
</template>

<script>
export default {
    name: 'GithubCorner',
    props: {
        url: {
            type: String,
            default: ''
        }
    }
}
</script>

<style>
/* ----------------------------------------------
 * GitHub Corners
 * w: https://github.com/tholman/github-corners
 * ---------------------------------------------- */

 .github-corner {
  fill: #071511;
  color: #43D9AD;
  position: absolute;
  top: 0;
  border: 0;
  right: 0;
}

.github-corner:hover .octo-arm {
  animation: octocat-wave 560ms ease-in-out;
}

@keyframes octocat-wave {
  0%,
  100% {
    transform: rotate(0);
  }
  20%,
  60% {
    transform: rotate(-25deg);
  }
  40%,
  80% {
    transform: rotate(10deg);
  }
}

@media (max-width: 500px) {
  .github-corner {
    display: none;
  }
}
</style>

================================================
FILE: components/MobileMenu.vue
================================================
<template>
  <div id="mobile-menu" class="w-full z-10 lg:hidden">

    <!-- header -->
    <div id="mobile-header" class="w-full h-16 flex justify-between items-center">
      <NuxtLink class="text-menu-text font-fira_retina flex h-full items-center mx-5" to="/" @click="goHome()">
        {{ config.logoName }}
      </NuxtLink>
      <img src="/icons/burger.svg" alt="Open menu" v-if="!menuOpen" @click="toggleMobileMenu()"
        class="w-5 h-5 mx-5 my-auto" />
      <img src="/icons/burger-close.svg" alt="Closed menu" v-else @click="toggleMobileMenu()"
        name="icon-park-outline:close" class="w-5 h-5 mx-5 my-auto" />
    </div>

    <!-- mobile menu -->
    <div id="menu" class="bg-mobile-menu-blue z-10 hidden">
      <NuxtLink id="nav-link-mobile" to="/" :class="{ active: isActive('/') }" @click="toggleMobileMenu()">
        _hello
      </NuxtLink>

      <NuxtLink id="nav-link-mobile" to="/about-me" :class="{ active: isActive('/about-me') }"
        @click="toggleMobileMenu()">
        _about-me
      </NuxtLink>

      <NuxtLink id="nav-link-mobile" to="/projects" :class="{ active: isActive('/projects') }"
        @click="toggleMobileMenu()">
        _projects
      </NuxtLink>

      <NuxtLink id="nav-link-mobile" to="/contact-me" :class="{ active: isActive('/contact-me') }"
        @click="toggleMobileMenu()">
        _contact-me
      </NuxtLink>
    </div>

  </div>
</template>

<script setup>
import { ref } from 'vue';

import DevConfig from '~/developer.json';

const config = ref(DevConfig);

const menuOpen = ref(false);

function toggleMobileMenu(){
  menuOpen.value = !menuOpen.value;

  const menu = document.getElementById('menu');
  menu.classList.toggle('hidden');

  const page = document.getElementsByTagName('main')[0];
  // Hide / show section
  if (page.style.display === 'none') {
    page.style.display = 'flex';
  } else {
    page.style.display = 'none';
  }
};

function goHome(){
  const menu = document.getElementById('menu');
  if (!menu.classList.contains('hidden')) {
    menu.classList.toggle('hidden');
    document.getElementsByTagName('main')[0].style.display = 'flex';
    menuOpen.value = !menuOpen.value;
  }
};

const isActive = (route) => {
  return route === route.path;
};

</script>

<style>
#mobile-header {
  border-bottom: 1px solid #1E2D3D;
}

#nav-link-mobile {
  border-bottom: 1px solid #1E2D3D;
  @apply text-menu-text font-fira_retina px-6 py-4 flex items-center;
}

#nav-link-mobile.active {
  color: white
}
</style>

================================================
FILE: components/ProjectCard.vue
================================================
<template>
    <div id="project" :key="key" class="lg:mx-5">

        <span class="flex text-sm my-3">
            <h3 v-if="index == null" class="text-purplefy font-fira_bold mr-3">Project {{ key + 1 }}</h3>
            <h3 v-else class="text-purplefy font-fira_bold mr-3">Project {{ index + 1 }}</h3>
            <h4 class="font-fira_retina text-menu-text"> // {{ project.title }}</h4>
        </span>

        <div id="project-card" class="flex flex-col">
            <div id="window">
                <div class="absolute flex right-3 top-3">
                <img v-for="tech in project.tech" :key="tech" :src="'/icons/techs/filled/' + tech + '.svg'" alt="" class="w-6 h-6 mx-1 hover:opacity-75">
                </div>
                <img id="showcase" :src="project.img" alt="" class="">
            </div>

            <div class="pb-8 pt-6 px-6 border-top">
                <p class="text-menu-text font-fira_retina text-sm mb-5">
                {{ project.description }}
                </p>
                <a id="view-button" :href="project.url" target="_blank" class="text-white font-fira_retina py-2 px-4 w-fit text-xs rounded-lg">
                    view-project
                </a>
            </div>
        </div>
    </div>
</template>

<script setup>
const { project, key, index } = defineProps(['project', 'key', 'index'])
</script>

<style scoped>
#project {
  min-width: 400px;
  margin-bottom: 5px;
}

#project-card {
  border: 1px solid #1E2D3D;
  background-color: #011221;
  border-radius: 15px;
  max-width: 400px;
}

#window {
  max-height: 120px;
  position: relative;
  overflow: hidden;
}

#showcase {
  border-top-right-radius: 15px;
  border-top-left-radius: 15px;
}

@media (max-width: 768px) {
  #project {
    min-width: 100%;
  }
}

@media (min-width: 768px) {
  #project {
    width: 100%;
    min-width: 100%;
    padding-inline: 5px;
  }
}

@media (min-width: 1350px) {
  #project {
    width: 100%;
    min-width: 100%;
    padding-inline: 20px;
  }
}

</style>

================================================
FILE: components/README.md
================================================
# `components/` [Directory](https://nuxt.com/docs/getting-started/views#components)

Most components are reusable pieces of the user interface, like buttons and menus. In Nuxt, you can create these components in the `components/` directory, and they will be automatically available across your application without having to explicitly import them.

*app.vue*
````html
<template>
  <div>
    <h1>Welcome to the homepage</h1>
    <AppAlert>
      This is an auto-imported component.
    </AppAlert>
  </div>
</template>
````

*components/AppAlert.vue*
````html
<template>
  <span>
    <slot />
  </span>
</template>
````

================================================
FILE: components/SnakeGame.vue
================================================
<template>
    <div id="console">

      <!-- bolts -->
      <img id="corner" src="/icons/console/bolt-up-left.svg" alt="" class="absolute top-2 left-2 opacity-70">
      <img id="corner"  src="/icons/console/bolt-up-right.svg" alt="" class="absolute top-2 right-2 opacity-70">
      <img id="corner"  src="/icons/console/bolt-down-left.svg" alt="" class="absolute bottom-2 left-2 opacity-70">
      <img id="corner"  src="/icons/console/bolt-down-right.svg" alt="" class="absolute bottom-2 right-2 opacity-70">


      <!-- Game Screen -->
      <div id="game-screen" ref="gameScreen"></div>

      <button id="start-button" class="font-fira_retina" @click="startGame">start-game</button>

      <!-- Game Over -->
      <div id="game-over" class="hidden">
        <span class="font-fira_retina text-greenfy bg-bluefy-dark h-12 flex items-center justify-center">GAME OVER!</span>
        <button class="font-fira_retina text-menu-text text-sm flex items-center justify-center w-full py-6 hover:text-white" @click="startAgain">start-again</button>
      </div>

      <div id="congrats" class="hidden">
        <span class="font-fira_retina text-greenfy bg-bluefy-dark h-12 flex items-center justify-center">WELL DONE!</span>
        <button class="font-fira_retina text-menu-text text-sm flex items-center justify-center w-full py-6 hover:text-white" @click="startAgain">play-again</button>
      </div>

      <div id="console-menu" class="h-full flex flex-col items-end justify-between">

        <div>

        <div id="instructions" class="font-fira_retina text-sm text-white">
          <p>// use your keyboard</p>
          <p>// arrows to play</p>

          <div id="buttons" class="w-full flex flex-col items-center gap-1 pt-5">

              <button id="console-button" class="button-up" @click="move('up')">
                <img src="/icons/console/arrow-button.svg" alt="move up">
              </button>

              <div class="grid grid-cols-3 gap-1">
                <button id="console-button" class="button-left" @click="move('left')">
                  <img src="/icons/console/arrow-button.svg" alt="move left" class="-rotate-90">
                </button>

                <button id="console-button" class="button-down" @click="move('down')">
                  <img src="/icons/console/arrow-button.svg" alt="move down" class="rotate-180">
                </button>

                <button id="console-button" class="button-right" @click="move('right')">
                  <img src="/icons/console/arrow-button.svg" alt="move right" class="rotate-90">
                </button>
            </div>

          </div>
        </div>

        <!-- score board -->
        <div id="score-board" class="w-full flex flex-col pl-5">
          <p class="font-fira_retina text-white pt-5">// food left</p>

          <div id="score" class="grid grid-cols-5 gap-5 justify-items-center pt-5 w-fit">
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>
            <div class="food"></div>

          </div>
        </div>
      </div>
        <!-- skip -->
        <NuxtLink id="skip-btn" to="/about-me" class="font-fira_retina flex hover:bg-white/20">
          skip
        </NuxtLink>
        
      </div>
    </div>
    
  </template>
  
  <script>
  export default {
    data() {
      return {
        score: 0,
        gameInterval: null,
        gameStarted: false,
        gameOver: false,
        food: { x: 10, y: 5 },
        snake: [
        { x: 10, y: 12 },
        { x: 10, y: 13 },
        { x: 10, y: 14 },
        { x: 10, y: 15 },
        { x: 10, y: 16 },
        { x: 10, y: 17 },
        { x: 10, y: 18 },
        { x: 11, y: 18 },
        { x: 12, y: 18 },
        { x: 13, y: 18 },
        { x: 14, y: 18 },
        { x: 15, y: 18 },
        { x: 15, y: 19 },
        { x: 15, y: 20 },
        { x: 15, y: 21 },
        { x: 15, y: 22 },
        { x: 15, y: 23 },
        { x: 15, y: 24 },
      ],
        direction: "up",
      };
    },
    methods: {
      startGame() {

        // hide start button
        document.getElementById("start-button").style.display = "none";

        // start game
        this.gameStarted = true;
        this.gameInterval = setInterval(this.moveSnake, 50);
      },
      generateNewFood() {
        let newFood;
        do {
          newFood = {
            x: Math.floor(Math.random() * 24),
            y: Math.floor(Math.random() * 40)
          };
          // check if the new food is not on the snake
        } while (this.snake.some(segment => segment.x === newFood.x && segment.y === newFood.y));
        return newFood;
      },
      startAgain() {
        // Mostrar botón de start-game
        document.getElementById("start-button").style.display = "block";
        
        // Ocultar game over
        document.getElementById("game-over").style.display = "none";
        document.getElementById("congrats").style.display = "none";


        // reiniciar datos del juego
        this.gameStarted = false;
        this.gameOver = false;
        this.restartScore();
        this.food = {
          x: 10,
          y: 5
        };
        this.snake = [
            { x: 10, y: 12 },
            { x: 10, y: 13 },
            { x: 10, y: 14 },
            { x: 10, y: 15 },
            { x: 10, y: 16 },
            { x: 10, y: 17 },
            { x: 10, y: 18 },
            { x: 11, y: 18 },
            { x: 12, y: 18 },
            { x: 13, y: 18 },
            { x: 14, y: 18 },
            { x: 15, y: 18 },
            { x: 15, y: 19 },
            { x: 15, y: 20 },
            { x: 15, y: 21 },
            { x: 15, y: 22 },
            { x: 15, y: 23 },
            { x: 15, y: 24 },
          ],
        this.direction = "up";

        // limpiar intervalo de juego
        clearInterval(this.gameInterval);
        this.render();
      },
      // ... resto del código
      moveSnake() {
        let newX = this.snake[0].x;
        let newY = this.snake[0].y;

        switch (this.direction) {
          case "up":
            newY--;
            break;
          case "down":
            newY++;
            break;
          case "left":
            newX--;
            break;
          case "right":
            newX++;
            break;
        }

        if (
            newX >= 0 &&
            newX < 24 &&
            newY >= 0 &&
            newY < 40 &&
            !this.snake.find(
                snakeCell => snakeCell.x === newX && snakeCell.y === newY
            )
        ) {
          this.snake.unshift({ x: newX, y: newY });

          if (newX === this.food.x && newY === this.food.y) {
            this.score++;
            const scoreFoods = document.getElementsByClassName("food");
            scoreFoods[this.score - 1].style.opacity = 1;

            if(this.score === 10) {
              this.snake.unshift({ x: newX, y: newY });
              this.food = { x: null, y: null }
              clearInterval(this.gameInterval);
              document.getElementById('congrats').style.display = 'block'
              this.gameOver = true;
              this.gameStarted = false;
            } else {
              // generate new food
              this.food = this.generateNewFood();
            }
          } else {
            this.snake.pop();
          }
        } else {
          clearInterval(this.gameInterval);
          document.getElementById('game-over').style.display = 'block'
          this.gameStarted = false;
          this.gameOver = true;
        }
        this.render();
      },
      render() {
        let gameScreen = this.$refs.gameScreen;
        gameScreen.innerHTML = "";

        const cellSize = window.innerWidth > 1536 ? "10px" : "8px";

        for (let i = 0; i < 40; i++) {

          for (let j = 0; j < 24; j++) {

            let cell = document.createElement("div");
            cell.classList.add("cell");
            cell.style.width = cellSize
            cell.style.height = cellSize
            cell.style.display = "flex";
            cell.style.flexShrink = 0;
            cell.classList.add("black");

            /* 先渲染蛇身 */
            let snakeCell = this.snake.find(
                snakeCell => snakeCell.x === j && snakeCell.y === i
            );

            if (snakeCell) {
              cell.style.backgroundColor = "#43D9AD";
              cell.style.opacity = 1 - (this.snake.indexOf(snakeCell) / this.snake.length);
              cell.classList.add("green");

              /* 蛇頭的特殊樣式 */
              if (this.snake.indexOf(snakeCell) === 0) {
                let headRadius = "5px";
                if (this.direction === "up") {
                  cell.style.borderTopLeftRadius = headRadius;
                  cell.style.borderTopRightRadius = headRadius;
                }
                if (this.direction === "down") {
                  cell.style.borderBottomLeftRadius = headRadius;
                  cell.style.borderBottomRightRadius = headRadius;
                }
                if (this.direction === "left") {
                  cell.style.borderTopLeftRadius = headRadius;
                  cell.style.borderBottomLeftRadius = headRadius;
                }
                if (this.direction === "right") {
                  cell.style.borderTopRightRadius = headRadius;
                  cell.style.borderBottomRightRadius = headRadius;
                }
              }
            }

            /* 最後渲染食物,確保食物始終可見 */
            if (j === this.food.x && i === this.food.y && !snakeCell) {
              cell.style.backgroundColor = "#43D9AD";
              cell.style.borderRadius = "50%";
              cell.style.boxShadow = "0 0 10px #43D9AD";
            }

            gameScreen.appendChild(cell);
          }
        }
      },
    restartScore(){
      this.score = 0;
      const scoreFoods = document.getElementsByClassName("food");
      for (let i = 0; i < scoreFoods.length; i++) {
        scoreFoods[i].style.opacity = 0.3;
      }
    },
    move(direction){
      switch (direction) {
        case "up":
        if (this.direction !== "down") {
              this.direction = "up";
            }
          break;
        case "down":
        if (this.direction !== "up") {
              this.direction = "down";
            }
          break;
        case "left":
        if (this.direction !== "right") {
              this.direction = "left";
            }
          break;
        case "right":
        if (this.direction !== "left") {
              this.direction = "right";
            }
          break;
      }
    }
  },
  mounted() {
    document.addEventListener("keydown", event => {
      if (this.gameStarted) {
        switch (event.keyCode) {
          case 37:
            if (this.direction !== "right") {
              this.direction = "left";
            }
            break;
          case 38:
            if (this.direction !== "down") {
              this.direction = "up";
            }
            break;
          case 39:
            if (this.direction !== "left") {
              this.direction = "right";
            }
            break;
          case 40:
            if (this.direction !== "up") {
              this.direction = "down";
            }
            break;
        }
      } else {
        switch (event.keyCode) {
          case 32:
            if(this.gameOver){
              this.startAgain();
            }else {
              this.startGame();
            }
            break;
        }
      }
    });

    /* window.innerWidth < 1536 ? cellSize = 8 : cellSize = 10; */
    /* this.food = {
      x: Math.floor(Math.random() * 24),
      y: Math.floor(Math.random() * 40)
    }; */
    window.onresize = () => {
      this.render();
    };

    this.render();


  }
};
</script>

<style>
#console {
    width: 530px;
    height: 475px;
    border: 1px solid black;
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: linear-gradient(to bottom, rgba(35, 123, 109, 1), rgba(67, 217, 173, 0.13));
    border-radius: 10px;
    padding: 30px;
    position: relative;

}

#game-screen {
    width: 240px;
    height: 400px;
    border-radius: 10px;
    background-color: rgba(1, 22, 39, 0.84);
    display: flex;
    flex-wrap: wrap;
    box-shadow: inset 0 0 10px #00000071;
}

#start-button {
  padding-inline: 16px;
  padding-block: 8px;
  border-radius: 10px;
  border: 1px solid black;
  background-color: #FEA55F;
  color: black;
  cursor: pointer;
  position: absolute;
  bottom: 20%;
  left: 17%;
  font-size: 0.875rem; /* 14px */
  line-height: 1.25rem; /* 20px */
}

#start-button:hover {
  background-color: rgb(255, 178, 119);
}

#console-menu{
  height: 400px;
}

#console-button {
  background-color: #010C15;
  border-radius: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 50px;
  height: 30px;
}

#console-button:hover {
  background-color: #010c15d8;
  box-shadow: #43D9AD 0 0 10px;
}

#instructions {
  background-color: rgba(1, 20, 35, 0.19);
  border-radius: 7px;
  padding: 10px;
}

.food {
  background-color: #43D9AD;
  border-radius: 50%;
  box-shadow: 0 0 10px #43D9AD;
  width: 8px;
  height: 8px;
  opacity: 0.3;
}

#game-over, #congrats {
  position: absolute;
  bottom: 12%;
  color: #43D9AD;
  width: 240px;
}

#game-over, #congrats > span {
  font-size: 1.5rem; /* 24px */
  line-height: 2rem; /* 32px */
}

#corner {
  width: 24px;
  height: 24px;
}

#skip-btn{
  font-size: 14px;
  color: white;
  padding-inline: 16px;
  padding-block: 8px;
  border: 2px solid white;
  border-radius: 0.5rem; /* 8px */
}


@media (min-width: 1024px) and (max-width: 1536px) {
  #game-screen {
    width: 192px;
    height: 320px;
  }

  #console {
    width: 420px;
    height: 370px;
    padding: 24px;

  }

  #start-button {
  padding-inline: 12px;
  padding-block: 6px;
  border-radius: 8px;
  bottom: 20%;
  left: 17%;
  font-size: 0.75rem; /* 14px */
  line-height: 1rem; /* 20px */
}

  #console-menu{
  height: 320px;
}

#instructions {
  font-size: 12px;
}

#console-button {
  width: 40px;
  height: 25px;
  border-radius: 6px;
}

#score-board {
  font-size: 12px;
}

.food {
  width: 6px;
  height: 6px;
}

#game-over, #congrats {
  position: absolute;
  bottom: 10%;
  color: #43D9AD;
  width: 192px;
}

#game-over, #congrats > span {
  font-size: 1.125rem; /* 18px */
  line-height: 1.75rem; /* 28px */
}

#corner {
  width: 20px;
  height: 20px;
}

#skip-btn{
  font-size: 12px;
  padding-inline: 12px;
  padding-block: 6px;
  border: 2px solid white;
  border-radius: 0.5rem; /* 8px */
}
}
</style>

================================================
FILE: content/README.md
================================================
# `content/` [Directory](https://nuxt.com/docs/guide/directory-structure/content)

The Nuxt Content module reads the `content/` directory in your project and parses .md, .yml, .csv and .json files to create a file-based CMS for your application.

1. Render your content with built-in components.
2. Query your content with a MongoDB-like API.
3. Use your Vue components in Markdown files with the MDC syntax.
4. Automatically generate your navigation.

## Installation

Install the `@nuxt/content` module in your project:

````
yarn add --dev @nuxt/content
````

Then, add `@nuxt/content` to the `modules` section of `nuxt.config.ts`.

````ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/content'
  ],
  content: {
    // https://content.nuxtjs.org/api/configuration
  }
})

````

## Create Content

Place your markdown files inside the `content/` directory in the root directory of your project:
``content/index.md`

````
# Hello Content
````

The module automatically loads and parses them.

## Documentation

Head over to https://content.nuxtjs.org to learn more about the Content module features, such as how to build queries and use Vue components in your Markdown files with the MDC syntax.

================================================
FILE: developer.json
================================================
{
    "name": "Micheal Weaver",
    "logo_name": "micheal-weaver",
    "role": "Front-end developer",
    "about": {
        "sections": {
            "professional-info": {
                "title": "professional-info",
                "icon": "icons/info-professional.svg",
                "info": {
                    "experience": {
                        "title": "experience",
                        "description": "<br>Over the past 5 years, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br><br> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
                    },
                    "hard-skills": {
                        "title": "hard-skills",
                        "description": "<br>As a front-end developer, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br><br> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
                    },
                    "soft-skills": {
                        "title": "soft-skills",
                        "description": "<br>I bring more than Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br><br> Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
                    }
                }
            },
            "personal-info": {
                "title": "personal-info",
                "icon": "icons/info-personal.svg",
                "info": { 
                    "bio": {
                        "title": "bio",
                        "description": "<br> About me <br> I have 5 years of experience in web development lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. <br><br> Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat  nulla pariatur. Excepteur sint occaecat  officia deserunt mollit anim id est laborum."
                    },
                    "interests": {
                        "title": "interests",
                        "description": "<br>I am constantly learning and lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
                    },
                    "education": {
                        "title": "education",
                        "description": "<br>I have always been passionate about lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br><br> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
                        "files": {
                            "high-school": "I have been in 'Las viñas'...",
                            "university": "The university..."
                        }
                    }
                }
            },
            "hobbies-info": {
                "title": "hobbies-info",
                "icon": "icons/info-hobbies.svg",
                "info": {
                    "sports": {
                        "title": "sports",
                        "description": "<br>I am an avid sports enthusiast and lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. <br> Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
                    },
                    "favorite-games": {
                        "title": "favorite-games",
                        "description": "<br>I am a passionate gamer with Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <br><br> Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
                    }
                }
            }
        }
    },
    "contacts": {
        "direct": {
            "title": "contacts",
            "sources": {
                "email": "user@gmail.com",
                "phone": "+3598246359"
            }
        },
        "social": {
            "github": {
                "title": "Github profile",
                "url": "https://github.com/",
                "user": "username"
            },
            "facebook": {
                "title": "Facebook profile",
                "url": "https://facebook.com/",
                "user": "username"
            },
            "twitter": {
                "title": "Twitter account",
                "url": "https://twitter.com/",
                "user": "username"
            }
        },
        "find_me_also_in": {
            "title": "find-me-also-in",
            "sources": {
                "youtube": {
                    "title": "YouTube channel",
                    "url": "https://www.youtube.com/",
                    "user": "username"
                },
                "gurushots": {
                    "title": "GuruShots profile",
                    "url": "https://gurushots.com/",
                    "user": "username"
                },
                "instagram": {
                    "title": "Instagram account",
                    "url": "https://instagram.com/",
                    "user": "username"
                },
                "twitch": {
                    "title": "Twitch profile",
                    "url": "https://twitch.com/",
                    "user": "username"
                }
            }
        }
    },
    "gists": {
        "1": "83861a67e377633ee8368df01ee3a355",
        "2": "694c1f32332788a2ac7f37b09e5aa40e"
    },
    "projects": {
        "1": {
            "title": "_ui-animations",
            "description": "Duis aute irure dolor in velit esse cillum dolore.",
            "img": "/images/projects/ui-animations2.png",
            "tech": ["Flutter"],
            "url": "https://github.com/"
        },
        "2": {
            "title": "_ai-resources",
            "description": "Duis aute irure dolor in velit esse cillum dolore.",
            "img": "/images/projects/ai-resources.png",
            "tech": ["Gatsby"],
            "url": "https://github.com/"
        },
        "3": {
            "title": "_worldmap",
            "description": "Duis aute irure dolor in velit esse cillum dolore.",
            "img": "/images/projects/worldmap.png",
            "tech": ["Angular"],
            "url": "https://github.com/"
        },
        "4": {
            "title": "_ui-animations",
            "description": "Duis aute irure dolor in velit esse cillum dolore.",
            "img": "/images/projects/ui-animations.png",
            "tech": ["React"],
            "url": "https://github.com/"
        },
        "5": {
            "title": "_tetris-game",
            "description": "Duis aute irure dolor in velit esse cillum dolore.",
            "img": "/images/projects/tetris-game.png",
            "tech": ["React"],
            "url": "https://github.com/"
        },
        "6": {
            "title": "_ethereum",
            "description": "Duis aute irure dolor in velit esse cillum dolore.",
            "img": "/images/projects/ethereum.png",
            "tech": ["Vue"],
            "url": "https://github.com/"
        }
    }
}

================================================
FILE: layouts/README.md
================================================
# `layouts/` [Directory](https://nuxt.com/docs/getting-started/views#layouts)

Layouts are wrappers around pages that contain a common User Interface for several pages, such as a header and footer display. Layouts are Vue files using `<slot />` components to display the page content. The `layouts/default.vue` file will be used by default. Custom layouts can be set as part of your page metadata.

>> If you only have a single layout in your application, we recommend using app.vue with the `<NuxtPage />` component instead.

*layouts/default.vue*
````html
<template>
  <div>
    <AppHeader />
    <slot />
    <AppFooter />
  </div>
</template>
````

*pages/index.vue*
````html
<template>
  <div>
    <h1>Welcome to the homepage</h1>
    <AppAlert>
      This is an auto-imported component
    </AppAlert>
  </div>
</template>
````

*pages/about.vue*
````html
<template>
  <section>
    <p>This page will be displayed at the /about route.</p>
  </section>
</template>
````

If you want to create more layouts and learn how to use them in your pages, find more information in the [Layouts section](https://nuxt.com/docs/guide/directory-structure/layouts).

================================================
FILE: layouts/default.vue
================================================
<template>
    <div>
      <AppHeader />
      <slot />
      <AppFooter />
    </div>
  </template>

================================================
FILE: middleware/README.md
================================================
# `middleware/` [Directory](https://nuxt.com/docs/guide/directory-structure/middleware)

Nuxt provides a customizable **route middleware** framework you can use throughout your application, ideal for extracting code that you want to run before navigating to a particular route.

>> Route middleware run within the Vue part of your Nuxt app. Despite the similar name, they are completely different from server middleware, which are run in the Nitro server part of your app.

There are three kinds of route middleware:

1. Anonymous (or inline) route middleware, which are defined directly in the pages where they are used.

2. Named route middleware, which are placed in the `middleware/` directory and will be automatically loaded via asynchronous import when used on a page. (Note: The route middleware name is normalized to kebab-case, so `someMiddleware` becomes `some-middleware`.)

3. Global route middleware, which are placed in the `middleware/` directory (with a `.global` suffix) and will be automatically run on every route change.

The first two kinds of route middleware can be defined in `definePageMeta`.

[Here](https://nuxt.com/docs/guide/directory-structure/middleware) is more info about middleware.

================================================
FILE: nuxt.config.ts
================================================
const config = require('./developer.json')
const siteTitle = `${config.name} | ${config.role}`


/*
 * Nuxt 3 Config File
 Usage: https://nuxt.com/docs/api/configuration/nuxt-config
 */
export default defineNuxtConfig({
  compatibilityDate: '2025-02-28',
  devtools: { enabled: true },
  /**
   * * App Config
   * app config: https://nuxt.com/docs/api/configuration/nuxt-config#app
   * head config: https://nuxt.com/docs/api/configuration/nuxt-config#head
   * meta config: https://nuxt.com/docs/getting-started/seo-meta
   * pageTransition config: https://nuxt.com/docs/getting-started/transitions#transitions
   * TODO: Add more meta tags for SEO
   * TODO: Add tags for social media sharing
   * TODO: Migrate apple-touch-icon config to manifest.json
   */
  app: {
    head: {
      htmlAttrs: {
        lang: 'en', // App language
      },
      title: siteTitle, // App window nav title
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'description', name: 'description', content: 'A awesome developer portfolio design.' },
        { hid: 'og:title', property: 'og:title', content: siteTitle },
        { hid: 'og:description', property: 'og:description', content: 'A awesome developer portfolio design.' },
        { hid: 'og:image', property: 'og:image', content: 'demo-share.jpg' },
        { hid: 'og:url', property: 'og:url', content: 'https://developer-portfolio-v1.netlify.app/' },
        { name: 'theme-color', content: '#010C15' },
        // ...
      ],
      link: [
        { rel: 'manifest', href: 'pwa/manifest.json' },
        { rel: 'apple-touch-icon', href: 'pwa/icons/apple-touch-icon.png' },
      ],
    },
  },

  /**
   * * Nuxt 3 Modules
   * Official modules: https://nuxt.com/modules
   */
  modules: [
    '@nuxtjs/tailwindcss',
  ],

  components: {
    dirs: [
      '~/components',
    ],
  },
  
  /**
   * * Tailwind CSS Config
   * Options: https://tailwindcss.nuxt.dev/getting-started/options/
   * Docs: https://tailwindcss.nuxt.dev
   */
  tailwindcss: {
    cssPath: '~/assets/tailwind.css',
    configPath: 'tailwind.config',
    exposeConfig: true, // true to resolve the tailwind config in runtime. https://tailwindcss.nuxt.dev/getting-started/options/#exposeconfig
    injectPosition: 0,
    viewer: false,
  },

  /**
   * * Runtime Config (Environment Variables)
   * Usage: https://nuxt.com/docs/guide/going-further/runtime-config
   */
  runtimeConfig: {
    // The private keys which are only available server-side
    apiSecret: '123',
    // Keys within public are also exposed client-side
    public: {
      apiBase: '/api',

    }
  }
})

================================================
FILE: package.json
================================================
{
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "test": "vitest"
  },
  "devDependencies": {
    "nuxt": "^3.15.4",
    "vitest": "^3.0.7"
  },
  "dependencies": {
    "@highlightjs/vue-plugin": "highlightjs/vue-plugin",
    "@nuxtjs/tailwindcss": "^6.13.1",
    "aos": "^2.3.4",
    "highlight.js": "^11.7.0"
  }
}


================================================
FILE: pages/README.md
================================================
# `pages/` [Directory](https://nuxt.com/docs/getting-started/views#pages)

Pages represent views use for each specific route pattern. Every file in the `pages/` directory represents a different route displaying its content.

To use pages, create pages/index.vue file and add `<NuxtPage />` component to the *app.vue* (or remove *app.vue* for default entry). You can now create more pages and their corresponding routes by adding new files in the `pages/` directory.

*pages/index.vue*
````html
<template>
  <div>
    <h1>Welcome to the homepage</h1>
    <AppAlert>
      This is an auto-imported component
    </AppAlert>
  </div>
</template>
````

*pages/about.vue*
````html
<template>
  <section>
    <p>This page will be displayed at the /about route.</p>
  </section>
</template>

````

================================================
FILE: pages/about-me.vue
================================================
<template>
  <main v-if="!loading" id="about-me" class="page">

    <div id="mobile-page-title">
      <h2>_about-me</h2>
    </div>

    <div id="page-menu" class="w-full flex">

      <!-- DESKTOP section icons -->
      <div id="sections">
        <div id="section-icon" v-for="section in config.about.sections" :key="section.title" :class="{ active: isSectionActive(section.title)}">
          <img :id="'section-icon-' + section.title" :src="section.icon" :alt="section.title + '-section'" @click="focusCurrentSection(section)">
        </div>
      </div>

      <!-- focused section content -->
      <div id="section-content" class="hidden lg:block w-full h-full border-right">

        <!-- title -->
        <div id="section-content-title" class="hidden lg:flex items-center min-w-full">
          <img id="section-arrow-menu" src="/icons/arrow.svg" alt="" class="section-arrow mx-3 open">
          <p v-html="config.about.sections[currentSection]?.title" class="font-fira_regular text-white text-sm"></p>
        </div>

        <!-- folders -->
        <div>
          <div v-for="(folder, key, index) in config.about.sections[currentSection]?.info" :key="key" class="grid grid-cols-2 items-center my-2 font-fira_regular text-menu-text" @click="focusCurrentFolder(folder)">
            <div class="flex col-span-2 hover:text-white hover:cursor-pointer">
              <img id="diple" src="/icons/diple.svg" alt="" :class="{ open: isOpen(folder.title)}">
              <img :src="'/icons/folder' + (index+1) + '.svg'" alt="" class="mr-3">
              <p :id="folder.title" v-html="key" :class="{ active: isActive(folder.title)}"></p>
            </div>
            <div v-if="folder.files !== undefined" class="col-span-2">
              <div v-for="(file, key) in folder.files" :key="key" class="hover:text-white hover:cursor-pointer flex my-2">
                <img src="/icons/markdown.svg" alt="" class="ml-8 mr-3"/>
                <p >{{ key }}</p>
              </div> 
            </div>
          </div>
        </div>

        <!-- contact -->
        <div id="section-content-title-contact" class="flex items-center min-w-full border-top">
          <img id="section-arrow-menu" src="/icons/arrow.svg" alt="" class="section-arrow mx-3 open">
          <p v-html="config.contacts.direct.title" class="font-fira_regular text-white text-sm"></p>
        </div>
        <div id="contact-sources" class="hidden lg:flex lg:flex-col my-2">
          <div v-for="(source, key) in config.contacts.direct.sources" :key="key" class="flex items-center mb-2">
            <img :src="'/icons/' + key + '.svg'" alt="" class="mx-4">
            <a v-html="source" href="/" class="font-fira_retina text-menu-text hover:text-white"></a>
          </div>
        </div>

      </div>

      <!-- mobile -->
      <div id="section-content" class="lg:hidden w-full font-fira_regular">

        <div v-for="section in config.about.sections" :key="section.title">
          
          <!-- section title (mobile) -->
          <div :key="section.title" :src="section.icon" id="section-content-title" class="flex lg:hidden mb-1" @click="focusCurrentSection(section)">
            <img src="/icons/arrow.svg" :id="'section-arrow-' + section.title" alt="" class="section-arrow">
            <p v-html="section.title" class=" text-white text-sm"></p>
          </div>

          <!-- folders -->
          <div :id="'folders-' + section.title" class="hidden"> <!-- <div :id="'folders-' + section.title" :class="currentSection == section.title ? 'block' : 'hidden'"> -->
            <div v-for="(folder, key, index) in config.about.sections[section.title]?.info" :key="key" class="grid grid-cols-2 items-center my-2 font-fira_regular text-menu-text hover:text-white hover:cursor-pointer" @click="focusCurrentFolder(folder)">
              <div class="flex col-span-2">
                <img id="diple" src="/icons/diple.svg">
                <img :src="'icons/folder' + (index+1) + '.svg'" alt="" class="mr-3">
                <p :id="folder.title" v-html="key" :class="{ active: isActive(folder.title)}"></p>
              </div>
              <div v-if="folder.files !== undefined" class="col-span-2">
                <div v-for="(file, key) in folder.files" :key="key" class="hover:text-white hover:cursor-pointer flex my-2">
                  <img src="/icons/markdown.svg" alt="" class="ml-8 mr-3"/>
                  <p >{{ key }}</p>
                </div>
                
              </div>
            </div>
          </div>
          
        </div>

        <!-- section content title -->
        <div id="section-content-title" class="flex items-center min-w-full" @click="showContacts()">
          <img src="/icons/arrow.svg" alt="" id="section-arrow" class="section-arrow">
          <p v-html="config.contacts.direct.title" class="font-fira_regular text-white text-sm"></p>
        </div>

        <!-- section content folders -->
        <div id="contacts" class="hidden">
          <div v-for="(source, key) in config.contacts.direct.sources" :key="key" class="flex items-center my-2">
            <img :src="'/icons/' + key + '.svg'" alt="">
            <a v-html="source" href="/" class="font-fira_retina text-menu-text hover:text-white ml-4"></a>
          </div>
        </div>

      </div>

    </div>
    <!-- MENU END -->

    <!-- content -->
    <div class="flex flex-col lg:grid lg:grid-cols-2 h-full w-full">
      
      <div id="left" class="w-full flex flex-col border-right">
        
        <!-- windows tab desktop -->
        <div class="tab-height w-full hidden lg:flex border-bot items-center">
          <div class="flex items-center border-right h-full">
            <p v-html="config.about.sections[currentSection]?.title" class="font-fira_regular text-menu-text text-sm px-3"></p>
            <img src="/icons/close.svg" alt="" class="mx-3">
          </div>
        </div>

        <!-- windows tab mobile -->
        <div id="tab-mobile" class="flex lg:hidden font-fira_retina">
            <span class="text-white">// </span>
            <h3 v-html="config.about.sections[currentSection]?.title" class="text-white px-2"></h3>
            <span class="text-menu-text"> / </span>
            <h3 v-html="config.about.sections[currentSection]?.info[folder].title" class="text-menu-text pl-2"></h3>
        </div>
        
        <!-- text -->
        <div id="commented-text" class="flex h-full w-full lg:border-right overflow-hidden">

          <div class="w-full h-full ml-5 mr-10 lg:my-5 overflow-scroll">
              <CommentedText :text="config.about.sections[currentSection]?.info[folder].description" />
          </div>
          
          <!-- scroll bar -->
          <div id="scroll-bar" class="h-full border-left hidden lg:flex justify-center py-1">
            <div id="scroll">
          </div>

        </div>

      </div>
      
    </div>

    <div id="right" class="max-w-full flex flex-col">
        
      <!-- windows tab -->
      <div class="tab-height w-full h-full hidden lg:flex border-bot items-center">

      </div>

      <!-- windows tab mobile -->
      <div class="tab-height w-full h-full flex-none lg:hidden items-center">

      </div>

        <div id="gists-content" class="flex">
        
          <div id="gists" class="flex flex-col lg:px-6 lg:py-4 w-full overflow-hidden">
            <!-- title -->
            <h3 class="text-white lg:text-menu-text mb-4 text-sm">// Code snippet showcase:</h3>

            <div class="flex flex-col overflow-scroll">
              <!-- snippets -->
              <GistSnippet data-aos="fade-down" v-for="(gist, key) in config.gists" :key="key" :id="gist" />
            </div>
          </div>

          <!-- scroll bar -->
          <div id="scroll-bar" class="h-full border-left hidden lg:flex justify-center py-1">
            <div id="scroll"></div>
          </div>
        </div>
      </div>
    </div>
  </main>
</template>

<style>

#sections {
  width: 5rem; /* 80px */
  height: 100%;
  display: none;
  border-right: 1px solid #1E2D3D;
}

/* LG */
@media (min-width: 1024px) {
  #sections {
    display: block;
  }
}

#section-icon {
  @apply my-6 hover:cursor-pointer flex justify-center;
  opacity: 0.4;
}

#section-icon.active {
  opacity: 1;
}

#section-icon:hover {
  opacity: 1;
}

.tab-height {
  min-height: 35px;
  max-height: 35px;
}

#tab-mobile {
  padding: 25px 20px 0px 25px;
  align-items: flex-end;
}

#scroll-bar{
  width: 20px;
}

#scroll {
  width: 14px;
  height: 7px;
  background-color: #607B96;
}

#diple {
  @apply mx-3 w-2 max-w-fit;
}

.open {
  transform: rotate(90deg);
}

.active {
  color:white;
}

#right, #left {
  height: 100%;
  overflow: hidden;
}

#gists-content {
  height: 100%;
  overflow: hidden;
}

@media (max-width: 1024px) {
  #gists-content {
    height: 100%;
    padding: 0px 25px;
    overflow: hidden;
  }

  #about {
  min-height: stretch;
}
}

.section-arrow {
  transition: 0.1s;
}

#section-content #contacts {
  padding: 0px 25px;
}

</style>

<script>
import DevConfig from '~/developer.json';
export default {
  data() {
    return {
      currentSection: 'personal-info',
      folder: 'bio',
      loading: true,
    }
  },
  /**
   * In setup we can define the data we want to use in the component before the component is created.
   */
  setup() {
    return {
      config: DevConfig
    }
  },
  computed: {
    // Set active class to current page link
    isActive() {
      return folder => this.folder === folder;
    },
    isSectionActive() {
      return section => this.currentSection === section;
    },
    isOpen() {
      return folder => this.folder === folder;
    },
  },
  methods: {
    focusCurrentSection(section) {
      this.currentSection = section.title
      this.folder = Object.keys(section.info)[0]

      document.getElementById('folders-' + section.title).classList.toggle('hidden') // show folders
      document.getElementById('section-arrow-' + section.title).classList.toggle('rotate-90'); // rotate arrow
    },
    focusCurrentFolder(folder) {
      this.folder = folder.title
      // handle if folder belongs to the current section. It happens when you click on a folder from a different section in mobile view.
      this.currentSection = this.config.about.sections[this.currentSection].info[folder.title] ? this.currentSection : Object.keys(this.config.about.sections).find(section => this.config.about.sections[section].info[folder.title])
    },
    /**
     * TODO: Hay que crear un método para que cuando se haga click en un folder, se muestren los archivos que contiene. Y si se hace click en un archivo, se muestre el contenido del archivo.
     * TODO:  Además de girar el icono del diple.
     */
    toggleFiles() {
      document.getElementById('file-' + this.folder).classList.toggle('hidden')
    },
    /* Mobile */
    showContacts() {
      document.getElementById('contacts').classList.toggle('hidden')
      document.getElementById('section-arrow').classList.toggle('rotate-90'); // rotate arrow
    },
  },
  mounted(){
    this.loading = false
  }
}
</script>

================================================
FILE: pages/contact-me.vue
================================================
<template>
    <main id="contact-me" class="page">

        <div id="mobile-page-title">
            <h2>_contact-me</h2>
        </div>

        <div id="page-menu" class="w-full h-full flex flex-col border-right">

            <!-- contacts -->
            <div id="contacts" class="submenu">
                <div class="title" @click="open('contacts')">
                    <img class="arrow" src="/icons/arrow.svg" alt="">
                    <h3>
                        contacts
                    </h3>
                </div>
                <div id="links">
                    <div v-for="(source, key) in contact.direct.sources" :key="key" class="link">
                        <img :src="'/icons/' + key + '.svg'" alt="">
                        <a v-html="source" href="/" class="font-fira_retina text-menu-text hover:text-white"></a>
                    </div>
                </div>
            </div>

            <!-- find me also in -->
            <div id="find-me-in" class="submenu border-top">
                <div class="title" @click="open('find-me-in')">
                    <img class="arrow" src="/icons/arrow.svg" alt="">
                    <h3>
                        find-me-also-in
                    </h3>
                </div>
                <div id="links">
                    <div v-for="(source, key) in contact.find_me_also_in.sources" :key="key" class="link">
                        <img src="/icons/link.svg" alt="">
                        <a :href="source.url + source.user" class="font-fira_retina text-menu-text hover:text-white" target="_blank">{{ source.title }}</a>
                    </div>
                </div>
            </div>

        </div>
            
        <div class="flex flex-col w-full">

        <!-- windows tab -->
        <div class="tab-height w-full hidden lg:flex border-right border-bot items-center">

                <div class="flex items-center border-right h-full">
                    <p class="font-fira_regular text-menu-text text-sm px-3">contacts</p>
                    <img src="/icons/close.svg" alt="" class="m-3">
                </div>

            </div>

            <!-- main -->
            <div class="flex lg:grid lg:grid-cols-2 h-full w-full">
        
                <div id="left" class="h-full w-full flex flex-col border-right items-center">
                    
                    <ContactForm :name="name" :email="email" :message="message" />

                </div>

                <div id="right" class="h-full w-full hidden lg:flex">
                    
                    <div class="form-content">
                        <FormContentCode :name="name" :email="email" :message="message" />
                    </div>
                    <!-- scroll bar -->
                    <div id="scroll-bar" class="h-full border-left flex justify-center py-1">
                        <div id="scroll"></div>
                    </div>
                
                </div>
            </div>

        </div>
    </main>
</template>

<script>
import DevConfig from '~/developer.json';
export default {
    data() {
        return {
            name: '',
            email: '',
            message: '',
        }
    },
    setup() {
        return {
            contact: DevConfig.contacts,
        }
    },
    methods: {
        open(elementId) {
            const element = document.getElementById(elementId);
            const arrow = element.querySelector('.arrow');
            const links = element.querySelector('#links');

            if (links.style.display === 'block') {
                links.style.display = 'none';
                arrow.style.transform = 'rotate(0deg)';
            } else {
                links.style.display = 'block';
                arrow.style.transform = 'rotate(90deg)';
            }
        }
    },
    mounted(){

        const nameInput = document.getElementById('name-input');
        const emailInput = document.getElementById('email-input');
        const messageInput = document.getElementById('message-input');

        nameInput.addEventListener('input', (event) => {
            const nameValue = document.getElementById('name-value')
            nameValue.innerHTML = event.target.value;
        })

        emailInput.addEventListener('input', (event) => {
            const emailValue = document.getElementById('email-value')
            emailValue.innerHTML = event.target.value;
        })

        messageInput.addEventListener('input', (event) => {
            const messageValue = document.getElementById('message-value')
            messageValue.innerHTML = event.target.value;
        })

        /**
         * * Close all submenus
         * ! This is a temporary solution.
         * ! This is needed because when the page is loaded, height style on #links are not applied.
         */
        const links = document.getElementsByClassName('submenu');
        for (let i = 0; i < links.length; i++) {
            if(window.innerWidth > 1024){ 
                links[i].querySelector("#links").style.display = "block";
                links[i].querySelector(".arrow").style.transform = "rotate(90deg)";
            } else {
                links[i].querySelector("#links").style.display = "none";
            }
        }
    },
}
</script>

<style>

.arrow {
    transition: 0.1s;
    margin-right: 10px;
    width: 9px;
    height: 9px;
}

.submenu {
    display: flex;
    flex-direction: column;
}

.submenu .title h3 {
    @apply font-fira_regular;
    color: white;
    font-size: 16px;
}

.link {
    display: flex;
    align-items: center;
    padding: 4px 25px;
}

.link img {
    width: 16px;
    height: 16px;
    margin-right: 10px;
}

#links {
    padding: 10px 0px;
}

.form-content {
    padding: 75px 50px 0px 75px;
    width: 100%;
    height: 100%;
    overflow-y: auto;
    font-size: 15px;
}
@media (min-width: 1024px) {
    
    .submenu .title {
        display: flex;
        align-items: center;
        border-bottom: 1px solid #1E2D3D;
        padding: 0px 25px;
        height: 35px;
        padding: 0px 25px;
    }
    .submenu .title:hover {
        cursor: pointer;
    }
    .submenu .title h3 {
        font-size: 14px;
    }
}

</style>

================================================
FILE: pages/index.vue
================================================
<template>
  	<main v-if="!loading" id="hello">

    	<!-- gradients -->
    	<div class="css-blurry-gradient-blue"></div>
    	<div class="css-blurry-gradient-green"></div>

		<section class="hero">
		
			<div class="head">
				<span>
					Hi all, I am
				</span>
				<h1>{{ config.name }}</h1>
        <span class="diple flex">
          >&nbsp;
				<h2 class="line-1 anim-typewriter max-w-fit"> {{ config.role }} </h2>
        </span>
			</div>

			<div id="info">
				<span class="action">
					// complete the game to continue
				</span>
				<span :class="{hide: isMobile}">
					// you can also see it on my Github page
				</span>
				<span :class="{hide: !isMobile}">
					// find my profile on Github:
				</span>
				<p class="code">
					<span class="identifier">
						const
					</span>
					<span class="variable-name">
						githubLink
					</span>
					<span class="operator">
						=
					</span>
					<a class="string" :href="'https://github.com/' + config.contacts.social.github.user">
						"https://github.com/{{ config.contacts.social.github.user }}"
					</a>
				</p>
			</div>
		</section>

		<section data-aos="fade-up" class="game" v-if="!isMobile">
			<SnakeGame />
		</section>

	</main>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import DevConfig from '~/developer.json';

const config = ref(DevConfig)

const isMobile = ref(false)
const loading = ref(false)

onMounted(() => {
  if (window.innerWidth <= 1024) isMobile.value = true
  window.addEventListener('resize', handleResize)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize)
})

function handleResize() {
  if (window.innerWidth <= 1024) {
    isMobile.value = true
  } else {
    isMobile.value = false
  }
}
</script>

<style scoped>
#hello {
  display: flex;
  height: 100%;
  width: 100%;
  flex: 1 1 auto;
  padding-left: 275px;
  overflow: hidden;
}
.hero {
	width: 100%;
	justify-content: center;
	
}
.game {
	display: flex;
	flex-direction: column;
	width: 100%;
	height: 100%;
	justify-content: center;
/* 	align-items: center; */
	z-index: 20;
}

#hello .hero {
	display: flex;
	flex-direction: column;
	/* display: grid;
	grid-template-columns: repeat(12, minmax(0, 1fr)); */
	margin: 0rem;
}


#hello .head span {
  font-size: 18px;
  line-height: 1;
  color: #E5E9F0;
  font-family: 'Fira Code Retina';
}

#hello .head h1 {
  font-size: 58px;
  line-height: 1;
  color: #E5E9F0;
  font-family: 'Fira Code Regular';
  padding-top: 1rem; /* 16px */
  padding-bottom: 1rem; /* 16px */
}

#hello .head h2, #hello .head .diple {
  font-size: 32px;
  line-height: 1;
  color: #4D5BCE;
  font-family: 'Fira Code Retina';
}

.head {
  padding-bottom: 3rem;
}

#info {
	display: flex;
	flex-direction: column;
}

#info > span {
  font-size: 14px;
  line-height: 1;
  color: #8da9c6;
  font-family: 'Fira Code Retina';
  padding-bottom: 1rem; /* 16px */
}

.code {
  font-family: 'Fira Code Medium';
  color: #E5E9F0;
}

.code .identifier {
  color: #6172ff;
}

.code .variable-name {
  color: #43D9AD;
}

.code .operator {
  color: white;
}

.code .string {
  color: #E99287;
  text-decoration-line: underline;
  text-underline-offset: 4px;
}

#info {
	padding-block: 2.5rem;
}

#info .action {
	display: flex
}

.hide {
  display: none;
}

.css-blurry-gradient-blue {
  position: fixed;
  bottom: 25%;
  right: 5%;
  width: 300px;
  height: 300px;
	border-radius: 0% 0% 50% 50%;
  rotate: 10deg;
	filter: blur(70px);
  background: radial-gradient(circle at 50% 50%,rgba(77, 91, 206, 1), rgba(76, 0, 255, 0));
  opacity: 0.5;
  z-index: 10;
}

.css-blurry-gradient-green {
  position: absolute;
  top: 20%;
  right: 30%;
  width: 300px;
  height: 300px;
	border-radius: 0% 50% 0% 50%;
	filter: blur(70px);
  background: radial-gradient(circle at 50% 50%,rgba(67, 217, 173, 1), rgba(76, 0, 255, 0));
  opacity: 0.5;
  z-index: 10;
}

#info {
  font-size: 14px;
}

/* Typewrite Animation */

.line-1 {
    width: fit-content;
    border-right: 3px solid rgba(255,255,255,.75);
    white-space: nowrap;
    overflow: hidden;
    padding-right: 2px;
}

.anim-typewriter{
    animation: typewriter 3.5s steps(40) 1s 1 normal both,
    blinkTextCursor 800ms steps(40) infinite normal;
}

@keyframes typewriter{
  from{width: 0;}
  to{width: 100%;}
}

@keyframes blinkTextCursor{
  from{border-right-color: rgba(255,255,255,.75);}
  to{border-right-color: transparent;}
}


/* mobile */
@media (max-width: 768px) {

	#hello {
		padding-left: 0;
	}

	#hello .hero {
		display: flex;
		flex-direction: column;
		justify-content: space-between;
		margin: 1.75rem; /* 28px */
	}
	.head {
		padding-top: 4rem; /* 40px */
	}

	#hello .head h2, #hello .head .diple {
		font-size: 20px;
		color: #43D9AD;
	}
	
	#info .action {
		display: none;
	}

}

/* tablet */
@media (min-width: 768px) and (max-width: 1024px) {
	#hello {
		padding-left: 0;
	}
	#hello .hero {
		display: flex;
		flex-direction: column;
		justify-content: center;
		margin: 1.75rem; /* 28px */
	}
	.head {
		padding-top: 4rem; /* 40px */
	}

}

@media (min-width: 1024px) and (max-width: 1320px) {
	#hello {
		padding-left: 135px;
	}
}


/* LG */

@media (min-width: 1024px) {

  .css-blurry-gradient-blue {
    position: fixed;
    bottom: 10%;
    right: 10%;
    width: 500px;
    height: 500px;
    opacity: 0.7;
    border-radius: 100% 50% 100% 0%;
  }

  .css-blurry-gradient-green {
    position: fixed;
    top: 10%;
    right: 35%;
    filter: blur(100px);
    rotate: 10deg;
    width: 400px;
    height: 400px;
    opacity: 0.5;
    border-radius: 100% 0% 0% 0%;
    rotate: 20deg;
  }
}

@media (min-width: 1920px){
	#hello {
		padding-left: 310px;
	}
	#hello .head h1 {
		font-size: 62px;
	}
}

</style>


================================================
FILE: pages/projects.vue
================================================
<template>
  <main class="flex flex-col flex-auto lg:flex-row overflow-hidden">

    <div id="mobile-page-title">
      <h2>_projects</h2>
    </div>

    <!-- section title (mobile) -->
    <div id="section-content-title" class="flex lg:hidden" @click="showFilters = !showFilters">
      <img :class="showFilters ? 'section-arrow rotate-90' : 'section-arrow'" src="/icons/arrow.svg">
      <span class="font-fira_regular text-white text-sm">projects</span>
    </div>

    <div v-if="showFilters" id="filter-menu"
      class="w-full flex-col border-right font-fira_regular text-menu-text lg:flex">
      <!-- title -->
      <div id="section-content-title" class="hidden lg:flex items-center min-w-full">
        <img id="section-arrow-menu" src="/icons/arrow.svg" alt="" class="section-arrow mx-3">
        <p class="font-fira_regular text-white text-sm">projects</p>
      </div>

      <!-- filter menu -->
      <nav id="filters" class="w-full flex-col">

        <div v-for="tech in techs" :key="tech" class="flex items-center py-2">
          <input type="checkbox" :id="tech" @click="filterProjects(tech)">
          <img :id="'icon-tech-' + tech" :src="'/icons/techs/' + tech + '.svg'" alt="" class="tech-icon w-5 h-5 mx-4">
          <label :for="tech" :id="'title-tech-' + tech">{{ tech }}</label>
        </div>
      </nav>
    </div>

    <!-- content -->

    <div class="flex flex-col w-full overflow-hidden">

      <!-- windows tab -->
      <div class="tab-height w-full hidden lg:flex border-bot items-center">
        <div class="flex items-center border-right h-full">
          <p v-for="filter in filters" :key="filter" class="font-fira_regular text-menu-text text-sm px-3">{{ filter }};
          </p>
          <img src="/icons/close.svg" alt="" class="m-3">
        </div>
      </div>

      <!-- windows tab mobile -->
      <div id="tab" class="flex lg:hidden items-center">
        <span class="text-white"> // </span>
        <p class="font-fira_regular text-white text-sm px-3">projects</p>
        <span class="text-menu-text"> / </span>
        <p v-for="filter in filters" :key="filter" class="font-fira_regular text-menu-text text-sm px-3">{{ filter }};
        </p>
      </div>

      <!-- projects -->
      <div id="projects-case" class="grid grid-cols-1 lg:grid-cols-2 max-w-full h-full overflow-scroll lg:self-center">
        <div id="not-found"
          class="hidden flex flex-col font-fira_retina text-menu-text my-5 h-full justify-center items-center">
          <span class="flex justify-center text-4xl pb-3">
            X__X
          </span>
          <span class="text-white flex justify-center text-xl">
            No matching projects
          </span>
          <span class="flex justify-center">
            for these technologies
          </span>
        </div>

        <project-card v-for="(project, index) in projects" :index="index" :project="project" />

      </div>
    </div>
  </main>
</template>

<script setup>
import { ref } from 'vue'
import DevConfig from '~/developer.json';

const config = ref(DevConfig)

const techs = ['React', 'HTML', 'CSS', 'Vue', 'Angular', 'Gatsby', 'Flutter']
const filters = ref(['all'])
const showFilters = ref(true)
const projects = ref(config.value.projects)

function filterProjects(tech) {
  document.getElementById('icon-tech-' + tech).classList.toggle('active')
  document.getElementById('title-tech-' + tech).classList.toggle('active')

  const check = document.getElementById(tech)
  if (check.checked) {
    filters.value = filters.value.filter((item) => item !== 'all')
    filters.value.push(tech)
  } else {
    filters.value = filters.value.filter((item) => item !== tech)
    filters.value.length === 0 ? filters.value.push('all') : null
  }
  filters.value[0] == 'all' ? projects.value = config.value.projects : projects.value = filterProjectsBy(filters.value)

  if (projects.value.length === 0) {
    document.getElementById('projects-case').classList.remove('grid')
    document.getElementById('not-found').classList.remove('hidden')
  } else {
    document.getElementById('projects-case').classList.add('grid')
    document.getElementById('not-found').classList.add('hidden')
  }
}

function filterProjectsBy(filters) {
  const projectArray = Object.values(config.value.projects)
  return projectArray.filter(project => {
    return filters.some(filter => project.tech.includes(filter))
  })
}
</script>

<style>
#filters {
  padding: 10px 25px;
}

#tab {
  padding: 25px 25px 5px;
  flex-wrap: wrap;
}

.tech-icon {
  opacity: 0.4;
}

.tech-icon.active {
  opacity: 1;
}

#title-tech.active {
  color: white;
}

#view-button {
  background-color: #1C2B3A;
}

#view-button:hover {
  background-color: #263B50;
}

input[type="checkbox"] {
  appearance: none;
  background-color: transparent;
  width: 1.15em;
  height: 1.15em;
  border: 2px solid currentColor;
  border-radius: 0.15em;
  margin-top: 1px;
}

input[type="checkbox"]:checked {
  background-color: currentColor;
  background-image: url("data:image/svg+xml;utf8,<svg width='13' height='10' viewBox='0 0 13 10' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M5.38587 7.2802L11.9718 0.693573L12.9856 1.70668L5.38587 9.30641L0.826172 4.74671L1.83928 3.73361L5.38587 7.2802Z' fill='white'/></svg>");
  background-repeat: no-repeat;
  background-position: center;
}

input[type="checkbox"]:checked:hover {
  box-shadow: #607b968b 0px 0px 0px 2px;
}

input[type="checkbox"]:not(:checked) {
  border-color: currentColor;
}

input[type="checkbox"]:hover {
  cursor: pointer;
  background-color: currentColor;
  background-image: url("data:image/svg+xml;utf8,<svg width='13' height='10' viewBox='0 0 13 10' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M5.38587 7.2802L11.9718 0.693573L12.9856 1.70668L5.38587 9.30641L0.826172 4.74671L1.83928 3.73361L5.38587 7.2802Z' fill='white'/></svg>");
  background-repeat: no-repeat;
  background-position: center;
  box-shadow: #607b968b 0px 0px 0px 2px;
}

input[type="checkbox"]:hover:not(:checked) {
  cursor: pointer;
  background-color: rgba(0, 0, 0, 0.1);
  background-image: none;
  box-shadow: #607b968b 0px 0px 0px 2px;
}

input[type="checkbox"]:focus {
  box-shadow: none;
}

@media (max-width: 768px) {
  #projects-case {
    padding: 0px 25px 40px;
  }

}

@media (min-width: 768px) {
  #projects-case {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    padding: 50px 50px 40px;
  }
}

@media (min-width: 1350px) {
  #projects-case {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    padding: 50px 80px 40px;
    /* padding: 100px 100px 40px; */
  }
}

@keyframes animateToBottom {
  from {
    transform: translate3d(0, -200px, 0);
  }

  to {
    transform: translate3d(0, 10px, 0);
  }
}
</style>


================================================
FILE: public/README.md
================================================
# `public/` Directory

The `public/` directory is used as a public server for static assets publicly available at a defined URL of your application.

You can get a file in the `public/` directory from your application's code or from a browser by the root URL `/`.

## Example

For example, referencing an image file in the public/img/ directory, available at the static URL `/img/nuxt.png`:

````html
<template>
  <img src="/img/nuxt.png" alt="Discover Nuxt 3" />
</template>
````

================================================
FILE: public/pwa/manifest.json
================================================
{
    "name": "Portfolio for Developers Concept v2",
    "short_name": "Developer Portfolio",
    "description": "A awesome portfolio template for developers",
    "start_url": "https://developer-portfolio-v1.netlify.app/",
    "display": "standalone",
    "orientation": "portrait",
    "background_color": "#010C15",
    "theme_color": "#010C15",
    "scope": "https://developer-portfolio-v1.netlify.app/",
    "icons": [{
        "src": "icons/icon48.png",
        "sizes": "48x48",
        "type": "image/png"
      }, {
        "src": "icons/icon72.png",
        "sizes": "72x72",
        "type": "image/png"
      }, {
        "src": "icons/icon96.png",
        "sizes": "96x96",
        "type": "image/png"
      }, {
        "src": "icons/icon144.png",
        "sizes": "144x144",
        "type": "image/png"
      }, {
        "src": "icons/icon168.png",
        "sizes": "168x168",
        "type": "image/png"
      }, {
        "src": "icons/icon192.png",
        "sizes": "192x192",
        "type": "image/png"
      }, {
        "src": "icons/icon512.png",
        "sizes": "512x512",
        "type": "image/png",
        "purpose": "any maskable"
      }]
}

================================================
FILE: public/worker.js
================================================
console.log('Service worker file loaded.')

/**
 * * FETCH event
 * ? It is triggered when the browser is requesting a resource
 * TODO: Add the logic to handle the request (investigate best practices and options)
 */
 self.addEventListener("fetch", (event) => {
/*     event
      .respondWith(
        console.log('fetching!'),
      ); */
  });

/**
 * * INSTALL event
 * * It is triggered when the service worker has been installed
 * ? It is the best place to cache the app
 * TODO: Add the resources to cache
 */
self.addEventListener("install", (event) => {

    console.log('Installing service worker...')
    /*
    event.waitUntil(
    // ...
    ); 
    */
});

================================================
FILE: tailwind.config.js
================================================
module.exports = {
    theme: {
        extend: {
            colors: {
                'gray-dark': '#121212',
                'glitch-green': '#00ff00',
                'glitch-rose': '#f0f',
                'glitch-blue': '#0ff',
                'neon-blue-xs': '#08cff6',
                'neon-blue-s': '#194262',
                'neon-blue-m': '#0e2535',
                'neon-blue-l': '#05131e',
                'neon-blue-xl': '#020204',
                'dark-background': '#010C15',
                'menu-text': '#85a5c4',
                'blue-background': '#011627',
                'hello-name': '#E5E9F0',
                'purple-text': '#81a0fd',
                'hello-gray': '#84a6c8',
                'codeline-link': '#E99287',
                'codeline-tag': '#4D5BCE',
                'codeline-name': '#43D9AD',
                'mobile-menu-blue': '#011627',
                'placeholder-gray': '#465E77',
                'greenfy': '#43D9AD',
                'bluefy-dark': '#011627',
                'purplefy': '#799ffb',

            }
        },
        fontFamily: {
            fira_light: "Fira Code Light",
            fira_regular: "Fira Code Regular",
            fira_retina: "Fira Code Retina",
            fira_medium: "Fira Code Medium",
            fira_semibold: "Fira Code SemiBold",
            fira_bold: "Fira Code Bold",
            fira_variable: "Fira Code Variable",
          }
    }
}

================================================
FILE: test/github.test.js
================================================
import { beforeEach, describe, expect, it } from 'vitest'
import { getGistById } from '../utils/github-api.js'

describe('Testing Github Gists API', () => {

    const gistId1 = '799fe15f4b75706242b10d978e935067';

    // do this before each test
    beforeEach(() => {

    })

    it('Response status is successfull: 200', async () => {
            
            const getGithubGist = async (gistId) => await fetch('https://api.github.com/gists/' + gistId)
            const response = await getGithubGist(gistId1)
    
            expect(response.status).toBe(200)
    })

    /**
     * Check if the response is a JSON
     */
    it('The response is a JSON', async () => {

        const gist = await getGistById(gistId1)

        function isJson(str) {
            try {
                JSON.parse(str);
            } catch (e) {
                return true;
            }
            return false;
        }
        expect(isJson(gist)).toBe(true)
  })

})

================================================
FILE: test/global.test.js
================================================
import { describe, expect, it } from 'vitest'

describe('Testing test', () => {
  it('works!', async () => {
    expect(true).toBe(true)
  })

})

================================================
FILE: tsconfig.json
================================================
{
  // https://nuxt.com/docs/guide/concepts/typescript
  "extends": "./.nuxt/tsconfig.json"
}


================================================
FILE: utils/README.md
================================================
# `utils/` [Directory](https://nuxt.com/docs/guide/directory-structure/utils)

Nuxt 3 uses the `utils/` directory to automatically import helper functions and other utilities throughout your application using auto-imports!

>> The main purpose of the `utils/` directory is to allow a semantic distinction between your Vue composables and other auto-imported utility functions.The way utils/ auto-imports work and are scanned is identical to the [composables/ directory](https://nuxt.com/docs/guide/directory-structure/composables). You can see examples and more information about how they work in that section of the docs.

================================================
FILE: utils/github-api.js
================================================

/**
 * Get a gist from Github API by gist ID
 * @param {*} gistId 
 * @returns 
 */
const getGistById = async (gistId) => {
    const response = await fetch('https://api.github.com/gists/' + gistId)
    return response.json()
}

module.exports = {
    getGistById
}
Download .txt
gitextract_xra8ecv9/

├── .gitignore
├── LICENSE
├── README.md
├── app.config.ts
├── app.vue
├── assets/
│   ├── README.md
│   └── tailwind.css
├── components/
│   ├── AppFooter.vue
│   ├── AppHeader.vue
│   ├── CommentedText.vue
│   ├── ContactForm.vue
│   ├── FormContentCode.vue
│   ├── GistSnippet.vue
│   ├── GithubCorner.vue
│   ├── MobileMenu.vue
│   ├── ProjectCard.vue
│   ├── README.md
│   └── SnakeGame.vue
├── content/
│   └── README.md
├── developer.json
├── layouts/
│   ├── README.md
│   └── default.vue
├── middleware/
│   └── README.md
├── nuxt.config.ts
├── package.json
├── pages/
│   ├── README.md
│   ├── about-me.vue
│   ├── contact-me.vue
│   ├── index.vue
│   └── projects.vue
├── public/
│   ├── README.md
│   ├── pwa/
│   │   └── manifest.json
│   └── worker.js
├── tailwind.config.js
├── test/
│   ├── github.test.js
│   └── global.test.js
├── tsconfig.json
└── utils/
    ├── README.md
    └── github-api.js
Download .txt
SYMBOL INDEX (1 symbols across 1 files)

FILE: test/github.test.js
  function isJson (line 28) | function isJson(str) {
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (116K chars).
[
  {
    "path": ".gitignore",
    "chars": 58,
    "preview": "node_modules\n*.log*\n.nuxt\n.nitro\n.cache\n.output\n.env\ndist\n"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2022 Álex Rueda\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 3046,
    "preview": "<h1 align=\"center\">\n  developer-portfolio-v2\n</h1>\n<p align=\"center\">\n  The first open source version of <a href=\"https:"
  },
  {
    "path": "app.config.ts",
    "chars": 288,
    "preview": "/*\n* Nuxt 3 Config File\n* https://nuxt.com/docs/getting-started/configuration#app-configuration\n*/\nexport default define"
  },
  {
    "path": "app.vue",
    "chars": 1989,
    "preview": "<template>\n  <MobileMenu/>\n  <AppHeader/>\n  <NuxtPage data-aos=\"fade-in\"/>\n  <AppFooter/>\n</template>\n\n<script>\nimport A"
  },
  {
    "path": "assets/README.md",
    "chars": 1142,
    "preview": "# `assets/` Directory\n\nNuxt uses **Vite** or **webpack** to build and bundle your application. The main function of thes"
  },
  {
    "path": "assets/tailwind.css",
    "chars": 5528,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nh3, h2 {\n /*  @apply font-fira_retina; */\n  font-family: 'Fi"
  },
  {
    "path": "components/AppFooter.vue",
    "chars": 2440,
    "preview": "<template>\n    <footer class='flex md:justify-between border-top text-menu-text font-fira_retina'>\n\n        <!-- social "
  },
  {
    "path": "components/AppHeader.vue",
    "chars": 2068,
    "preview": "<template>\n    <header id=\"navbar\" class=\"w-full hidden lg:flex flex-col\">\n      <nav class=\"w-full flex justify-between"
  },
  {
    "path": "components/CommentedText.vue",
    "chars": 1751,
    "preview": "<template>\n  <div class=\"code-container flex font-fira_retina text-menu-text\">\n    <div class=\"line-numbers lg:flex flex"
  },
  {
    "path": "components/ContactForm.vue",
    "chars": 3247,
    "preview": "<template>\n    <form id=\"contact-form\" class=\"text-sm\">\n        <div class=\"flex flex-col\">\n            <label for=\"name"
  },
  {
    "path": "components/FormContentCode.vue",
    "chars": 4936,
    "preview": "<template>\n    <div class=\"code-container flex font-fira_retina text-menu-text\">\n        <div class=\"line-numbers lg:fle"
  },
  {
    "path": "components/GistSnippet.vue",
    "chars": 5741,
    "preview": "<template>\n    <div class=\"gist mb-5\" v-if=\"dataFetched\">\n        \n        <!-- head info -->\n        <div class=\"flex j"
  },
  {
    "path": "components/GithubCorner.vue",
    "chars": 2006,
    "preview": "<template>\n    <a :href=\"url\" class=\"github-corner\" target=\"_blank\" aria-label=\"View source on Github\">\n        <svg wid"
  },
  {
    "path": "components/MobileMenu.vue",
    "chars": 2502,
    "preview": "<template>\n  <div id=\"mobile-menu\" class=\"w-full z-10 lg:hidden\">\n\n    <!-- header -->\n    <div id=\"mobile-header\" class"
  },
  {
    "path": "components/ProjectCard.vue",
    "chars": 2006,
    "preview": "<template>\n    <div id=\"project\" :key=\"key\" class=\"lg:mx-5\">\n\n        <span class=\"flex text-sm my-3\">\n            <h3 v"
  },
  {
    "path": "components/README.md",
    "chars": 618,
    "preview": "# `components/` [Directory](https://nuxt.com/docs/getting-started/views#components)\n\nMost components are reusable pieces"
  },
  {
    "path": "components/SnakeGame.vue",
    "chars": 14866,
    "preview": "<template>\n    <div id=\"console\">\n\n      <!-- bolts -->\n      <img id=\"corner\" src=\"/icons/console/bolt-up-left.svg\" alt"
  },
  {
    "path": "content/README.md",
    "chars": 1209,
    "preview": "# `content/` [Directory](https://nuxt.com/docs/guide/directory-structure/content)\n\nThe Nuxt Content module reads the `co"
  },
  {
    "path": "developer.json",
    "chars": 9673,
    "preview": "{\n    \"name\": \"Micheal Weaver\",\n    \"logo_name\": \"micheal-weaver\",\n    \"role\": \"Front-end developer\",\n    \"about\": {\n   "
  },
  {
    "path": "layouts/README.md",
    "chars": 1156,
    "preview": "# `layouts/` [Directory](https://nuxt.com/docs/getting-started/views#layouts)\n\nLayouts are wrappers around pages that co"
  },
  {
    "path": "layouts/default.vue",
    "chars": 100,
    "preview": "<template>\n    <div>\n      <AppHeader />\n      <slot />\n      <AppFooter />\n    </div>\n  </template>"
  },
  {
    "path": "middleware/README.md",
    "chars": 1217,
    "preview": "# `middleware/` [Directory](https://nuxt.com/docs/guide/directory-structure/middleware)\n\nNuxt provides a customizable **"
  },
  {
    "path": "nuxt.config.ts",
    "chars": 2685,
    "preview": "const config = require('./developer.json')\nconst siteTitle = `${config.name} | ${config.role}`\n\n\n/*\n * Nuxt 3 Config Fil"
  },
  {
    "path": "package.json",
    "chars": 457,
    "preview": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"nuxt build\",\n    \"dev\": \"nuxt dev\",\n    \"generate\": \"nuxt generate\",\n "
  },
  {
    "path": "pages/README.md",
    "chars": 789,
    "preview": "# `pages/` [Directory](https://nuxt.com/docs/getting-started/views#pages)\n\nPages represent views use for each specific r"
  },
  {
    "path": "pages/about-me.vue",
    "chars": 11121,
    "preview": "<template>\n  <main v-if=\"!loading\" id=\"about-me\" class=\"page\">\n\n    <div id=\"mobile-page-title\">\n      <h2>_about-me</h2"
  },
  {
    "path": "pages/contact-me.vue",
    "chars": 6242,
    "preview": "<template>\n    <main id=\"contact-me\" class=\"page\">\n\n        <div id=\"mobile-page-title\">\n            <h2>_contact-me</h2"
  },
  {
    "path": "pages/index.vue",
    "chars": 5736,
    "preview": "<template>\n  \t<main v-if=\"!loading\" id=\"hello\">\n\n    \t<!-- gradients -->\n    \t<div class=\"css-blurry-gradient-blue\"></di"
  },
  {
    "path": "pages/projects.vue",
    "chars": 6764,
    "preview": "<template>\n  <main class=\"flex flex-col flex-auto lg:flex-row overflow-hidden\">\n\n    <div id=\"mobile-page-title\">\n      "
  },
  {
    "path": "public/README.md",
    "chars": 480,
    "preview": "# `public/` Directory\n\nThe `public/` directory is used as a public server for static assets publicly available at a defi"
  },
  {
    "path": "public/pwa/manifest.json",
    "chars": 1171,
    "preview": "{\n    \"name\": \"Portfolio for Developers Concept v2\",\n    \"short_name\": \"Developer Portfolio\",\n    \"description\": \"A awes"
  },
  {
    "path": "public/worker.js",
    "chars": 671,
    "preview": "console.log('Service worker file loaded.')\n\n/**\n * * FETCH event\n * ? It is triggered when the browser is requesting a r"
  },
  {
    "path": "tailwind.config.js",
    "chars": 1431,
    "preview": "module.exports = {\n    theme: {\n        extend: {\n            colors: {\n                'gray-dark': '#121212',\n        "
  },
  {
    "path": "test/github.test.js",
    "chars": 962,
    "preview": "import { beforeEach, describe, expect, it } from 'vitest'\nimport { getGistById } from '../utils/github-api.js'\n\ndescribe"
  },
  {
    "path": "test/global.test.js",
    "chars": 145,
    "preview": "import { describe, expect, it } from 'vitest'\n\ndescribe('Testing test', () => {\n  it('works!', async () => {\n    expect("
  },
  {
    "path": "tsconfig.json",
    "chars": 94,
    "preview": "{\n  // https://nuxt.com/docs/guide/concepts/typescript\n  \"extends\": \"./.nuxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "utils/README.md",
    "chars": 622,
    "preview": "# `utils/` [Directory](https://nuxt.com/docs/guide/directory-structure/utils)\n\nNuxt 3 uses the `utils/` directory to aut"
  },
  {
    "path": "utils/github-api.js",
    "chars": 266,
    "preview": "\n/**\n * Get a gist from Github API by gist ID\n * @param {*} gistId \n * @returns \n */\nconst getGistById = async (gistId) "
  }
]

About this extraction

This page contains the full source code of the alexdeploy/developer-portfolio-v2 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (105.8 KB), approximately 29.5k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. 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.

Copied to clipboard!