🔧

Tinker Stack

My personal portfolio website

Node.js Express HTML/CSS Nodemailer Docker

Overview

My personal portfolio website built from scratch with a focus on clean design and minimal dependencies. Features a responsive layout with smooth animations, a working contact form, and a Nord-inspired color scheme.

Key Features

Contact Form with Email Delivery

The contact form submits to a simple Express API endpoint that validates input and sends emails using nodemailer. Supports both plain text and HTML email formats.

app.post('/api/contact', async (req, res) => {
  const { name, email, message } = req.body;
  
  if (!name || !email || !message) {
    return res.status(400).json({ error: 'All fields are required' });
  }

  try {
    await transporter.sendMail({
      from: fromEmail,
      replyTo: email,
      to: toEmail,
      subject: `New contact from ${name}`,
      text: `Name: ${name}\nEmail: ${email}\n\nMessage:\n${message}`,
      html: `<h3>New contact form submission</h3>
             <p><strong>Name:</strong> ${name}</p>
             <p><strong>Email:</strong> ${email}</p>
             <p><strong>Message:</strong></p>
             <p>${message.replace(/\n/g, '<br>')}</p>`
    });
    
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to send email: ' + error.message });
  }
});

Scroll Animations with Intersection Observer

Elements fade in as they scroll into view using the Intersection Observer API for performant, JavaScript-heavy animation-free scrolling.

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
    }
  });
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });

document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));

Client-Side Form Handling

The contact form uses vanilla JavaScript with async/await for clean, readable API calls and proper error handling.

contactForm.addEventListener('submit', async (e) => {
  e.preventDefault();
  
  const data = {
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message')
  };
  
  submitBtn.disabled = true;
  submitBtn.textContent = 'Sending...';
  
  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (response.ok) {
      formStatus.textContent = 'Message sent!';
      contactForm.reset();
    }
  } catch (error) {
    formStatus.textContent = 'Failed to send message.';
  } finally {
    submitBtn.disabled = false;
  }
});

Vanilla JS Mobile Navigation

A lightweight mobile menu toggle using only vanilla JavaScript and CSS. Toggles a class on both the button and nav menu for smooth animations.

// Mobile menu toggle
const menuToggle = document.getElementById('menuToggle');
const navLinks = document.getElementById('navLinks');

menuToggle.addEventListener('click', () => {
  menuToggle.classList.toggle('active');
  navLinks.classList.toggle('active');
});

// Close menu when clicking a link
navLinks.querySelectorAll('a').forEach(link => {
  link.addEventListener('click', () => {
    menuToggle.classList.remove('active');
    navLinks.classList.remove('active');
  });
});

Challenges & Solutions