mikeschlottig's picture
Built out the Services, Projects and Contact Page. Ensure that the links in the footer work and are properly routed.
2917b05 verified
/**
* Main Application Logic
* Handles routing, data fetching, and component interaction.
*/
// Data Mockups (Simulating Public API response)
const servicesData = [
{
id: 1,
title: "Tree Removal",
desc: "Complete removal of hazardous or unwanted trees using advanced rigging techniques to protect your property.",
icon: "log-out",
image: "http://static.photos/nature/640x360/1"
},
{
id: 2,
title: "Stump Grinding",
desc: "Eliminate tripping hazards and improve curb appeal by grinding stumps below ground level.",
icon: "disc",
image: "http://static.photos/nature/640x360/2"
},
{
id: 3,
title: "Tree Pruning",
desc: "Selective branch removal to improve structure, health, and aesthetics of your trees.",
icon: "scissors",
image: "http://static.photos/nature/640x360/3"
},
{
id: 4,
title: "Emergency Storm Care",
desc: "24/7 rapid response for fallen trees or branches threatening your home or power lines.",
icon: "alert-triangle",
image: "http://static.photos/nature/640x360/4"
},
{
id: 5,
title: "Land Clearing",
desc: "Preparing lots for construction or renovation by removing vegetation efficiently.",
icon: "map",
image: "http://static.photos/nature/640x360/5"
},
{
id: 6,
title: "Cabling & Bracing",
desc: "Installing support systems to preserve structurally weak trees and extend their lifespan.",
icon: "anchor",
image: "http://static.photos/nature/640x360/6"
}
];
const projectsData = [
{ title: "Oak Tree Removal", location: "Downtown", img: "http://static.photos/nature/640x360/10" },
{ title: "Storm Damage Cleanup", location: "Westside", img: "http://static.photos/nature/640x360/11" },
{ title: "Stump Grinding Project", location: "Hillside", img: "http://static.photos/nature/640x360/12" },
{ title: "Palm Tree Trimming", location: "Beach Blvd", img: "http://static.photos/nature/640x360/13" },
{ title: "Hazardous Limb Removal", location: "Suburbia", img: "http://static.photos/nature/640x360/14" },
{ title: "Commercial Clearing", location: "Industrial Park", img: "http://static.photos/nature/640x360/15" },
];
class Router {
constructor() {
this.routes = ['home', 'services', 'projects', 'contact'];
this.init();
}
init() {
// Handle initial load based on hash or default to home
const hash = window.location.hash.replace('#', '') || 'home';
this.navigate(hash);
// Handle browser back/forward buttons
window.addEventListener('popstate', (event) => {
if(event.state && event.state.page) {
this.renderPage(event.state.page);
}
});
}
navigate(pageId) {
// Update URL hash without reload
if (window.location.hash !== `#${pageId}`) {
history.pushState({ page: pageId }, null, `#${pageId}`);
}
this.renderPage(pageId);
}
renderPage(pageId) {
// Hide all sections
document.querySelectorAll('.page-section').forEach(section => {
section.classList.remove('active');
});
// Show target section
const target = document.getElementById(pageId);
if (target) {
target.classList.add('active');
window.scrollTo(0, 0);
} else {
// Fallback to home
document.getElementById('home').classList.add('active');
}
// Update Nav Active State (works inside Shadow DOM via querySelector logic below)
this.updateNavState(pageId);
// Load specific page data if needed
if (pageId === 'services') this.loadServices();
if (pageId === 'projects') this.loadProjects();
// Close mobile menu if open
this.closeMobileMenu();
}
updateNavState(activeId) {
// Since header is shadow DOM, we need to access it specifically
const header = document.querySelector('custom-header');
if (header && header.shadowRoot) {
const links = header.shadowRoot.querySelectorAll('.nav-link');
links.forEach(link => {
if (link.getAttribute('data-target') === activeId) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
}
closeMobileMenu() {
const header = document.querySelector('custom-header');
if (header && header.shadowRoot) {
const mobileMenu = header.shadowRoot.querySelector('.mobile-menu');
if (mobileMenu) {
mobileMenu.classList.remove('open');
}
}
}
async loadServices() {
const grid = document.getElementById('services-grid');
if (!grid) return;
// Check if already loaded to prevent re-render
if (grid.children.length > 1 && !grid.querySelector('.spinner')) return;
// Simulate API delay
grid.innerHTML = '<div class="col-span-full flex justify-center py-10"><div class="spinner"></div></div>';
setTimeout(() => {
grid.innerHTML = servicesData.map(service => `
<div class="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-2xl transition duration-300 group flex flex-col">
<div class="h-48 overflow-hidden relative">
<img src="${service.image}" alt="${service.title}" class="w-full h-full object-cover group-hover:scale-110 transition duration-500">
<div class="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition"></div>
</div>
<div class="p-8 flex-grow flex flex-col">
<div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center text-primary-600 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
${this.getIconPath(service.icon)}
</svg>
</div>
<h4 class="text-xl font-bold mb-2">${service.title}</h4>
<p class="text-gray-600 mb-4 flex-grow">${service.desc}</p>
<button onclick="window.router.navigate('contact')" class="text-secondary-600 font-bold hover:text-secondary-800 inline-flex items-center self-start cursor-pointer bg-transparent border-0 p-0">
Request Service
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ml-1"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</button>
</div>
</div>
`).join('');
}, 600);
}
getIconPath(iconName) {
const icons = {
'log-out': '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
'disc': '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/>',
'scissors': '<circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/>',
'alert-triangle': '<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
'map': '<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>',
'anchor': '<circle cx="12" cy="5" r="3"/><line x1="12" y1="22" x2="12" y2="8"/><path d="M5 12H2a10 10 0 0 0 20 0h-3"/>'
};
return icons[iconName] || icons['log-out'];
}
loadProjects() {
const grid = document.getElementById('gallery-grid');
if (!grid) return;
if (grid.children.length > 0) return;
const html = projectsData.map(proj => `
<div class="relative group overflow-hidden rounded-lg cursor-pointer aspect-[4/3]">
<img src="${proj.img}" alt="${proj.title}" class="w-full h-full object-cover transition duration-500 group-hover:scale-110">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition duration-300 flex flex-col justify-end p-6">
<h4 class="text-white font-bold text-xl">${proj.title}</h4>
<p class="text-gray-300 text-sm flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
${proj.location}
</p>
</div>
</div>
`).join('');
grid.innerHTML = html;
}
}
// Initialize Router when DOM is ready
let router;
document.addEventListener('DOMContentLoaded', () => {
router = new Router();
// Expose to window for onclick handlers
window.router = router;
});