WhatsApp Icon css/style.css (Basic Styling) css /* --- Global Styles --- */ :root { --primary-white: #FFFFFF; --secondary-gold: #DAA520; --accent-green: #006400; --dark-text: #333; --light-text: #666; --bg-light: #f8f8f8; --transition-speed: 0.3s; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Lato', sans-serif; line-height: 1.6; color: var(--dark-text); background-color: var(--primary-white); overflow-x: hidden; /* Prevent horizontal scroll */ scroll-behavior: smooth; } .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; } h1, h2, h3, h4, h5, h6 { font-family: 'Montserrat', sans-serif; margin-bottom: 15px; line-height: 1.3; } h1 { font-size: 3em; } h2 { font-size: 2.5em; } h3 { font-size: 1.8em; } p { margin-bottom: 15px; } a { color: var(--accent-green); text-decoration: none; transition: color var(--transition-speed) ease; } a:hover { color: var(--secondary-gold); } img { max-width: 100%; height: auto; display: block; } .section { padding: 80px 0; position: relative; overflow: hidden; /* For 3D elements within sections */ } .section-title { text-align: center; margin-bottom: 50px; color: var(--secondary-gold); position: relative; z-index: 2; /* Ensure text is above potential 3D elements */ } .btn { display: inline-block; padding: 12px 25px; border: none; border-radius: 5px; cursor: pointer; font-family: 'Montserrat', sans-serif; font-weight: 700; text-transform: uppercase; transition: background-color var(--transition-speed) ease, transform var(--transition-speed) ease; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); } .btn-primary { background-color: var(--secondary-gold); color: var(--primary-white); } .btn-primary:hover { background-color: #e09f00; transform: translateY(-2px); } .btn-secondary { background-color: var(--accent-green); color: var(--primary-white); } .btn-secondary:hover { background-color: #005000; transform: translateY(-2px); } /* --- Loader --- */ #loader-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--primary-white); display: flex; justify-content: center; align-items: center; z-index: 9999; transition: opacity 0.5s ease-out; } .loader { border: 8px solid #f3f3f3; /* Light grey */ border-top: 8px solid var(--secondary-gold); /* Gold */ border-radius: 50%; width: 60px; height: 60px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* --- Header --- */ #main-header { position: fixed; top: 0; left: 0; width: 100%; background-color: rgba(255, 255, 255, 0.9); padding: 15px 0; z-index: 1000; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); backdrop-filter: blur(5px); } #main-header .container { display: flex; justify-content: space-between; align-items: center; } .logo img { height: 40px; /* Adjust as needed */ } #main-nav ul { list-style: none; display: flex; } #main-nav ul li { margin-left: 25px; } #main-nav ul li a { font-weight: 700; color: var(--dark-text); transition: color var(--transition-speed) ease; position: relative; } #main-nav ul li a::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 0%; height: 2px; background-color: var(--secondary-gold); transition: width var(--transition-speed) ease; } #main-nav ul li a:hover { color: var(--secondary-gold); } #main-nav ul li a:hover::after { width: 100%; } #inquiry-cart-btn { padding: 8px 18px; font-size: 0.9em; } /* --- Hero Section --- */ #hero { height: 100vh; display: flex; justify-content: center; align-items: center; text-align: center; background-color: var(--bg-light); /* Fallback if 3D fails */ position: relative; overflow: hidden; } #hero-canvas-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; } .hero-content { position: relative; z-index: 2; color: var(--primary-white); /* To ensure text visibility over 3D globe */ text-shadow: 0 2px 10px rgba(0,0,0,0.3); } .hero-content h1 { font-size: 3.5em; margin-bottom: 20px; color: var(--primary-white); /* White for contrast on dark globe */ } .hero-content p { font-size: 1.4em; color: rgba(255, 255, 255, 0.9); } .hero-cta { margin-top: 40px; } /* --- Products Section --- */ #products .section-title { color: var(--dark-text); /* Or use gold again */ } .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 30px; } .product-card { background-color: var(--primary-white); border-radius: 10px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08); padding: 25px; text-align: center; transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease; position: relative; overflow: hidden; /* For 3D elements inside */ cursor: pointer; display: flex; flex-direction: column; justify-content: space-between; } .product-card:hover { transform: translateY(-10px); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15); } .product-card .card-icon { font-size: 3em; margin-bottom: 15px; color: var(--secondary-gold); } .product-card h3 { font-size: 1.5em; margin-bottom: 10px; color: var(--dark-text); } .product-card p { font-size: 0.95em; color: var(--light-text); flex-grow: 1; /* Allows description to take available space */ margin-bottom: 20px; } .product-card .card-actions { display: flex; justify-content: center; gap: 10px; margin-top: auto; /* Pushes buttons to the bottom */ } .product-card .btn-sm { padding: 8px 15px; font-size: 0.85em; } /* --- About/Contact --- */ #about .section-title, #contact .section-title { color: var(--dark-text); } .about-content, .contact-content { display: flex; flex-wrap: wrap; gap: 40px; justify-content: center; } .about-text, .contact-form-wrapper { flex: 1; min-width: 300px; } .contact-form label { display: block; margin-bottom: 8px; font-weight: 700; } .contact-form input, .contact-form textarea { width: 100%; padding: 12px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 5px; font-size: 1em; font-family: 'Lato', sans-serif; } .contact-form textarea { min-height: 150px; resize: vertical; } .contact-form .btn-primary { width: auto; display: block; /* Ensure button takes full width if needed, or set to auto */ margin-top: 10px; } /* --- Floating WhatsApp Button --- */ .whatsapp-float-btn { position: fixed; bottom: 30px; right: 30px; z-index: 1001; width: 60px; height: 60px; background-color: #25D366; /* WhatsApp green */ border-radius: 50%; display: flex; justify-content: center; align-items: center; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); transition: transform var(--transition-speed) ease; } .whatsapp-float-btn:hover { transform: scale(1.1); } .whatsapp-float-btn img { width: 35px; height: 35px; } /* --- Footer --- */ #main-footer { background-color: #333; color: rgba(255, 255, 255, 0.8); padding: 30px 0; text-align: center; font-size: 0.9em; } #main-footer .container { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; } #main-footer p { margin-bottom: 5px; } #main-footer a { color: var(--secondary-gold); } #main-footer a:hover { color: var(--primary-white); } /* --- Utility Classes --- */ .scroll-reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.8s ease-out, transform 0.8s ease-out; } .scroll-reveal.active { opacity: 1; transform: translateY(0); } /* --- Media Queries --- */ @media (max-width: 768px) { h1 { font-size: 2.5em; } h2 { font-size: 2em; } .hero-content h1 { font-size: 3em; } .hero-content p { font-size: 1.2em; } #main-header .container { flex-direction: column; } #main-nav ul { margin-top: 15px; justify-content: center; flex-wrap: wrap; } #main-nav ul li { margin: 5px 10px; } .products-grid { grid-template-columns: 1fr; } .about-content, .contact-content { flex-direction: column; align-items: center; } .whatsapp-float-btn { bottom: 20px; right: 20px; width: 50px; height: 50px; } .whatsapp-float-btn img { width: 30px; height: 30px; } #main-footer .container { flex-direction: column; } .footer-info, .footer-dev-info { margin-bottom: 15px; } } js/spa.js (SPA Router and Content Loader) javascript // js/spa.js const appContent = document.getElementById('app-content'); const loaderContainer = document.getElementById('loader-container'); let currentPage = null; const routes = { '/': { template: 'views/home.html', script: 'js/pages/home.js', init: initHomePage, destroy: null // Add if cleanup is needed }, '/products': { template: 'views/products.html', script: 'js/pages/products.js', init: initProductsPage, destroy: null }, '/about': { template: 'views/about.html', script: 'js/pages/about.js', init: initAboutPage, destroy: null }, '/contact': { template: 'views/contact.html', script: 'js/pages/contact.js', init: initContactPage, destroy: null }, '/rfq': { template: 'views/rfq.html', script: 'js/pages/rfq.js', init: initRfqPage, destroy: null } // Add more routes as needed }; function showLoader() { loaderContainer.style.opacity = '1'; loaderContainer.style.display = 'flex'; } function hideLoader() { loaderContainer.style.opacity = '0'; setTimeout(() => { loaderContainer.style.display = 'none'; }, 500); } async function loadPage(path) { showLoader(); const route = routes[path] || routes['/']; // Default to home if path not found try { // Clean up previous page if it exists and has a destroy function if (currentPage && currentPage.destroy) { currentPage.destroy(); } // Load HTML template const response = await fetch(route.template); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const html = await response.text(); appContent.innerHTML = html; // Load and execute page-specific JavaScript if (route.script) { // Dynamically import the script. This ensures it only runs when needed // and can be cleared if necessary. For simplicity here, we'll use a tag. // A more robust solution uses dynamic import() and handles script removal. const scriptElement = document.createElement('script'); scriptElement.src = route.script; scriptElement.onload = () => { // Initialize page-specific logic if (route.init) { route.init(); } currentPage = route; // Update current page reference hideLoader(); applyScrollReveal(); // Re-apply scroll reveal after content load }; scriptElement.onerror = () => { console.error(`Failed to load script: ${route.script}`); hideLoader(); // Hide loader even on error }; document.body.appendChild(scriptElement); } else { // No specific script, just initialize if a function is provided if (route.init) { route.init(); } currentPage = route; hideLoader(); applyScrollReveal(); } } catch (error) { console.error('Error loading page:', error); appContent.innerHTML = '

Error loading content. Please try again later.

'; hideLoader(); } } function navigateTo(path) { if (window.location.pathname !== path) { window.history.pushState({}, '', path); loadPage(path); } } window.onpopstate = () => { loadPage(window.location.pathname); }; // Initial load document.addEventListener('DOMContentLoaded', () => { const initialPath = window.location.pathname === '/' ? '/' : '/' + window.location.pathname.split('/').pop(); loadPage(initialPath); // Event delegation for navigation links document.body.addEventListener('click', (e) => { const link = e.target.closest('a[href^="#"]'); // For anchor links within the same page if (!link) return; const targetId = link.getAttribute('href').substring(1); const targetElement = document.getElementById(targetId); if (targetElement) { e.preventDefault(); targetElement.scrollIntoView({ behavior: 'smooth' }); // Update URL for internal anchors if desired (optional) // window.history.pushState(null, null, `#${targetId}`); } }); document.body.addEventListener('click', (e) => { const link = e.target.closest('a[href^="/"]'); // For SPA navigation links if (link) { e.preventDefault(); navigateTo(link.getAttribute('href')); } }); }); // Basic Scroll Reveal Implementation const scrollRevealElements = document.querySelectorAll('.scroll-reveal'); function applyScrollReveal() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('active'); observer.unobserve(entry.target); // Stop observing once revealed } }); }, { root: null, // Use viewport threshold: 0.1 // Trigger when 10% of the element is visible }); scrollRevealElements.forEach(el => { observer.observe(el); }); } // Initial call for elements already in view on load document.addEventListener('DOMContentLoaded', () => { applyScrollReveal(); }); js/three-setup.js (General Three.js Setup) javascript // js/three-setup.js import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // Make THREE available globally or pass it around as needed window.THREE = THREE; window.GLTFLoader = GLTFLoader; window.OrbitControls = OrbitControls; let scene, camera, renderer; export function initThree(containerId, options = {}) { const container = document.getElementById(containerId); if (!container) { console.error(`3D container with ID "${containerId}" not found.`); return null; } scene = new THREE.Scene(); // Basic camera setup, can be adjusted or made more complex based on scene needs camera = new THREE.PerspectiveCamera(options.fov || 75, container.clientWidth / container.clientHeight, 0.1, 1000); camera.position.z = 5; // Default position renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); // For sharper rendering on high-res displays container.appendChild(renderer.domElement); // Handle window resizing window.addEventListener('resize', () => { const width = container.clientWidth; const height = container.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); }, false); // Optional: Basic lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); directionalLight.position.set(5, 5, 5); scene.add(directionalLight); // Add more lights as needed for specific scenes return { scene, camera, renderer, controls: null, loader: new GLTFLoader() }; } export function animate() { requestAnimationFrame(animate); if (renderer) renderer.render(scene, camera); } export function addControls(camera, domElement) { const controls = new OrbitControls(camera, domElement); controls.enableDamping = true; // Smooth camera movement controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.minDistance = 1; // Adjust as needed controls.maxDistance = 500; // Adjust as needed controls.maxPolarAngle = Math.PI / 2; // Prevent camera from going below the scene return controls; } // Expose scene, camera, renderer for global access or pass them to scene modules export { scene, camera, renderer }; js/scenes/heroScene.js (Hero Section 3D Scene) javascript // js/scenes/heroScene.js import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // Assuming you might use a texture for the globe // import globeTextureUrl from '../assets/textures/earth_texture.jpg'; // Placeholder let scene, camera, renderer; let globeMesh, globeGeometry; let tradeRouteLines = []; let particleSystem; let particles = []; let numParticles = 2000; let particleSpeed = 0.05; let particleSpread = 100; let globeRadius = 10; // Base radius for the globe let containerElement; // Reference to the div where the canvas will be appended // Store the Three.js objects created by this scene let heroSceneObjects = { globe: null, particles: null, routes: [] }; // Function to initialize the hero scene export function initHeroScene(threeConfig) { ({ scene, camera, renderer } = threeConfig); containerElement = document.getElementById('hero-canvas-container'); if (!containerElement) { console.error("Hero canvas container not found!"); return; } // Set up the canvas size to match the container const width = containerElement.clientWidth; const height = containerElement.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); // --- Globe --- globeGeometry = new THREE.SphereGeometry(globeRadius, 64, 64); // Basic material, can be enhanced with texture or custom shaders const globeMaterial = new THREE.MeshPhongMaterial({ color: 0x006400, // Deep Green color specular: 0xaaaaaa, shininess: 30, // You'd typically use a texture for a realistic globe: // map: new THREE.TextureLoader().load(globeTextureUrl) }); globeMesh = new THREE.Mesh(globeGeometry, globeMaterial); scene.add(globeMesh); heroSceneObjects.globe = globeMesh; // --- Trade Routes (Example) --- // Define some example routes (simplified coordinates) const routesData = [ // Pakistan to Europe { start: { lat: 30, lon: 70 }, end: { lat: 52, lon: 10 } }, // Pakistan to East Asia { start: { lat: 30, lon: 70 }, end: { lat: 35, lon: 115 } }, // Pakistan to Africa { start: { lat: 30, lon: 70 }, end: { lat: -20, lon: 30 } } ]; routesData.forEach(route => { const startVector = latLonToVector3(route.start.lat, route.start.lon, globeRadius + 0.1); // Slightly above the globe const endVector = latLonToVector3(route.end.lat, route.end.lon, globeRadius + 0.1); const curve = new THREE.CatmullRomCurve3([ startVector, startVector.clone().lerp(endVector, 0.5).add(new THREE.Vector3(0, 0, 5)), // Mid-point arc endVector ]); const geometry = new THREE.TubeGeometry(curve, 64, 0.05, 20, false); const material = new THREE.MeshBasicMaterial({ color: 0xFFD700, // Gold for routes transparent: true, opacity: 0.8, wireframe: false // Set to true to see the tube structure }); const lineMesh = new THREE.Mesh(geometry, material); scene.add(lineMesh); heroSceneObjects.routes.push(lineMesh); }); // --- Animated Particles (Stars/Dust) --- const particleGeometry = new THREE.BufferGeometry(); const positions = new Float32Array(numParticles * 3); for (let i = 0; i < numParticles; i++) { // Distribute particles in a spherical shell around the globe const phi = Math.random() * Math.PI * 2; const theta = Math.random() * Math.PI; const r = globeRadius + particleSpread * (0.5 + Math.random()); // Distribute them outwards positions[i * 3] = r * Math.sin(theta) * Math.cos(phi); positions[i * 3 + 1] = r * Math.cos(theta); positions[i * 3 + 2] = r * Math.sin(theta) * Math.sin(phi); } particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const particleMaterial = new THREE.PointsMaterial({ color: 0xFFFFFF, size: 0.1, // Size of each particle transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending // Makes particles glow }); particleSystem = new THREE.Points(particleGeometry, particleMaterial); scene.add(particleSystem); heroSceneObjects.particles = particleSystem; // Set initial camera position camera.position.set(0, 0, globeRadius * 2); // Zoomed out initially // Add some basic orbit controls for debugging/interaction if needed // const controls = addControls(camera, renderer.domElement); // Requires OrbitControls from three-setup.js // Call update particles to set their initial velocities/positions updateParticles(); // Add other 3D elements for the hero section here (e.g., subtle parallax background elements) } // Helper function to convert Latitude/Longitude to 3D Cartesian coordinates function latLonToVector3(lat, lon, radius) { const phi = (90 - lat) * Math.PI / 180; const theta = (lon + 180) * Math.PI / 180; const x = -(radius * Math.sin(phi) * Math.cos(theta)); const y = (radius * Math.cos(phi)); const z = (radius * Math.sin(phi) * Math.sin(theta)); return new THREE.Vector3(x, y, z); } // Function to animate elements specific to the hero scene export function animateHeroScene(deltaTime) { if (globeMesh) { // Rotate the globe slowly globeMesh.rotation.y += 0.0005; // Adjust rotation speed } // Animate trade route glow/pulsing (optional) heroSceneObjects.routes.forEach(route => { if (route.material.opacity > 0.5) { route.material.opacity -= 0.005; } else { route.material.opacity = 0.8; } }); // Update particle positions for a flowing effect if (particleSystem) { updateParticles(); } } // Function to update particle positions (simple animation) function updateParticles() { const positions = particleSystem.geometry.attributes.position.array; for (let i = 0; i < numParticles; i++) { // Move particles towards or away from the globe const vector = new THREE.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]); const direction = vector.normalize(); // Direction from origin // Adjust position to simulate a subtle drift or movement // For a static starfield, no update is needed. For a flowing feel: positions[i * 3] += direction.x * particleSpeed * Math.random(); positions[i * 3 + 1] += direction.y * particleSpeed * Math.random(); positions[i * 3 + 2] += direction.z * particleSpeed * Math.random(); // Optional: Wrap particles around if they go too far if (vector.length() > globeRadius + particleSpread * 2) { const phi = Math.random() * Math.PI * 2; const theta = Math.random() * Math.PI; const r = globeRadius + particleSpread * (0.5 + Math.random()); positions[i * 3] = r * Math.sin(theta) * Math.cos(phi); positions[i * 3 + 1] = r * Math.cos(theta); positions[i * 3 + 2] = r * Math.sin(theta) * Math.sin(phi); } } particleSystem.geometry.attributes.position.needsUpdate = true; } // Function to handle scene resizing export function resizeHeroScene() { if (!containerElement) return; const width = containerElement.clientWidth; const height = containerElement.clientHeight; if (camera) { camera.aspect = width / height; camera.updateProjectionMatrix(); } if (renderer) { renderer.setSize(width, height); } } // Function to clean up scene objects (call when leaving the hero section) export function destroyHeroScene() { if (heroSceneObjects.globe) scene.remove(heroSceneObjects.globe); if (heroSceneObjects.particles) scene.remove(heroSceneObjects.particles); heroSceneObjects.routes.forEach(route => scene.remove(route)); globeMesh = null; globeGeometry = null; tradeRouteLines = []; particleSystem = null; particles = []; heroSceneObjects = { globe: null, particles: null, routes: [] }; // Clean up event listeners if added } js/scenes/productsScene.js (Products Section 3D Elements) This would involve loading individual 3D models for categories or specific products if you have them. For this example, we’ll use simple geometries to represent categories. javascript // js/scenes/productsScene.js import * as THREE from 'three'; let scene, camera, renderer; let categoryMeshes = {}; // Store meshes for each category let categoryElements = {}; // Store references to the DOM elements for interactivity let interactiveObjects = []; // Objects that can be clicked/hovered const categories = [ { id: 'minerals', name: 'Minerals', color: 0xAAAAAA, icon: 'πŸ’Ž' }, // Example: Grey { id: 'agri', name: 'Agri Products', color: 0x8B4513, icon: '🌾' }, // Example: Brown { id: 'fresh-produce', name: 'Fresh Produce', color: 0x32CD32, icon: 'πŸ“' }, // Example: Lime Green { id: 'livestock', name: 'Livestock', color: 0xA0522D, icon: 'πŸ„' }, // Example: Sienna { id: 'dry-fruits', name: 'Dry Fruits', color: 0xD2B48C, icon: '🌰' }, // Example: Tan { id: 'forage', name: 'Forage', color: 0x556B2F, icon: '🌿' } // Example: Dark Olive Green ]; let raycaster; let mouse = new THREE.Vector2(); let INTERSECTED = null; // The object currently being hovered over export function initProductsScene(threeConfig) { ({ scene, camera, renderer } = threeConfig); raycaster = new THREE.Raycaster(); const productsContainer = document.getElementById('products-3d-canvas-container'); // Assuming you have this div if (!productsContainer) { console.warn("Products 3D canvas container not found. Skipping products scene init."); return; } // Set up camera and renderer for this section if it's a dedicated 3D canvas // If it's more of a background effect, you might reuse the main canvas. // For this example, let's assume it's a separate element for distinct interaction. // A more advanced setup would integrate this into the main SPA rendering loop. const width = productsContainer.clientWidth; const height = productsContainer.clientHeight; // You might need to adjust camera.aspect and position specifically for this scene. // camera.position.z = 20; // Example: move camera back // --- Create Category Geometries (e.g., Spheres, Cubes) --- const spacing = 5; // Distance between category objects const radius = 3; // Base size categories.forEach((cat, index) => { const geometry = new THREE.SphereGeometry(radius, 32, 32); const material = new THREE.MeshPhongMaterial({ color: cat.color, transparent: true, opacity: 0.8, specular: 0x555555, shininess: 20 }); const mesh = new THREE.Mesh(geometry, material); // Position the categories in a semi-circle or grid const angle = (index / categories.length) * Math.PI * 1.2 - Math.PI * 0.6; // Arrange in an arc mesh.position.x = Math.sin(angle) * spacing * 2; mesh.position.y = Math.cos(angle) * spacing * 1.5; mesh.position.z = 0; // Or adjust z for depth scene.add(mesh); categoryMeshes[cat.id] = mesh; interactiveObjects.push(mesh); // Add to interactable list // Link DOM elements (product cards) to these 3D objects // This assumes you have corresponding divs in your 'views/products.html' const domElement = document.getElementById(`product-category-${cat.id}`); if (domElement) { categoryElements[cat.id] = domElement; domElement.dataset.categoryId = cat.id; // Add data attribute for linking } }); // Add event listener for mouse interaction on the product canvas if (productsContainer) { productsContainer.addEventListener('mousemove', onMouseMove); productsContainer.addEventListener('click', onClick); } } function onMouseMove(event) { // Calculate mouse position in normalized device coordinates (-1 to +1) const rect = event.target.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // Update the raycaster with the camera and mouse position raycaster.setFromCamera(mouse, camera); // Find objects intersecting the ray const intersects = raycaster.intersectObjects(interactiveObjects); if (intersects.length > 0) { if (INTERSECTED !== intersects[0].object) { // If we're hovering over a new object if (INTERSECTED) { // Restore previous object's material INTERSECTED.material.opacity = 0.8; // Restore opacity } INTERSECTED = intersects[0].object; INTERSECTED.material.opacity = 1; // Highlight hovered object } } else { // If no object is intersected if (INTERSECTED) { INTERSECTED.material.opacity = 0.8; // Restore opacity } INTERSECTED = null; } } function onClick(event) { if (INTERSECTED) { const categoryId = Object.keys(categoryMeshes).find(key => categoryMeshes[key] === INTERSECTED); if (categoryId && categoryElements[categoryId]) { // Scroll to the corresponding DOM element for the category categoryElements[categoryId].scrollIntoView({ behavior: 'smooth' }); // Optionally, trigger a visual cue or animation on the DOM element categoryElements[categoryId].classList.add('highlight'); setTimeout(() => { categoryElements[categoryId].classList.remove('highlight'); }, 1000); } // You could also open a modal or load a specific product page here } } // Function to animate elements specific to the products scene export function animateProductsScene(deltaTime) { // Example: Rotate category meshes slowly for (const id in categoryMeshes) { categoryMeshes[id].rotation.y += 0.002; } // Handle mouse hover effects if not using raycaster (e.g., via CSS) } // Function to clean up scene objects export function destroyProductsScene() { // Remove objects from scene for (const id in categoryMeshes) { if (categoryMeshes[id]) { scene.remove(categoryMeshes[id]); categoryMeshes[id].geometry.dispose(); categoryMeshes[id].material.dispose(); } } categoryMeshes = {}; interactiveObjects = []; INTERSECTED = null; // Remove event listeners const productsContainer = document.getElementById('products-3d-canvas-container'); if (productsContainer) { productsContainer.removeEventListener('mousemove', onMouseMove); productsContainer.removeEventListener('click', onClick); } } views/home.html (Content for the Home Page) html

Connecting Pakistan’s Resources to Global Markets.

Your premier partner for high-quality exports.

About Ansar Trader IMEX

Ansar Trader IMEX (SMC-PRIVATE) LIMITED is a dynamic and forward-thinking export company based in Quetta, Pakistan. We are dedicated to showcasing Pakistan's rich natural resources and agricultural bounty to the world.

Leveraging our deep understanding of local sourcing and global logistics, we ensure that our clients receive products of the highest quality, delivered efficiently and reliably.

Our commitment to integrity, customer satisfaction, and excellence drives us to build lasting partnerships across international markets. We believe in connecting Pakistan’s finest to your doorstep.

Learn More About Us
Ansar Trader IMEX Exports

Get in Touch

Have questions or specific requirements? We're here to help. Connect with us today!

views/products.html (Content for the Products Page) html

Our Export Products

Minerals

Explore our comprehensive range of high-quality minerals, crucial for various industrial and manufacturing processes.

πŸ’Ž

Himalayan Salt

Pure, naturally formed rock salt, renowned for its mineral content and distinct pink hue. Available in various grades for culinary, therapeutic, and industrial use.

Email Inquiry
⛏️

Chromite Ore

High-grade Chromite ore, essential for stainless steel production and other industrial applications. Consistent quality and supply guaranteed.

Email Inquiry
🧱

Gypsum

Finely processed Gypsum, used extensively in construction (plasterboard, cement) and agriculture (soil conditioner).

Email Inquiry
✨

Talc Powder

High-purity Talc powder, versatile for use in plastics, ceramics, paper, cosmetics, and pharmaceuticals.

Email Inquiry

Agricultural Products

Discover Pakistan's finest agricultural produce, meticulously cultivated and processed for international markets.

🌾

Wheat

High-quality Pakistani wheat varieties, crucial for global food security. Consistent supply and compliance with international standards.

Email Inquiry
🍚

Basmati Rice

The world-renowned aromatic Basmati rice, characterized by its long grains and exquisite flavor. Available in various grades.

Email Inquiry
🌽

Maize (Corn)

Premium quality Maize, used in food, feed, and industrial applications. Sourced from fertile Pakistani plains.

Email Inquiry
🌿

Guar Gum

High-purity Guar Gum powder, a natural thickening and stabilizing agent for food, oil, and gas industries.

Email Inquiry

Fresh Produce

Taste the freshness of Pakistan's bountiful harvest. We export a variety of premium fruits and vegetables.

πŸ₯­

Mangoes

The King of Fruits! Juicy, sweet Pakistani mangoes (e.g., Chaunsa, Sindhri) exported globally during the season.

Email Inquiry
🍊

Kinnow (Mandarin Oranges)

Sweet and tangy Kinnow oranges, a popular citrus fruit from Pakistan, known for their juice content.

Email Inquiry
πŸ“

Strawberries

Fresh, ripe strawberries, cultivated for their superior taste and aroma.

Email Inquiry

Livestock & Poultry

High-quality, ethically raised livestock and poultry products meeting international standards.

πŸ„

Beef

Premium Halal beef cuts, sourced from healthy cattle, processed under strict hygiene protocols.

Email Inquiry
πŸ”

Chicken

High-quality broiler chicken, Halal certified, and processed for domestic and international markets.

Email Inquiry
πŸ₯š

Eggs

Fresh, high-quality chicken eggs, packed according to export standards.

Email Inquiry

Dry Fruits

Nutrient-rich and flavorful dry fruits sourced from Pakistan's finest harvests.

🌰

Walnuts

Premium quality walnuts, offering a rich taste and texture for snacking and culinary use.

Email Inquiry
πŸ₯œ

Almonds

High-grade almonds, known for their nutritional value and versatility.

Email Inquiry

Forage Crops

High-quality forage crops for animal feed, ensuring optimal nutrition for livestock.

🌿

Corn Silage

Nutrient-rich corn silage, a vital feed component for dairy and beef cattle.

Email Inquiry
🌿

Rhodes Grass Hay

High-quality hay made from Rhodes grass, providing essential fiber for livestock.

Email Inquiry
views/about.html (Content for the About Page) html

About Ansar Trader IMEX

Ansar Trader IMEX Team & Operations

Ansar Trader IMEX (SMC-PRIVATE) LIMITED is a Pakistan-based export enterprise committed to bridging global markets with Pakistan's finest resources. Our core mission is to facilitate seamless international trade, ensuring the highest standards of quality and service for our clients worldwide.

Our Vision

To be a globally recognized leader in Pakistan's export sector, renowned for our reliability, product quality, and commitment to ethical business practices.

Our Mission

To consistently deliver superior export products from Pakistan by fostering strong supplier relationships, employing stringent quality control measures, and providing exceptional logistical support.

Our Values

  • Integrity: Upholding the highest ethical standards in all our dealings.
  • Quality: Committing to excellence in every product we export.
  • Reliability: Ensuring timely deliveries and consistent supply.
  • Customer Focus: Prioritizing client satisfaction and building long-term partnerships.
  • Innovation: Continuously seeking ways to improve our services and product offerings.

Why Choose Ansar Trader IMEX?

We offer a deep understanding of Pakistan's export landscape, from sourcing premium raw materials to navigating complex international trade regulations. Our team is dedicated to providing personalized service, ensuring your export needs are met with professionalism and efficiency.

Company Details:

Registration Number: 0246004

NTN: C344442

Address: 158-B, Phase 2, Zargoonabad, Quetta

Map: View on Google Maps

views/contact.html (Content for the Contact Page) html

Contact Us

Get in Touch

We are eager to hear from you! Whether you have an inquiry, a request for quotation, or need more information about our services, feel free to reach out.

Registration No: 0246004

NTN: C344442

Send Us a Message

views/rfq.html (Content for the RFQ Page) html

Request for Quotation (RFQ)

Please provide as much detail as possible to help us understand your needs. We will respond with a comprehensive quotation.

js/pages/home.js javascript // js/pages/home.js import { initHeroScene, animateHeroScene, destroyHeroScene, resizeHeroScene } from '../scenes/heroScene.js'; import { initThree, animate } from '../three-setup.js'; let threeConfig = null; let animationFrameId = null; export function initHomePage() { // Initialize the main 3D scene for the hero section threeConfig = initThree('hero-canvas-container'); if (threeConfig) { initHeroScene(threeConfig); // Start the main animation loop for Three.js if (!animationFrameId) { animationFrameId = requestAnimationFrame(animateLoop); } } // Attach event listeners for internal links document.querySelectorAll('.scroll-to-section').forEach(link => { link.addEventListener('click', handleScrollToSection); }); // Add specific handlers for home page console.log('Home page initialized.'); } function animateLoop() { const deltaTime = 16; // Assume ~60fps for now, or calculate properly animateHeroScene(deltaTime); // Animate hero-specific elements animate(); // General Three.js render loop animationFrameId = requestAnimationFrame(animateLoop); } function handleScrollToSection(e) { e.preventDefault(); const targetId = e.currentTarget.getAttribute('href').substring(1); const targetElement = document.getElementById(targetId); if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth' }); // Optionally update URL // window.history.pushState({}, '', `#${targetId}`); } } export function destroyHomePage() { console.log('Destroying Home page...'); // Clean up Three.js scene specific to the hero section if (threeConfig) { destroyHeroScene(); // If you have other scenes on the home page, manage them here. // For this example, only hero is 3D. } // Stop the animation loop if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } // Remove event listeners document.querySelectorAll('.scroll-to-section').forEach(link => { link.removeEventListener('click', handleScrollToSection); }); } // Make sure to call resizeHeroScene when window resizes if it's a standalone canvas window.addEventListener('resize', () => { if (threeConfig) { resizeHeroScene(); } }); js/pages/products.js javascript // js/pages/products.js import { initProductsScene, animateProductsScene, destroyProductsScene } from '../scenes/productsScene.js'; import { initThree, animate } from '../three-setup.js'; // Re-using the main initThree let threeConfigProducts = null; let animationFrameIdProducts = null; export function initProductsPage() { // Initialize Three.js for the products scene if the container exists const productsCanvasContainer = document.getElementById('products-3d-canvas-container'); if (productsCanvasContainer) { // You might want a slightly different camera setup or controls for the products scene // For this example, we'll assume the initThree can handle multiple containers or // is called with a specific container ID. // NOTE: For multiple distinct 3D scenes on the same page, you would typically // have separate Three.js instances, cameras, and renderers. // Here, we'll assume a single renderer but potentially different scene setups. // A more robust SPA would manage multiple Three.js instances. // For this example, let's assume initThree can be called to *re-initialize* // with a different container, or we'd pass the renderer instance around. // For simplicity, let's assume initThree is smart enough to handle this or // we're just updating its internal renderer to target a new canvas. // A better approach: Pass the existing renderer and scene to initProductsScene. // Let's adjust initThree to return the renderer/scene and pass it. // For this example, we'll assume initThree needs to be called per scene. // If you only have ONE canvas in index.html for ALL 3D, then managing // different scenes on that single canvas is key. // If you have multiple dedicated divs like #hero-canvas-container and // #products-3d-canvas-container, you'd typically have separate Three.js instances. // Assuming separate instances: threeConfigProducts = initThree('products-3d-canvas-container', { fov: 50, z: 20 }); // Example: adjust fov/position if (threeConfigProducts) { initProductsScene(threeConfigProducts); // Start a separate animation loop for products scene if needed, or integrate with global // If it's a separate canvas, we'll start a loop specific to it. if (!animationFrameIdProducts) { animationFrameIdProducts = requestAnimationFrame(animateProductsLoop); } } } else { console.log("Products canvas container not found. Skipping 3D init for products."); } // Handle "Add to Inquiry" buttons document.querySelectorAll('.add-to-cart').forEach(button => { button.addEventListener('click', handleAddToCart); }); console.log('Products page initialized.'); } function animateProductsLoop() { const deltaTime = 16; // Assume ~60fps if (threeConfigProducts) { animateProductsScene(deltaTime); // Animate products-specific elements animate(); // General Three.js render loop for this scene } animationFrameIdProducts = requestAnimationFrame(animateProductsLoop); } function handleAddToCart(e) { const button = e.target; const productId = button.dataset.productId; const productName = button.dataset.productName; if (!productId || !productName) { console.error("Product ID or Name missing for cart."); return; } // Add product to cart logic (handled by cartManager.js) cartManager.addToCart({ id: productId, name: productName }); updateCartCount(); // Update the cart count in the header showFeedback(button, 'Added to Inquiry!'); } // Helper to show temporary feedback function showFeedback(element, message) { const feedbackDiv = document.createElement('div'); feedbackDiv.textContent = message; feedbackDiv.style.position = 'absolute'; feedbackDiv.style.top = '0'; feedbackDiv.style.left = '50%'; feedbackDiv.style.transform = 'translateX(-50%)'; feedbackDiv.style.backgroundColor = '#006400'; feedbackDiv.style.color = 'white'; feedbackDiv.style.padding = '5px 10px'; feedbackDiv.style.borderRadius = '3px'; feedbackDiv.style.zIndex = '10'; feedbackDiv.style.whiteSpace = 'nowrap'; const parentRect = element.getBoundingClientRect(); feedbackDiv.style.top = `${parentRect.top - 30}px`; // Position above the button feedbackDiv.style.left = `${parentRect.left + parentRect.width / 2}px`; document.body.appendChild(feedbackDiv); setTimeout(() => { feedbackDiv.style.transition = 'opacity 0.5s ease'; feedbackDiv.style.opacity = '0'; feedbackDiv.addEventListener('transitionend', () => { feedbackDiv.remove(); }); }, 1500); } export function destroyProductsPage() { console.log('Destroying Products page...'); // Clean up Three.js scene specific to products if (threeConfigProducts) { destroyProductsScene(); } // Stop the animation loop if (animationFrameIdProducts) { cancelAnimationFrame(animationFrameIdProducts); animationFrameIdProducts = null; } // Remove event listeners from buttons document.querySelectorAll('.add-to-cart').forEach(button => { button.removeEventListener('click', handleAddToCart); }); } js/pages/about.js javascript // js/pages/about.js export function initAboutPage() { console.log('About page initialized.'); // Add any specific JS interactions for the about page here, if needed. // For now, it's mostly static content. } export function destroyAboutPage() { console.log('Destroying About page...'); // Clean up any listeners or elements specific to this page. } js/pages/contact.js javascript // js/pages/contact.js export function initContactPage() { console.log('Contact page initialized.'); const contactForm = document.getElementById('contact-form'); const formFeedback = document.getElementById('form-feedback'); if (contactForm) { contactForm.addEventListener('submit', handleContactFormSubmit); } } async function handleContactFormSubmit(e) { e.preventDefault(); const form = e.target; const formData = new FormData(form); const formFeedback = document.getElementById('form-feedback'); formFeedback.textContent = 'Sending...'; formFeedback.style.color = 'var(--light-text)'; try { // IMPORTANT: This POST request should go to your serverless function or backend API. // For this example, we'll simulate a successful response. // Replace with your actual API endpoint. const response = await fetch('/.netlify/functions/sendContactEmail', { // Example Netlify function endpoint method: 'POST', body: formData, // If using FormData, you might need to process it into JSON on the server. // Or send as JSON directly if inputs are not file uploads. // headers: { 'Content-Type': 'application/json' } // body: JSON.stringify(Object.fromEntries(formData.entries())) // If sending JSON }); const result = await response.json(); // Assuming server returns JSON if (response.ok && result.success) { formFeedback.textContent = 'Message sent successfully! We will be in touch soon.'; formFeedback.style.color = 'green'; form.reset(); // Clear the form } else { formFeedback.textContent = `Failed to send message. ${result.message || ''} Please try again.`; formFeedback.style.color = 'red'; } } catch (error) { console.error('Error submitting contact form:', error); formFeedback.textContent = 'An unexpected error occurred. Please try again later.'; formFeedback.style.color = 'red'; } } export function destroyContactPage() { console.log('Destroying Contact page...'); const contactForm = document.getElementById('contact-form'); if (contactForm) { contactForm.removeEventListener('submit', handleContactFormSubmit); } } js/pages/rfq.js javascript // js/pages/rfq.js export function initRfqPage() { console.log('RFQ page initialized.'); const rfqForm = document.getElementById('rfq-form'); const rfqFormFeedback = document.getElementById('rfq-form-feedback'); if (rfqForm) { rfqForm.addEventListener('submit', handleRfqFormSubmit); } } async function handleRfqFormSubmit(e) { e.preventDefault(); const form = e.target; const formData = new FormData(form); const rfqFormFeedback = document.getElementById('rfq-form-feedback'); rfqFormFeedback.textContent = 'Submitting RFQ...'; rfqFormFeedback.style.color = 'var(--light-text)'; try { // IMPORTANT: This POST request should go to your serverless function or backend API. // Replace with your actual API endpoint. const response = await fetch('/.netlify/functions/sendRfqEmail', { // Example Netlify function endpoint method: 'POST', body: formData, // body: JSON.stringify(Object.fromEntries(formData.entries())) // If sending JSON }); const result = await response.json(); // Assuming server returns JSON if (response.ok && result.success) { rfqFormFeedback.textContent = 'RFQ submitted successfully! We will review and get back to you shortly.'; rfqFormFeedback.style.color = 'green'; form.reset(); // Clear the form } else { rfqFormFeedback.textContent = `Failed to submit RFQ. ${result.message || ''} Please try again.`; rfqFormFeedback.style.color = 'red'; } } catch (error) { console.error('Error submitting RFQ form:', error); rfqFormFeedback.textContent = 'An unexpected error occurred. Please try again later.'; rfqFormFeedback.style.color = 'red'; } } export function destroyRfqPage() { console.log('Destroying RFQ page...'); const rfqForm = document.getElementById('rfq-form'); if (rfqForm) { rfqForm.removeEventListener('submit', handleRfqFormSubmit); } } js/components/ProductCard.js (Conceptual - might not need a separate file if logic is simple) This logic is mostly embedded within js/pages/products.js for add-to-cart buttons. js/components/InquiryCart.js (Cart Manager) This will be crucial for managing the inquiry cart’s state. javascript // js/components/InquiryCart.js // Use localStorage for persistence across sessions, or a simple array for session-only. const CART_STORAGE_KEY = 'ansarTraderImexCart'; let cart = { items: [], // Add other properties like total quantity, etc. if needed }; // Load cart from localStorage if it exists function loadCart() { const storedCart = localStorage.getItem(CART_STORAGE_KEY); if (storedCart) { try { cart = JSON.parse(storedCart); // Ensure items array exists and is an array if (!Array.isArray(cart.items)) { cart.items = []; } } catch (e) { console.error("Error parsing cart from localStorage:", e); cart = { items: [] }; // Reset to empty if parsing fails } } } // Save cart to localStorage function saveCart() { localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart)); } // Add item to cart function addItem(item) { const existingItemIndex = cart.items.findIndex(cartItem => cartItem.id === item.id); if (existingItemIndex > -1) { // Item already in cart, could increment quantity or just update. // For this use case, we likely want unique products, so we don't increment quantity. // If product is added again, we just ensure it's there. // Or, if we allow quantity: // cart.items[existingItemIndex].quantity += item.quantity || 1; console.log(`Item ${item.name} already in cart.`); // Optionally update name if it changed (though unlikely for product buttons) cart.items[existingItemIndex].name = item.name; } else { // Add new item cart.items.push({ id: item.id, name: item.name, // quantity: item.quantity || 1 // If you support quantity }); } saveCart(); updateCartDisplay(); // Refresh the cart UI } // Remove item from cart function removeItem(itemId) { cart.items = cart.items.filter(item => item.id !== itemId); saveCart(); updateCartDisplay(); } // Clear the entire cart function clearCart() { cart.items = []; saveCart(); updateCartDisplay(); } // Get current cart items function getItems() { return [...cart.items]; // Return a copy } // Get total number of items in cart function getTotalItems() { return cart.items.length; } // Render cart items to the DOM function updateCartDisplay() { const cartItemsContainer = document.getElementById('cart-items'); const cartCountSpan = document.getElementById('cart-count'); const cartTotalItemsSpan = document.getElementById('cart-total-items'); const inquiryCartBtn = document.getElementById('inquiry-cart-btn'); if (!cartItemsContainer || !cartCountSpan || !cartTotalItemsSpan || !inquiryCartBtn) { // If DOM elements aren't ready yet, wait. This often happens on SPA load. // console.warn("Cart display elements not found, waiting for DOM."); return; } cartItemsContainer.innerHTML = ''; // Clear current list if (cart.items.length === 0) { cartItemsContainer.innerHTML = '

Your inquiry cart is empty.

'; inquiryCartBtn.style.display = 'none'; // Hide button if empty } else { cart.items.forEach(item => { const itemElement = document.createElement('div'); itemElement.classList.add('cart-item'); itemElement.innerHTML = ` ${item.name} `; cartItemsContainer.appendChild(itemElement); }); inquiryCartBtn.style.display = 'block'; // Show button if not empty } cartCountSpan.textContent = getTotalItems(); cartTotalItemsSpan.textContent = getTotalItems(); // Add event listeners to remove buttons document.querySelectorAll('.remove-item-btn').forEach(btn => { btn.addEventListener('click', (e) => { const itemId = e.target.dataset.itemId; if (itemId) { removeItem(itemId); } }); }); } // Initialize cart manager and display document.addEventListener('DOMContentLoaded', () => { loadCart(); // Load cart on initial page load updateCartDisplay(); // Update UI based on loaded cart // Add event listeners for modal buttons const inquiryCartBtn = document.getElementById('inquiry-cart-btn'); const cartModal = document.getElementById('inquiry-cart-modal'); const closeCartModalBtn = cartModal.querySelector('.close-modal'); const clearCartBtn = document.getElementById('clear-cart-btn'); const checkoutCartBtn = document.getElementById('checkout-cart-btn'); if (inquiryCartBtn) inquiryCartBtn.addEventListener('click', () => cartModal.style.display = 'block'); if (closeCartModalBtn) closeCartModalBtn.addEventListener('click', () => cartModal.style.display = 'none'); if (clearCartBtn) clearCartBtn.addEventListener('click', () => { if (confirm('Are you sure you want to clear your inquiry cart?')) { clearCart(); } }); if (checkoutCartBtn) checkoutCartBtn.addEventListener('click', handleCheckoutClick); // Close modal if clicked outside window.addEventListener('click', (e) => { if (e.target === cartModal) { cartModal.style.display = 'none'; } }); }); function handleCheckoutClick() { if (cart.items.length === 0) { alert('Your inquiry cart is empty!'); return; } const checkoutModal = document.getElementById('checkout-confirmation-modal'); if (checkoutModal) { checkoutModal.style.display = 'block'; const confirmWaBtn = document.getElementById('confirm-wa-btn'); const confirmEmailBtn = document.getElementById('confirm-email-btn'); // Construct WA link let waMessage = "Hello Ansar Trader IMEX, I would like to inquire about the following products:\n\n"; cart.items.forEach(item => { waMessage += `- ${item.name}\n`; }); waMessage += "\nKindly provide more details."; // Encode message for URL const encodedMessage = encodeURIComponent(waMessage); confirmWaBtn.href = `https://wa.me/923322233383?text=${encodedMessage}`; // Attach click handler for email confirmation (requires backend or client-side PDF generation) confirmEmailBtn.addEventListener('click', handleEmailConfirmation); } } async function handleEmailConfirmation() { // This will likely involve a backend call to generate a PDF and send email // For demonstration, we'll just log and alert. alert("Generating PDF and sending email... (This requires backend implementation)"); // Example of what you'd do: // 1. Collect cart items data. // 2. Send to a backend function (e.g., /.netlify/functions/generatePdfAndEmail) // 3. The backend generates PDF and sends email. // 4. Show feedback to the user. // If using client-side PDF generation (e.g., jsPDF) - less ideal for complex data: // try { // const { jsPDF } = window.jspdf; // Assuming jsPDF is loaded globally or imported // const doc = new jsPDF(); // doc.text("Ansar Trader IMEX Inquiry", 10, 10); // let y = 20; // cart.items.forEach(item => { // doc.text(`- ${item.name}`, 10, y); // y += 10; // }); // const pdfBlob = doc.output('blob'); // // Now you'd need to upload this blob to a server or use Fetch API // // to send it via email with a backend. // // For now, alert. // alert("PDF generated (client-side). Backend required to send it."); // } catch (error) { // console.error("Error generating PDF:", error); // alert("Could not generate PDF."); // } } // Export functions for external use const cartManager = { addItem, removeItem, clearCart, getItems, getTotalItems, updateCartDisplay // Also exported for direct calls if needed }; // Ensure cart is loaded and displayed when the module is imported and DOM is ready loadCart(); // Call once on load updateCartDisplay(); // Call once on load js/main.js (The main entry point and setup) javascript // js/main.js // Import all page controllers import { initHomePage, destroyHomePage } from './pages/home.js'; import { initProductsPage, destroyProductsPage } from './pages/products.js'; import { initAboutPage, destroyAboutPage } from './pages/about.js'; import { initContactPage, destroyContactPage } from './pages/contact.js'; import { initRfqPage, destroyRfqPage } from './pages/rfq.js'; // Import cart manager for global access import './components/InquiryCart.js'; // This initializes cartManager and event listeners // Keep track of the currently active page's cleanup function let currentDestroyFunction = null; // Centralized Three.js animation loop management let globalAnimationFrameId = null; let activeScenes = []; // Store functions to animate active 3D scenes export function register3DSceneAnimation(animateFunction) { activeScenes.push(animateFunction); } export function unregister3DSceneAnimation(animateFunction) { const index = activeScenes.indexOf(animateFunction); if (index > -1) { activeScenes.splice(index, 1); } } function globalAnimateLoop() { const deltaTime = 16; // Approximate delta time for ~60fps // Animate all active 3D scenes activeScenes.forEach(animateFn => { try { animateFn(deltaTime); } catch (error) { console.error("Error animating 3D scene:", error); } }); // If any Three.js renderer is active (e.g., from three-setup.js) if (window.renderer) { // Assuming 'renderer' is globally exposed or passed try { window.renderer.render(window.scene, window.camera); // Render the currently active scene } catch (error) { console.error("Error during global renderer update:", error); } } globalAnimationFrameId = requestAnimationFrame(globalAnimateLoop); } const pageControllers = { '/': { init: initHomePage, destroy: destroyHomePage }, '/products': { init: initProductsPage, destroy: destroyProductsPage }, '/about': { init: initAboutPage, destroy: destroyAboutPage }, '/contact': { init: initContactPage, destroy: destroyContactPage }, '/rfq': { init: initRfqPage, destroy: destroyRfqPage }, // Add other routes here }; function handlePageChange(path) { // Normalize path to match keys in pageControllers let routePath = path; if (path.endsWith('/')) { routePath = path.slice(0, -1); // Remove trailing slash for consistency } if (routePath === '') routePath = '/'; // Handle root path // Find the correct controller const controller = pageControllers[routePath]; // Destroy previous page's logic if it exists if (currentDestroyFunction) { currentDestroyFunction(); currentDestroyFunction = null; // Clear the reference } // Initialize new page's logic if (controller) { controller.init(); currentDestroyFunction = controller.destroy; // Store the destroy function for the next change } else { console.warn(`No controller found for path: ${routePath}. Displaying default content.`); // Optionally, handle a 404 page here } // Update SPA content and potentially trigger 3D scene animations // Note: The actual HTML content is loaded by spa.js. This function // primarily handles the JS initialization/cleanup. } // --- Initialization --- document.addEventListener('DOMContentLoaded', () => { // Start the global animation loop if it's not already running if (!globalAnimationFrameId) { globalAnimationFrameId = requestAnimationFrame(globalAnimateLoop); } // Initial page load handling (spa.js will call this indirectly via loadPage) // We need to hook into spa.js's page change mechanism. // Let's assume spa.js provides a hook or we can intercept the URL change. // The spa.js file handles the initial loading and routing. // We need to ensure our handlePageChange is called *after* spa.js // has loaded content and potentially its own JS. // Let's modify spa.js slightly or assume it calls a global handler. // For now, we'll assume `spa.js` uses `window.history.pushState` and `popstate`, // and we can listen to those or have spa.js call `handlePageChange`. // --- Intercepting spa.js page changes --- // If spa.js calls a function like `onContentLoad(path)`, we'd hook into that. // Since spa.js handles the routing and content loading, we'll rely on its // mechanism to trigger our page handlers. The `loadPage` function in `spa.js` // calls `route.init()`. We can ensure that `route.init()` in `spa.js` // correctly calls our `handlePageChange` or our page-specific init functions. // Modify spa.js loadPage: // Inside spa.js's loadPage, after loading the script and calling route.init(): // route.init() in spa.js should directly call the correct page init from this file. // Example modification in spa.js: /* scriptElement.onload = () => { if (route.init) { // Instead of just route.init(), we'll use our pageControllers map const path = window.location.pathname === '/' ? '/' : '/' + window.location.pathname.split('/').pop(); // If spa.js just calls `route.init()`, and `route.init` points to our `initHomePage`, etc. // That works directly. // OR, if spa.js calls a common handler: // handlePageChange(window.location.pathname); // This would be called from spa.js route.init(); // Assuming route.init is the correct page init function } currentPage = route; hideLoader(); applyScrollReveal(); }; */ // For this setup, spa.js correctly uses `route.init` which is our page-specific init. // So, `initHomePage` etc. are called correctly. // We just need to ensure `currentDestroyFunction` is managed. // Let's add a call to `handlePageChange` to set the initial `currentDestroyFunction`. const initialPath = window.location.pathname === '/' ? '/' : '/' + window.location.pathname.split('/').pop(); if (pageControllers[initialPath]) { currentDestroyFunction = pageControllers[initialPath].destroy; } // Update global Three.js scene and camera if they are exposed // The three-setup.js module should ideally export these for global access if needed. // For example: export { scene, camera, renderer }; // Then in main.js: import { scene as globalScene, camera as globalCamera, renderer as globalRenderer } from './three-setup.js'; // For simplicity, we'll assume they might be available on `window` or passed around. console.log("Main application initialized."); }); // Make sure spa.js correctly calls the init functions from this file. // For example, in spa.js's `loadPage` function, when it calls `route.init()`: // If `route.init` is directly pointing to `initHomePage` etc., it will work. // If `route.init` is a generic handler, that handler needs to know which page is loaded. // This `main.js` is primarily for orchestrating page-specific JS and the global animation loop. three-setup.js (Revised for clarity and global access) javascript // js/three-setup.js import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // Global variables to hold the main Three.js instance if needed for the entire app // For SPAs with multiple distinct 3D scenes, you'd typically have separate instances. // Here, we'll provide a way to get a THREE instance for a specific container. let activeThreeInstances = []; // To manage multiple renderers if needed // Global access for convenience in animation loops (use with caution) let currentScene = null; let currentCamera = null; let currentRenderer = null; // Function to initialize a Three.js instance for a specific container export function initThreeInstance(containerId, options = {}) { const container = document.getElementById(containerId); if (!container) { console.error(`3D container with ID "${containerId}" not found.`); return null; } const scene = new THREE.Scene(); // Basic camera setup, can be adjusted or made more complex based on scene needs const camera = new THREE.PerspectiveCamera(options.fov || 75, container.clientWidth / container.clientHeight, 0.1, 1000); camera.position.set(options.x || 0, options.y || 0, options.z || 5); // Default position, allow overrides const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); // For sharper rendering on high-res displays container.appendChild(renderer.domElement); // Store this instance const threeInstance = { scene, camera, renderer, container, options, controls: null, loader: new GLTFLoader() }; activeThreeInstances.push(threeInstance); // Handle window resizing for this specific instance const onResize = () => { const width = container.clientWidth; const height = container.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); }; window.addEventListener('resize', onResize); // Basic lighting - can be made more sophisticated or scene-specific const ambientLight = new THREE.AmbientLight(0xffffff, 0.8); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); directionalLight.position.set(5, 5, 5); scene.add(directionalLight); // Return the instance so scene modules can use it return threeInstance; } // Function to get the currently active renderer and scene (useful for a global loop) export function getCurrentThreeGlobals() { // This might need more sophisticated management if multiple 3D scenes are active simultaneously // For now, assume the 'latest' initialized renderer/scene is the one intended for global render. if (activeThreeInstances.length > 0) { const lastInstance = activeThreeInstances[activeThreeInstances.length - 1]; currentScene = lastInstance.scene; currentCamera = lastInstance.camera; currentRenderer = lastInstance.renderer; return { scene: currentScene, camera: currentCamera, renderer: currentRenderer }; } return { scene: null, camera: null, renderer: null }; } // Global animation loop placeholder - actual animation logic is in scene-specific files export function animate(deltaTime, scene, camera, renderer) { if (renderer && scene && camera) { renderer.render(scene, camera); } } // Add OrbitControls for a specific instance export function addControls(threeInstance) { const controls = new OrbitControls(threeInstance.camera, threeInstance.renderer.domElement); controls.enableDamping = true; // Smooth camera movement controls.dampingFactor = 0.05; controls.screenSpacePanning = false; // Adjust these limits based on your scene scale controls.minDistance = 1; controls.maxDistance = 500; controls.maxPolarAngle = Math.PI / 2; // Prevent camera from going below the scene threeInstance.controls = controls; return controls; } // Function to clean up a specific Three.js instance export function disposeThreeInstance(threeInstance) { if (!threeInstance) return; const { scene, renderer, camera } = threeInstance; // Remove event listeners window.removeEventListener('resize', () => {}); // This is a simplification; you'd need to manage specific listeners // Traverse and dispose geometries and materials scene.traverse((object) => { if (object.isMesh) { if (object.geometry) object.geometry.dispose(); if (object.material) { if (Array.isArray(object.material)) { object.material.forEach(material => material.dispose()); } else { object.material.dispose(); } } } }); // Remove renderer from DOM if (renderer && renderer.domElement && renderer.domElement.parentNode) { renderer.domElement.parentNode.removeChild(renderer.domElement); } // Dispose renderer if (renderer) renderer.dispose(); // Remove from active instances const index = activeThreeInstances.indexOf(threeInstance); if (index > -1) { activeThreeInstances.splice(index, 1); } // Clear global references if this was the last active instance if (activeThreeInstances.length === 0) { currentScene = null; currentCamera = null; currentRenderer = null; } } Finalizing netlify/functions/sendContactEmail.js (Example) javascript // netlify/functions/sendContactEmail.js (Example for Netlify Functions) const sgMail = require('@sendgrid/mail'); // For SendGrid, or use Nodemailer for other SMTP const formData = require('form-data'); // To parse FormData const fs = require('fs'); // For file reading (if needed) const path = require('path'); // For path manipulation // Configure SendGrid with your API key from environment variables sgMail.setApiKey(process.env.SENDGRID_API_KEY); exports.handler = async (event, context) => { // Only allow POST requests if (event.httpMethod !== 'POST') { return { statusCode: 405, body: JSON.stringify({ message: 'Method Not Allowed' }), }; } try { // Parse the form data. This might require specific libraries depending on how you send it. // If sending as JSON: const body = JSON.parse(event.body); const { name, mobile, email, address, message } = body; // Basic validation if (!name || !email || !message) { return { statusCode: 400, body: JSON.stringify({ message: 'Missing required fields (name, email, message).' }), }; } // Construct the email message const emailSubject = `New Contact Inquiry from ${name} - Ansar Trader IMEX`; const emailHtml = `

New Contact Inquiry

Name: ${name}

Mobile/WhatsApp: ${mobile || 'N/A'}

Email: ${email}

Address: ${address || 'N/A'}

Message:
${message.replace(/\n/g, '
')}


Sent via Ansar Trader IMEX website.

`; // Define sender and recipient const msg = { to: 'contact@ansar.pk', // Your recipient email from: 'noreply@ansar.pk', // Your verified sender email in SendGrid subject: emailSubject, html: emailHtml, }; // Send the email await sgMail.send(msg); return { statusCode: 200, body: JSON.stringify({ success: true, message: 'Email sent successfully!' }), }; } catch (error) { console.error('Error sending contact email:', error); // Provide a more generic error message to the client return { statusCode: 500, body: JSON.stringify({ success: false, message: 'Failed to send email. Please try again later.' }), }; } }; Example: netlify/functions/sendRfqEmail.js (Example for Netlify Functions) javascript // netlify/functions/sendRfqEmail.js const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(process.env.SENDGRID_API_KEY); exports.handler = async (event, context) => { if (event.httpMethod !== 'POST') { return { statusCode: 405, body: JSON.stringify({ message: 'Method Not Allowed' }), }; } try { const body = JSON.parse(event.body); const { name, company, email, mobile, productCategory, productDetails, quantity, destination, message } = body; // Basic validation if (!name || !company || !email || !productCategory || !quantity) { return { statusCode: 400, body: JSON.stringify({ message: 'Missing required RFQ fields.' }), }; } // Construct the email message const emailSubject = `New RFQ for ${productDetails || productCategory} - ${company} - Ansar Trader IMEX`; const emailHtml = `

New Request for Quotation (RFQ)

Requester Information:

Name: ${name}

Company: ${company}

Email: ${email}

Mobile/WhatsApp: ${mobile || 'N/A'}


Product & Order Details:

Product Category: ${productCategory}

Specific Product Details: ${productDetails || 'N/A'}

Required Quantity: ${quantity}

Destination Port/Country: ${destination || 'N/A'}

Additional Information/Requirements:
${message.replace(/\n/g, '
') || 'None'}


Sent via Ansar Trader IMEX website.

`; const msg = { to: 'contact@ansar.pk', // Your recipient email from: 'noreply@ansar.pk', // Your verified sender email in SendGrid subject: emailSubject, html: emailHtml, }; await sgMail.send(msg); return { statusCode: 200, body: JSON.stringify({ success: true, message: 'RFQ submitted successfully!' }), }; } catch (error) { console.error('Error sending RFQ email:', error); return { statusCode: 500, body: JSON.stringify({ success: false, message: 'Failed to submit RFQ. Please try again later.' }), }; } }; js/utils/pdfGenerator.js (Conceptual - Client-Side PDF Generation) For sending a PDF via email inquiry, client-side generation is possible but can be complex. A backend approach is more robust for complex PDFs. This is a simplified example of how jsPDF might be used. javascript // js/utils/pdfGenerator.js // Requires the jsPDF library to be included in your HTML: // export async function generateInquiryPDF(cartItems) { if (!cartItems || cartItems.length === 0) { console.warn("Cannot generate PDF for an empty cart."); return null; } // Ensure jsPDF is loaded. This is a basic check. if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') { console.error("jsPDF library not loaded. Cannot generate PDF."); // You might want to dynamically load it or ensure it's in your index.html // Example: const script = document.createElement('script'); script.src = '...'; document.body.appendChild(script); return null; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); // Header doc.setFontSize(18); doc.text("Ansar Trader IMEX - Product Inquiry", 10, 15); doc.setFontSize(12); doc.text(`Date: ${new Date().toLocaleDateString()}`, 10, 25); doc.text(`Inquiry Ref: ${Date.now()}`, 10, 35); // Simple unique ref // Company Info doc.setFontSize(14); doc.text("Company Details:", 10, 50); doc.setFontSize(11); doc.text("Ansar Trader IMEX (SMC-PRIVATE) LIMITED", 10, 58); doc.text("Reg: 0246004 | NTN: C344442", 10, 65); doc.text("158-B, Phase 2, Zargoonabad, Quetta", 10, 72); doc.text("Email: contact@ansar.pk", 10, 79); doc.text("WhatsApp: +923322233383", 10, 86); // Inquiry Items doc.setFontSize(14); doc.text("Inquired Products:", 10, 100); doc.setFontSize(11); let yPos = 110; cartItems.forEach((item, index) => { doc.text(`${index + 1}. ${item.name}`, 10, yPos); yPos += 10; // Add more details like quantity if available from cart if (yPos > 270) { // Page break doc.addPage(); yPos = 15; // Reset y position for new page } }); // Footer (optional) doc.setFontSize(10); doc.setTextColor(150); // Grey doc.text("This is an automated inquiry summary.", 10, doc.internal.pageSize.height - 10); // Return the PDF as a Blob or Data URL // doc.output('dataurlnewwindow'); // For direct viewing return doc.output('blob'); // Return as a Blob for sending via email }