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; | |
| }); |