A web development portfolio site incorporating colorful icons and various photographs for improved visual engagement.

The Stack

  • HTML
  • CSS
  • Vanilla JavaScript
  • Node.js
  • Nodemailer
  • NGINX
  • Linux
  • Digital Ocean

Custom built HTML, CSS, and vanilla JavaScript on Node.js, using Express and EJS. Nodemailer used for the contact form. Served by NGINX on Debian, stored on a DigitalOcean droplet. Mobile friendly design with TLS encryption through Let's Encrypt.

Responsive Layout

Sticky Navigation

Nav bar sticks to the top of the browser as you scroll down, allowing quick navigation regardless of your current location on the page.

<!-- HTML -->
  <div id="nav-top-container">
    <nav id="nav-top">
      <ul>
        <li><a href="/#home">Home</a></li>
        <li><a href="/#work">Work</a></li>
        <li><a href="/#about">About</a></li>
        <li><a href="/#contact">Contact</a></li>
        <li><a href="/resume">Resume</a></li>
      </ul>
    </nav>
  </div>
/* CSS that will get applied to nav-top */
.nav-sticky {
  position: fixed;
  top: -1px;
  width: 100%;
}
// ============== //
//   JavaScript   //
// ============== //

var navTopContainer = document.getElementById('nav-top-container');
var navTop = document.getElementById('nav-top');
var navOffset;

// Function to set the distance from the top 
// (using nav container because it doesn't change location) 
function setNavOffset(){
  navOffset = navTopContainer.offsetTop;
};

// Function to apply sticky nav, depending on scroll location
function setStickyHeader(){
  if(window.scrollY > navOffset){
    navTop.classList.add('nav-sticky');
  } else {
    navTop.classList.remove('nav-sticky');
  };
};

// Run when page loads
setNavOffset();
setStickyHeader();
// Note: Must run setStickyHeader at load in case page loads partway down,
// which can happen when navigating directly to an element partway down a page
// (for example, About or Contact).

// When scrolling, apply sticky nav if appropriate
window.addEventListener('scroll', setStickyHeader);

// When resizing the window, reset scrolling distance needed to apply sticky nav
// (needed because the header changes size depending on viewport width)
window.addEventListener('resize', setNavOffset);

Skill Selector

Choose a skill and the description is shown.

<!-- HTML -->
<ul>
  <li class="skill"><img src="/img/icons/html5.svg" alt="HTML"></li>
  <li class="skill"><img src="/img/icons/css3.svg" alt="CSS"></li>
  ...
</ul>
<p id="skill-description" class="grayed-out">Choose one.</p>
/* CSS */
.skill {
  display: inline-block;
  width: 7rem;
  height: 7rem;
  margin: 1rem;
  padding: 1rem;
  border: 2px solid transparent;
  border-radius: 0.5rem;
}

.skill:hover {
  background-color: #eee;
  cursor: pointer;
}

.skill-focus {
  border-color: #bbb;
}
// ============== //
//   JavaScript   //
// ============== //

var skills = document.getElementsByClassName('skill');
var skillsDescrPara = document.getElementById('skill-description');
var skillsDescrArray = [
  "<strong>HTML:</strong> Ever tried building a website without HTML?...",
  "<strong>CSS:</strong> Using max-width, @media queries, and a few...",
  ...
];

// Pull description from HTML in case I change it later
var defaultDescription = skillsDescrPara.innerHTML;

// Variable to keep track of which skill is currently selected
var currentSkill = null;

// Function to highlight the selected skill and show its description
function selectSkill(){
  // Remove highlight from all skills
  for (var j = 0; j < skills.length; j++) {
    skills[j].classList.remove('skill-focus');
  };
  // Add highlight to only one skill
  skills[currentSkill].classList.add('skill-focus');
  // Make description NOT grayed out
  skillsDescrPara.classList.remove('grayed-out');
  // Change the description text to match selected
  skillsDescrPara.innerHTML = skillsDescrArray[currentSkill];
};

// Click skill to activate
for (let i = 0; i < skills.length; i++) {
  skills[i].addEventListener('click', function(event){
    currentSkill = i;
    selectSkill();
    event.stopPropagation(); // Needed for later
  });
};

Use the arrow keys to switch between skills.

document.addEventListener('keydown', function(event){
  // If a skill hasn't been selected don't do anything
  if (currentSkill === null) {
  }
  // Left or up arrow
  else if(event.which === 37 || event.which === 38) {
    event.preventDefault();
    // Loop around to end
    if(currentSkill < 1) {
      currentSkill = skills.length - 1;
      selectSkill();
    } 
    // Move to skill to the left
    else {
      currentSkill -= 1;
      selectSkill();
    };
  } 
  // Right or down arrow
  else if (event.which === 39 || event.which === 40) {
    event.preventDefault();
    // Loop around to beginning
    if(currentSkill >= skills.length - 1) {
      currentSkill = 0;
      selectSkill();
    } 
    // Move to skill to the right
    else {
      currentSkill += 1;
      selectSkill();
    };
  };
});

Click anywhere else on the page or press escape to clear the selection.

// Function to reset skills and clear the selected description
function skillsReset(){
  skillsDescrPara.innerHTML = defaultDescription;
  skillsDescrPara.classList.add('grayed-out');
  currentSkill = null;
  for (var j = 0; j < skills.length; j++) {
    skills[j].classList.remove('skill-focus');
  };
};

// Click somewhere else on the page to reset skills
document.addEventListener('click', function(){
  skillsReset();
});

// Prevent skills reset when clicking on the skill's description
// (to allow the user to select the text)
skillsDescrPara.addEventListener('click', function(event){
  event.stopPropagation();
});

// Hit escate to reset skills
document.addEventListener('keydown', function(event){
  if(event.which === 27) {
    skillsReset();
  };
});

Image Gallery

Hovering over an image produces an overlay and title with a CSS transition.

<!-- HTML -->
<div class="interests-container">
  <div id="camping">
    <span class="interest">Camping</span>
  </div>
</div>
/* CSS */

#camping {background-image: url(/img/interests/camping.jpg);}

.interests-container>div {
  float: left;
  width: 32.33%;
  padding-top: 32.33%;
  position: relative;
  cursor: pointer;
  margin: 0.5%;
  background-size: cover;
  background-position: center;
  overflow: hidden;
}

.interests-container>div>span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: -8px;
  left: 0;
  width: 100%;
  height: 100%;
  color: transparent;
  font-size: 1.8rem;
  transition: all 0.1s ease-in;
}

/* Hover is on the div because the span is offset by 8px: */
.interests-container>div:hover>span {
  top: 0;
  background: rgba(0,0,0,0.7);
  /* To hide the gap at the bottom during the CSS transition: */
  box-shadow: 0 0 0 2rem rgba(0,0,0,0.7);
  color: #fff;
}

Click an image to open the image viewer.

// ============== //
//   JavaScript   //
// ============== //

// Array of objects containing each image's URL, title and description
var popupData = [
...,
{
  title: 'Custom Mechanical Keyboards',
  description: "If you have a few hours...",
  image: '/img/interests/keyboards.jpg'
},
...
]

// Variable to keep track of the currently selected image
var currentImage = 0;

// Change the content of the popup to match the selected interest
function setPopupContent(){
  popupImg.src = popupData[currentImage].image;
  popupH2.textContent = popupData[currentImage].title;
  popupP.textContent = popupData[currentImage].description;
};

// Hide left/right arrow buttons on first/last image
function showHidePopupArrows(){
  if (currentImage === 0) {
    popupLeft.classList.add('hidden');
    popupRight.classList.remove('hidden');
  } else if (currentImage === popupData.length - 1) {
    popupLeft.classList.remove('hidden');
    popupRight.classList.add('hidden');
  } else {
    popupLeft.classList.remove('hidden');
    popupRight.classList.remove('hidden');
  };
};
// Note: The 'hidden' class must be removed in all cases above because the user
// could close the popup on the last image and then reopen it on the first. In
// this case, if you don't remove the hidden class, both arrows would be hidden.

// Click an image to view the popup
for(let i = 0; i < interest.length; i++){
  interest[i].addEventListener('click', function(){
    popup.classList.remove('hidden'); // Show the popup
    html.classList.add('noscroll'); // Add CSS class to prevent page scrolling
    currentImage = i;
    setPopupContent();
    showHidePopupArrows()
  });
};

Hit escape to close the image viewer, or use the arrow keys to change images.

document.addEventListener('keydown', function(event){
  // Hit escape to close the pop-up
  if(event.which === 27){
    popup.classList.add('hidden');
    html.classList.remove('noscroll');
    showPopupControls();
  } 
  // Use the left arrow key to navigate ONLY if the pop-up is NOT hidden
  else if (event.which === 37 && popup.classList.value === '') {
    if (currentImage > 0) {
      currentImage -= 1;
      setPopupContent();
      showHidePopupArrows();
    }
  } 
  // Use the right arrow key to navigate ONLY if the pop-up is NOT hidden
  else if (event.which === 39 && popup.classList.value === '') {
    if (currentImage < popupData.length - 1) {
      currentImage += 1;
      setPopupContent();
      showHidePopupArrows();
    }
  };
});