Enhance CTA section, fix errors in contact api, and update role animation in hero section.

This commit is contained in:
Daniel LaForce 2025-04-09 23:57:56 -06:00
parent a3a949ddc4
commit 606be6d044
13 changed files with 652 additions and 97 deletions

49
assets/css/style.css Normal file
View File

@ -0,0 +1,49 @@
@keyframes pulse-ring {
0% {
transform: scale(0.33);
opacity: 0;
}
80%, 100% {
opacity: 0;
}
40% {
opacity: 0.3;
}
}
/* Particles */
.particles-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.particle {
position: absolute;
background-color: rgba(59, 130, 246, 0.2);
border-radius: 50%;
pointer-events: none;
}
@keyframes float-particle {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(50px, -50px);
}
50% {
transform: translate(100px, 0);
}
75% {
transform: translate(50px, 50px);
}
100% {
transform: translate(0, 0);
}
}

50
assets/js/script.js Normal file
View File

@ -0,0 +1,50 @@
function initFormHandling() {
const form = document.getElementById('contact-form');
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
submitBtn.disabled = true;
const formData = new FormData(form);
const data = {
name: formData.get('name'),
email: formData.get('email'),
subject: formData.get('subject'),
message: formData.get('message')
};
try {
const response = await fetch('/api/send-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.details || result.error || 'Failed to send message');
}
// Show success message
showAlert('success', 'Message sent successfully! I will get back to you soon.');
form.reset();
} catch (error) {
console.error('Form submission error:', error);
showAlert('error', `Something went wrong: ${error.message}`);
} finally {
submitBtn.innerHTML = originalBtnText;
submitBtn.disabled = false;
}
});
}
// ... existing code ...

View File

@ -6,6 +6,13 @@ export async function onRequestPost(context) {
const { name, email, subject, message } = await context.request.json();
try {
// Check if API key is configured
if (!context.env.MAILERSEND_API_KEY) {
throw new Error('MailerSend API key is not configured');
}
console.log('Attempting to send email with MailerSend...');
const response = await fetch("https://api.mailersend.com/v1/email", {
method: "POST",
headers: {
@ -14,16 +21,16 @@ export async function onRequestPost(context) {
},
body: JSON.stringify({
from: {
email: "contact@argobox.com",
email: "contact@laforceit.com",
name: "Daniel LaForce"
},
to: [
{
email: "daniel.laforce@argobox.com",
email: "daniel.laforce@laforceit.com",
name: "Daniel LaForce"
}
],
subject: `[Argobox] ${subject}`,
subject: `[LaForceIT] ${subject}`,
html: `
<h2>New Contact Message</h2>
<p><strong>Name:</strong> ${name}</p>
@ -39,15 +46,32 @@ export async function onRequestPost(context) {
})
});
const responseData = await response.text();
console.log("MailerSend Response:", responseData);
if (!response.ok) {
console.error("MailerSend Error:", await response.json());
return new Response(JSON.stringify({ error: "Failed to send email" }), { status: 500 });
const errorDetail = responseData ? JSON.parse(responseData) : {};
throw new Error(`MailerSend API error (${response.status}): ${JSON.stringify(errorDetail)}`);
}
return new Response(JSON.stringify({ success: true }), { status: 200 });
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (err) {
console.error("Unexpected Error:", err);
return new Response(JSON.stringify({ error: "Unexpected server error" }), { status: 500 });
console.error("Email Send Error:", err);
return new Response(JSON.stringify({
error: "Failed to send email",
details: err.message,
timestamp: new Date().toISOString()
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -35,6 +35,9 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<!-- Particles Container -->
<div class="particles-container" id="particles-container"></div>
<!-- Navigation Bar -->
<nav class="navbar">
<div class="container">
@ -66,7 +69,6 @@
<!-- Hero Section -->
<section id="home" class="hero">
<!-- Background elements -->
<div class="particles-container" id="particles-container"></div>
<div class="floating-icons" id="floating-icons"></div>
<div class="container">
@ -80,14 +82,17 @@
<h1 class="hero-title">
<span class="role-wrapper">
<span class="role active" data-role="admin" data-description="Building scalable infrastructure with optimized performance and bulletproof security — from server management to end-user support that keeps businesses running smoothly.">
<span class="highlight">Network</span> &amp; <span class="highlight">Cyber Security</span> Professional
<span class="highlight">Systems</span> <span class="highlight">Administrator</span> &amp; IT Expert
</span>
<span class="role" data-role="devops" data-description="Streamlining development workflows with CI/CD pipelines and infrastructure as code — automating deployments and improving reliability across your entire technology stack.">
<span class="highlight">DevOps</span> &amp; <span class="highlight">Automation</span> Engineer
<span class="role" data-role="cyber" data-description="Protecting your digital assets with cutting-edge security solutions and proactive threat detection — ensuring your business stays ahead of evolving cyber threats.">
<span class="highlight">Network</span> &amp; <span class="highlight">Cyber Security</span> Professional
</span>
<span class="role" data-role="architect" data-description="Designing resilient infrastructure and automation solutions — from virtualization and containerization to secure network architecture and cloud migration strategies.">
<span class="highlight">Infrastructure</span> &amp; <span class="highlight">Systems</span> Architect
</span>
<span class="role" data-role="devops" data-description="Streamlining development workflows with CI/CD pipelines and infrastructure as code — automating deployments and improving reliability across your entire technology stack.">
<span class="highlight">DevOps</span> &amp; <span class="highlight">Automation</span> Expert
</span>
</span>
</h1>

View File

@ -9,7 +9,7 @@
<meta name="description" content="Professional resume of Daniel LaForce - Systems & Infrastructure Engineer specializing in virtualization, containerization, and secure network architecture.">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22256%22 height=%22256%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%230f172a%22></rect><path fill=%22%233b82f6%22 d=%22M30 40L50 20L70 40L50 60L30 40Z%22></path><path fill=%22%233b82f6%22 d=%22M50 60L70 40L70 70L50 90L30 70L30 40L50 60Z%22 fill-opacity=%220.6%22></path></svg>">
<link rel="icon" type="image/png" href="images/favicon.png">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -34,9 +34,11 @@
.resume-container {
max-width: 1000px;
margin: 6rem auto 4rem;
padding: 0 1.5rem;
flex: 1;
margin: 2rem auto;
padding: 2rem;
background: var(--card-bg);
border-radius: 1rem;
box-shadow: var(--card-shadow);
}
.resume-header {
@ -286,9 +288,149 @@
grid-template-columns: 1fr;
}
}
.contact-links {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin: 1rem 0;
}
.contact-links a {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-primary);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: var(--secondary-bg);
transition: all var(--transition-normal);
}
.contact-links a:hover {
background: var(--accent);
color: white;
transform: translateY(-2px);
}
@media print {
.contact-links a {
color: black;
background: none;
padding: 0;
margin-right: 1rem;
}
}
/* Call to Action Styles */
.cta-banner {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.2));
border-radius: 1rem;
padding: 2.5rem;
margin-bottom: 3rem;
border: 1px solid rgba(59, 130, 246, 0.3);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
}
.cta-content {
max-width: 800px;
margin: 0 auto;
}
.cta-title {
font-size: 2rem;
color: var(--text-primary);
margin-bottom: 1rem;
text-align: center;
background: linear-gradient(135deg, #3b82f6, #2563eb);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
}
.cta-description {
font-size: 1.1rem;
color: var(--text-secondary);
text-align: center;
margin-bottom: 2rem;
}
.cta-highlights {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.cta-highlight {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: rgba(59, 130, 246, 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(59, 130, 246, 0.2);
transition: all var(--transition-normal);
}
.cta-highlight:hover {
transform: translateY(-2px);
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
}
.cta-highlight i {
font-size: 1.5rem;
color: var(--accent);
}
.cta-highlight span {
color: var(--text-primary);
font-weight: 500;
}
.cta-buttons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.cta-btn {
min-width: 200px;
text-align: center;
}
@media (max-width: 768px) {
.cta-title {
font-size: 1.75rem;
}
.cta-description {
font-size: 1rem;
}
.cta-highlight {
padding: 0.75rem;
}
.cta-btn {
width: 100%;
}
}
</style>
</head>
<body>
<!-- Bubble Animation -->
<div class="bubbles">
<div class="bubble"></div>
<div class="bubble"></div>
<div class="bubble"></div>
<div class="bubble"></div>
<div class="bubble"></div>
<div class="bubble"></div>
<div class="bubble"></div>
</div>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
@ -320,9 +462,41 @@
<!-- Resume Content -->
<div class="resume-container">
<!-- Call to Action Section -->
<div class="cta-banner">
<div class="cta-content">
<h2 class="cta-title">Transform Your Business with Enterprise-Grade IT Solutions</h2>
<p class="cta-description">Whether you need to enhance your cybersecurity posture, modernize your infrastructure, or seek a seasoned IT leader for your team, I deliver proven results:</p>
<div class="cta-highlights">
<div class="cta-highlight">
<i class="fas fa-shield-alt"></i>
<span>Reduced security incidents by 83% for enterprise clients</span>
</div>
<div class="cta-highlight">
<i class="fas fa-tachometer-alt"></i>
<span>Improved system performance by 60% through optimization</span>
</div>
<div class="cta-highlight">
<i class="fas fa-chart-line"></i>
<span>Decreased operational costs by 30% with automation</span>
</div>
</div>
<div class="cta-buttons">
<a href="index.html#contact" class="btn btn-primary cta-btn">
<i class="fas fa-rocket"></i>
Schedule a Free Consultation
</a>
<a href="mailto:daniel.laforce@argobox.com" class="btn btn-outline cta-btn">
<i class="fas fa-envelope"></i>
Discuss Employment Opportunities
</a>
</div>
</div>
</div>
<div class="resume-header">
<h1 class="resume-name">Daniel LaForce</h1>
<h2 class="resume-title">Infrastructure & Systems Engineer</h2>
<h2 class="resume-title">Network & Cyber Security Professional</h2>
<div class="resume-contact">
<div class="resume-contact-item">

116
script.js
View File

@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize all website functionality
initNavigation();
initParticlesAndIcons();
initRoleRotation();
initRoleAnimation();
initTerminalTyping();
initSolutionsCarousel();
initScrollReveal();
@ -119,11 +119,12 @@ function createBackgroundParticles() {
particle.style.top = `${yPos}%`;
// Animation properties
particle.style.animationDuration = `${Math.random() * 20 + 10}s`;
particle.style.animationDelay = `${Math.random() * 5}s`;
const duration = Math.random() * 20 + 10;
particle.style.animationDuration = `${duration}s`;
particle.style.animationDelay = `${Math.random() * -duration}s`;
// Add particle animation
particle.style.animation = `particle-float ${Math.random() * 20 + 10}s linear infinite`;
particle.style.animation = `float-particle ${duration}s linear infinite`;
particlesContainer.appendChild(particle);
}
@ -180,51 +181,24 @@ function createFloatingIcons() {
/**
* Initialize role rotation in the hero section
*/
function initRoleRotation() {
function initRoleAnimation() {
const roles = document.querySelectorAll('.role');
const descriptionElement = document.getElementById('role-description');
if (roles.length === 0 || !descriptionElement) return;
let currentRole = 0;
function rotateRoles() {
// Hide current role
roles[currentRole].classList.remove('active');
// Move to next role
currentRole = (currentRole + 1) % roles.length;
// Show new role
roles[currentRole].classList.add('active');
// Update description text
const newDescription = roles[currentRole].getAttribute('data-description');
if (newDescription) {
descriptionElement.textContent = newDescription;
// Animate the description change
descriptionElement.style.opacity = '0';
descriptionElement.style.transform = 'translateY(10px)';
setTimeout(() => {
descriptionElement.style.opacity = '1';
descriptionElement.style.transform = 'translateY(0)';
}, 50);
}
const description = document.getElementById('role-description');
let currentIndex = 0;
function updateRole() {
roles.forEach(role => role.classList.remove('active'));
roles[currentIndex].classList.add('active');
description.textContent = roles[currentIndex].getAttribute('data-description');
description.style.opacity = '0';
setTimeout(() => {
description.style.opacity = '1';
}, 100);
currentIndex = (currentIndex + 1) % roles.length;
}
// Set initial role to active
roles[0].classList.add('active');
// Initialize with the first description
const initialDescription = roles[0].getAttribute('data-description');
if (initialDescription && descriptionElement) {
descriptionElement.textContent = initialDescription;
}
// Start rotation with delay
setInterval(rotateRoles, 5000);
updateRole(); // Set initial state
setInterval(updateRole, 5000); // Switch roles every 5 seconds
}
/**
@ -406,6 +380,10 @@ function initFormHandling(form) {
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
submitButton.disabled = true;
const notification = document.getElementById('form-notification');
const notificationIcon = notification.querySelector('i');
const notificationText = notification.querySelector('.notification-text');
const formData = {
name: form.elements['name'].value,
email: form.elements['email'].value,
@ -414,6 +392,7 @@ function initFormHandling(form) {
};
try {
console.log('Sending form data...');
const res = await fetch("/api/send-email", {
method: "POST",
headers: {
@ -423,47 +402,38 @@ function initFormHandling(form) {
});
const data = await res.json();
console.log('Response:', data);
const notification = document.getElementById('form-notification');
const notificationIcon = notification.querySelector('i');
const notificationText = notification.querySelector('.notification-text');
if (data.success) {
notification.style.display = 'block';
notification.classList.add('success');
if (res.ok) {
notification.style.display = 'flex';
notification.classList.remove('error');
notification.classList.add('success');
notificationIcon.className = 'fas fa-check-circle';
notificationText.textContent = "Message sent successfully! We'll get back to you soon.";
form.reset();
} else {
notification.style.display = 'block';
notification.classList.add('error');
notification.style.display = 'flex';
notification.classList.remove('success');
notification.classList.add('error');
notificationIcon.className = 'fas fa-exclamation-circle';
notificationText.textContent = "Failed to send message. Please try again or contact me directly.";
notificationText.textContent = `Error: ${data.error}${data.details ? ` - ${data.details}` : ''}`;
}
// Hide notification after 5 seconds
setTimeout(() => {
notification.style.display = 'none';
}, 5000);
} catch (error) {
console.error("Error:", error);
const notification = document.getElementById('form-notification');
notification.style.display = 'block';
notification.classList.add('error');
console.error("Form submission error:", error);
notification.style.display = 'flex';
notification.classList.remove('success');
notification.querySelector('i').className = 'fas fa-exclamation-circle';
notification.querySelector('.notification-text').textContent = "Something went wrong while sending your message.";
// Hide notification after 5 seconds
setTimeout(() => {
notification.style.display = 'none';
}, 5000);
notification.classList.add('error');
notificationIcon.className = 'fas fa-exclamation-circle';
notificationText.textContent = `Error: ${error.message}`;
} finally {
submitButton.innerHTML = originalButtonText;
submitButton.disabled = false;
// Hide notification after 10 seconds
setTimeout(() => {
notification.style.display = 'none';
}, 10000);
}
});
}

View File

@ -1540,24 +1540,25 @@ transform: scale(1.02);
.form-notification {
margin-top: 1rem;
padding: 1rem;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
animation: slideIn 0.3s ease-out;
font-weight: 500;
}
.form-notification.success {
background-color: #d1fae5;
color: #065f46;
border: 1px solid #34d399;
background-color: rgba(16, 185, 129, 0.1);
color: #10b981;
border: 1px solid #10b981;
}
.form-notification.error {
background-color: #fee2e2;
color: #991b1b;
border: 1px solid #f87171;
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
border: 1px solid #ef4444;
}
.form-notification i {
@ -1575,4 +1576,286 @@ transform: scale(1.02);
}
}
/* Bubble Animation */
.bubbles {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
z-index: -1;
overflow: hidden;
}
.bubble {
position: absolute;
background: radial-gradient(circle at center, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.05) 100%);
border-radius: 50%;
animation: float-bubble 8s infinite ease-in-out;
opacity: 0;
}
.bubble:nth-child(1) {
width: 80px;
height: 80px;
left: 10%;
animation-delay: 0s;
}
.bubble:nth-child(2) {
width: 120px;
height: 120px;
left: 25%;
animation-delay: 2s;
}
.bubble:nth-child(3) {
width: 100px;
height: 100px;
left: 40%;
animation-delay: 4s;
}
.bubble:nth-child(4) {
width: 150px;
height: 150px;
left: 65%;
animation-delay: 6s;
}
.bubble:nth-child(5) {
width: 90px;
height: 90px;
left: 80%;
animation-delay: 8s;
}
@keyframes float-bubble {
0% {
transform: translateY(100vh) scale(0);
opacity: 0;
}
20% {
opacity: 0.5;
}
50% {
opacity: 0.3;
}
80% {
opacity: 0.5;
}
100% {
transform: translateY(-100vh) scale(1.2);
opacity: 0;
}
}
/* Add this to both index.html and resume.html body */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 20% 35%, rgba(29, 78, 216, 0.15) 0%, transparent 50%),
radial-gradient(circle at 75% 60%, rgba(14, 165, 233, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: -2;
}
/* Slogan Styles */
.slogan-wrapper {
text-align: center;
margin: 1.5rem 0;
animation: fadeIn 1s ease-out;
}
.slogan {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(135deg, #3b82f6, #2563eb);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 0.5rem;
}
.slogan-sub {
font-size: 1.25rem;
color: var(--text-secondary);
font-weight: 500;
}
/* CTA Section Styles */
.cta-section {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.2));
padding: 6rem 0;
position: relative;
overflow: hidden;
}
.cta-grid {
display: flex;
justify-content: center;
align-items: center;
gap: 4rem;
}
.cta-content {
max-width: 800px;
text-align: center;
}
.cta-title {
font-size: 2.5rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #3b82f6, #2563eb);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cta-description {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 3rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.cta-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin-bottom: 3rem;
}
.cta-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: var(--accent);
}
.stat-label {
font-size: 1rem;
color: var(--text-secondary);
text-align: center;
}
.cta-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin-bottom: 3rem;
}
.cta-feature {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 1.5rem;
background: rgba(59, 130, 246, 0.1);
border-radius: 1rem;
border: 1px solid rgba(59, 130, 246, 0.2);
transition: all var(--transition-normal);
}
.cta-feature:hover {
transform: translateY(-5px);
background: rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
}
.cta-feature i {
font-size: 2rem;
color: var(--accent);
}
.cta-feature span {
font-weight: 500;
color: var(--text-primary);
text-align: center;
}
.cta-buttons {
display: flex;
justify-content: center;
gap: 1rem;
}
@media (max-width: 768px) {
.slogan {
font-size: 2rem;
}
.slogan-sub {
font-size: 1.1rem;
}
.cta-stats, .cta-features {
grid-template-columns: 1fr;
}
.cta-buttons {
flex-direction: column;
gap: 1rem;
}
.cta-title {
font-size: 2rem;
}
.cta-description {
font-size: 1.1rem;
}
}
/* Particles */
.particles-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.particle {
position: absolute;
background-color: rgba(59, 130, 246, 0.2);
border-radius: 50%;
pointer-events: none;
}
@keyframes float-particle {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(50px, -50px);
}
50% {
transform: translate(100px, 0);
}
75% {
transform: translate(50px, 50px);
}
100% {
transform: translate(0, 0);
}
}