commit 3410b6c64b9a3beca579f6045e8202c069facbfd Author: Hammer Date: Tue Jan 27 02:18:01 2026 +0000 Initial portfolio site diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aab24dc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:alpine +COPY . /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9a8cd3 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Donovan Kelly - Portfolio + +A clean, responsive portfolio site. + +## Structure + +``` +donovan-portfolio/ +├── index.html +├── css/ +│ └── style.css +├── js/ +│ └── main.js +├── assets/ +│ └── (images, favicon, etc.) +└── README.md +``` + +## Setup + +### Contact Form +The contact form uses [Formspree](https://formspree.io/) for handling submissions on static hosting. + +1. Create a free account at formspree.io +2. Create a new form and copy the form ID +3. Replace `YOUR_FORM_ID` in `index.html` with your actual form ID: + ```html +
+ ``` + +### Social Links +Update the GitHub link in `index.html` if you have one: +```html + +``` + +## Deployment + +### Static Hosting (Netlify, Vercel, GitHub Pages) +Just upload/push the folder - no build step required. + +### Docker +```dockerfile +FROM nginx:alpine +COPY . /usr/share/nginx/html +EXPOSE 80 +``` + +### Dokploy +Upload as static site or use the Docker method above. + +## Customization + +- **Colors**: Edit CSS variables in `css/style.css` under `:root` +- **Content**: Edit `index.html` directly +- **Add projects**: Copy a `.project-card` block and modify diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..da04a08 --- /dev/null +++ b/css/style.css @@ -0,0 +1,466 @@ +/* Reset & Base */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --color-bg: #fafafa; + --color-surface: #ffffff; + --color-text: #1a1a1a; + --color-text-muted: #666666; + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-border: #e5e5e5; + --color-accent: #f0f4ff; + --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --container-width: 1100px; + --spacing-xs: 0.5rem; + --spacing-sm: 1rem; + --spacing-md: 1.5rem; + --spacing-lg: 2rem; + --spacing-xl: 3rem; + --spacing-xxl: 5rem; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); + --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06); + --transition: 150ms ease; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-family); + font-size: 16px; + line-height: 1.6; + color: var(--color-text); + background-color: var(--color-bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container { + width: 100%; + max-width: var(--container-width); + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +/* Typography */ +h1, h2, h3, h4 { + font-weight: 600; + line-height: 1.3; +} + +h1 { font-size: 3rem; } +h2 { font-size: 2rem; } +h3 { font-size: 1.25rem; } + +p { + margin-bottom: var(--spacing-sm); +} + +a { + color: var(--color-primary); + text-decoration: none; + transition: color var(--transition); +} + +a:hover { + color: var(--color-primary-hover); +} + +/* Navigation */ +.nav { + position: fixed; + top: 0; + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--color-border); + z-index: 100; +} + +.nav-container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: var(--container-width); + margin: 0 auto; + padding: var(--spacing-sm) var(--spacing-md); +} + +.nav-logo { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-text); +} + +.nav-logo:hover { + color: var(--color-primary); +} + +.nav-links { + display: flex; + list-style: none; + gap: var(--spacing-lg); +} + +.nav-links a { + color: var(--color-text-muted); + font-size: 0.9rem; + font-weight: 500; + transition: color var(--transition); +} + +.nav-links a:hover { + color: var(--color-text); +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + font-weight: 500; + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition); + border: none; + text-align: center; +} + +.btn-primary { + background: var(--color-primary); + color: white; +} + +.btn-primary:hover { + background: var(--color-primary-hover); + color: white; +} + +.btn-secondary { + background: transparent; + color: var(--color-text); + border: 1px solid var(--color-border); +} + +.btn-secondary:hover { + border-color: var(--color-text); + color: var(--color-text); +} + +/* Hero */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-xxl) var(--spacing-md); + text-align: center; + background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-bg) 100%); +} + +.hero-content { + max-width: 700px; +} + +.hero h1 { + margin-bottom: var(--spacing-xs); +} + +.hero-subtitle { + font-size: 1.25rem; + color: var(--color-primary); + font-weight: 500; + margin-bottom: var(--spacing-md); +} + +.hero-description { + font-size: 1.1rem; + color: var(--color-text-muted); + margin-bottom: var(--spacing-lg); +} + +.hero-cta { + display: flex; + gap: var(--spacing-sm); + justify-content: center; + flex-wrap: wrap; +} + +/* Sections */ +.section { + padding: var(--spacing-xxl) 0; +} + +.section-title { + margin-bottom: var(--spacing-xl); + text-align: center; + position: relative; +} + +.section-title::after { + content: ''; + display: block; + width: 50px; + height: 3px; + background: var(--color-primary); + margin: var(--spacing-sm) auto 0; + border-radius: 2px; +} + +/* About */ +.about { + background: var(--color-surface); +} + +.about-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-xl); + align-items: start; +} + +.about-text p { + color: var(--color-text-muted); + margin-bottom: var(--spacing-md); +} + +.about-text strong { + color: var(--color-text); +} + +.about-skills h3 { + margin-bottom: var(--spacing-md); +} + +.skills-grid { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); +} + +.skill-tag { + display: inline-block; + padding: 0.4rem 0.8rem; + background: var(--color-accent); + color: var(--color-primary); + font-size: 0.85rem; + font-weight: 500; + border-radius: var(--radius-sm); +} + +/* Projects */ +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: var(--spacing-lg); +} + +.project-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + transition: box-shadow var(--transition), transform var(--transition); +} + +.project-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-xs); +} + +.project-header h3 { + margin: 0; +} + +.project-status { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #16a34a; + background: #dcfce7; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); +} + +.project-date { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.project-role { + font-size: 0.9rem; + color: var(--color-primary); + font-weight: 500; + margin-bottom: var(--spacing-sm); +} + +.project-description { + color: var(--color-text-muted); + font-size: 0.95rem; + margin-bottom: var(--spacing-md); +} + +.project-tags { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-xs); +} + +.project-tags span { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + background: var(--color-bg); + color: var(--color-text-muted); + border-radius: var(--radius-sm); +} + +/* Contact */ +.contact { + background: var(--color-surface); +} + +.contact-content { + display: grid; + grid-template-columns: 1fr 1.5fr; + gap: var(--spacing-xl); + align-items: start; +} + +.contact-info p { + color: var(--color-text-muted); + margin-bottom: var(--spacing-lg); +} + +.social-links { + display: flex; + gap: var(--spacing-sm); +} + +.social-link { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: var(--radius-md); + background: var(--color-bg); + color: var(--color-text-muted); + transition: all var(--transition); +} + +.social-link:hover { + background: var(--color-primary); + color: white; +} + +.contact-form { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.form-group { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.form-group label { + font-size: 0.9rem; + font-weight: 500; +} + +.form-group input, +.form-group textarea { + padding: 0.75rem 1rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-family: inherit; + font-size: 1rem; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.form-group textarea { + resize: vertical; + min-height: 120px; +} + +/* Footer */ +.footer { + padding: var(--spacing-lg) 0; + text-align: center; + border-top: 1px solid var(--color-border); +} + +.footer p { + color: var(--color-text-muted); + font-size: 0.9rem; + margin: 0; +} + +/* Responsive */ +@media (max-width: 768px) { + h1 { font-size: 2.25rem; } + h2 { font-size: 1.75rem; } + + .nav-links { + gap: var(--spacing-md); + } + + .about-content, + .contact-content { + grid-template-columns: 1fr; + } + + .projects-grid { + grid-template-columns: 1fr; + } + + .hero { + padding-top: calc(var(--spacing-xxl) + 60px); + } +} + +@media (max-width: 480px) { + h1 { font-size: 1.875rem; } + + .nav-links { + gap: var(--spacing-sm); + } + + .nav-links a { + font-size: 0.8rem; + } + + .hero-cta { + flex-direction: column; + } + + .hero-cta .btn { + width: 100%; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..ceec795 --- /dev/null +++ b/index.html @@ -0,0 +1,159 @@ + + + + + + Donovan Kelly | CTO & Software Engineer + + + + + + + + + +
+
+

Donovan Kelly

+

CTO & Software Engineer

+

Building secure authentication systems and autonomous robotics at the intersection of software and hardware.

+ +
+
+ +
+
+

About

+
+
+

I'm the CTO of Arcanum Technology LLC, where I lead the development of nKode — a cutting-edge multi-factor authentication system designed to make security seamless and robust.

+

My background spans embedded systems, robotics, and full-stack development. I've built everything from autonomous robots using ROS and computer vision to secure authentication infrastructure.

+

I'm passionate about solving hard problems at the edge of what's possible — whether that's making machines navigate the real world or protecting digital identities.

+
+
+

Skills & Technologies

+
+ Python + C/C++ + ROS + Computer Vision + SLAM + Embedded Systems + Authentication + Security + Full-Stack + Leadership +
+
+
+
+
+ +
+
+

Projects

+
+
+
+

nKode

+ Current +
+

CTO, Arcanum Technology LLC

+

A cutting-edge multi-factor authentication system designed for modern security needs. Leading architecture, development, and team strategy.

+
+ Authentication + Security + MFA +
+
+ +
+
+

F.R.A.N.C.

+ 2018 - Present +
+

Software Lead

+

Forest Roaming Autonomous Nature Creator — an autonomous seed-planting robot. Developed software using ROS for simultaneous localization and mapping (SLAM) with Xbox Kinect and LiDAR sensors.

+
+ ROS + SLAM + LiDAR + Computer Vision + Robotics +
+
+ +
+
+

Slug Slinger

+ Sep - Dec 2018 +
+

Software Lead

+

Autonomous combat robot capable of navigating a 16ft × 8ft arena, locating enemy robots, and firing projectiles up to 16ft. Built from scratch on a PIC32 microcontroller with full custom software stack.

+
+ PIC32 + Embedded C + Autonomous Navigation + Robotics +
+
+
+
+
+ +
+
+

Get in Touch

+
+
+

Interested in working together or have a question? Drop me a message.

+ +
+ +
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ + + + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..8d6cb8c --- /dev/null +++ b/js/main.js @@ -0,0 +1,72 @@ +// Smooth scroll for anchor links +document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + const navHeight = document.querySelector('.nav').offsetHeight; + const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - navHeight; + window.scrollTo({ + top: targetPosition, + behavior: 'smooth' + }); + } + }); +}); + +// Nav background on scroll +const nav = document.querySelector('.nav'); +let lastScroll = 0; + +window.addEventListener('scroll', () => { + const currentScroll = window.pageYOffset; + + if (currentScroll > 100) { + nav.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; + } else { + nav.style.boxShadow = 'none'; + } + + lastScroll = currentScroll; +}); + +// Form submission handling +const form = document.querySelector('.contact-form'); +if (form) { + form.addEventListener('submit', async (e) => { + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + // Show loading state + submitBtn.textContent = 'Sending...'; + submitBtn.disabled = true; + + // If using Formspree, let it handle normally + // This is just for UX feedback + setTimeout(() => { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + }, 3000); + }); +} + +// Intersection Observer for fade-in animations +const observerOptions = { + root: null, + rootMargin: '0px', + threshold: 0.1 +}; + +const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); +}, observerOptions); + +// Observe elements (if you want fade-in effects, add .fade-in class to elements) +document.querySelectorAll('.fade-in').forEach(el => { + observer.observe(el); +});