diff --git a/projects/AI Portfolio Builder/README.md b/projects/AI Portfolio Builder/README.md new file mode 100644 index 0000000..85ea80d --- /dev/null +++ b/projects/AI Portfolio Builder/README.md @@ -0,0 +1,72 @@ +# AI Portfolio Builder + +An interactive, production-quality static web application that enables developers to build, preview, customize, and export professional responsive portfolios entirely in the browser. + +## Features + +- **Interactive Portfolio Builder**: Dynamic forms to edit personal details, social profiles, skills, education history, work experience, projects, certifications, and achievements. +- **Three Sleek Templates**: + - **Modern**: Clean grid layouts, interactive elements, soft gradients, and hover transitions. + - **Minimal**: High contrast, elegant typography, rich layouts focusing on content clarity. + - **Creative**: Distinct layouts, timelines, expressive design accents, and animations. +- **Real-Time Live Preview**: Instantly synchronizes form changes to an isolated preview pane with device size toggles (Desktop, Tablet, Mobile). +- **Theme Customizer**: Control colors, accent gradients, font family choices, container borders, and selectively toggle the visibility of individual sections. +- **Exporting Options**: + - Export single-page `index.html` structure. + - Export customized `style.css` matching chosen variables. + - Export unified `portfolio.zip` file containing assets, HTML, and CSS (generated client-side). + - Print-to-PDF layout focusing strictly on the resume output. +- **LocalStorage Sync**: Saves configurations and values on every keystroke, restoring the environment immediately on load. + +## Folder Structure + +```text +projects/ +└── AI Portfolio Builder/ + ├── index.html # Dashboard layout and controls + ├── style.css # Editor interface styles (glassmorphism) + ├── script.js # Core state management, template compiler, and exporter + ├── README.md # Documentation + ├── project.json # BuildVerse project metadata + └── assets/ + ├── icons/ # Directory for custom icons + ├── images/ # Images and default assets + └── screenshots/ # Application preview screenshots +``` + +## Technologies Used + +- **HTML5**: Semantic tags, accessibility indicators. +- **CSS3**: Variables, Flexbox, Grid, Glassmorphic effects, responsive styling. +- **Vanilla JavaScript (ES6)**: State management, live iframe DOM generation, client-side ZIP builder. +- **Lucide Icons**: Vector iconography. +- **JSZip (via CDN)**: Archive bundling entirely client-side. + +## Installation + +No installations or local builds are required. To launch the application locally, open the `index.html` file in any modern web browser (e.g., Chrome, Edge, Firefox). + +```bash +# Clone the repository +git clone https://github.com/KolaSailaja/BuildVerse.git + +# Navigate to the project directory +cd "projects/AI Portfolio Builder" + +# Open index.html directly in your browser or run via live-server +``` + +## Usage + +1. **Fill Your Information**: Complete the personal bio, skills list, professional experience, academic background, and project fields. Use the **Add/Remove** buttons to add as many projects or experience entries as desired. +2. **Select a Template**: Use the template selector to switch layouts (Modern, Minimal, Creative) instantly. +3. **Customize Aesthetics**: Tweak primary/accent colors, adjust border sharpness, select from Google Fonts, and toggle section visibility under the Theme panel. +4. **Preview Responsiveness**: Switch the preview window dimensions to test Mobile, Tablet, and Desktop layouts. +5. **Export & Share**: + - Download the individual files (`index.html`, `style.css`). + - Click "Export ZIP" to download a fully packaged static site. + - Click "Print PDF" to save a clean PDF resume. + +## License + +This project is licensed under the MIT License - see the LICENSE file at the root repository for details. diff --git a/projects/AI Portfolio Builder/assets/icons/.gitkeep b/projects/AI Portfolio Builder/assets/icons/.gitkeep new file mode 100644 index 0000000..09c6916 --- /dev/null +++ b/projects/AI Portfolio Builder/assets/icons/.gitkeep @@ -0,0 +1 @@ +# Keep directory diff --git a/projects/AI Portfolio Builder/assets/images/.gitkeep b/projects/AI Portfolio Builder/assets/images/.gitkeep new file mode 100644 index 0000000..09c6916 --- /dev/null +++ b/projects/AI Portfolio Builder/assets/images/.gitkeep @@ -0,0 +1 @@ +# Keep directory diff --git a/projects/AI Portfolio Builder/assets/screenshots/.gitkeep b/projects/AI Portfolio Builder/assets/screenshots/.gitkeep new file mode 100644 index 0000000..09c6916 --- /dev/null +++ b/projects/AI Portfolio Builder/assets/screenshots/.gitkeep @@ -0,0 +1 @@ +# Keep directory diff --git a/projects/AI Portfolio Builder/index.html b/projects/AI Portfolio Builder/index.html new file mode 100644 index 0000000..2f01e75 --- /dev/null +++ b/projects/AI Portfolio Builder/index.html @@ -0,0 +1,421 @@ + + + + + + AI Portfolio Builder - Create, Customize & Export + + + + + + + + +
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + + + + +
+
+
+ + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ + +
+ +
+
+ + +
+ +
+ + + + +
+
+ +
+ +
+ + #4f46e5 +
+
+ +
+ +
+ + #06b6d4 +
+
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +
+
+ + +
+ +
+ + + + + + +
+
+ +
+
+
+ + +
+

Export Portfolio

+

Generate production-ready files instantly. No backend or subscriptions.

+
+ + + + +
+
+ +
+ + +
+
+
+ Live Preview +
+ +
+ + + +
+
+ +
+ +
+ +
+
+
+
+ + + +
+ + + + + + + + + diff --git a/projects/AI Portfolio Builder/project.json b/projects/AI Portfolio Builder/project.json new file mode 100644 index 0000000..acc6a39 --- /dev/null +++ b/projects/AI Portfolio Builder/project.json @@ -0,0 +1,24 @@ +{ + "title": "AI Portfolio Builder", + "description": "A modern static web application that enables users to create a professional developer portfolio through an interactive form, customize templates, preview in real time, and export the generated portfolio as a static website.", + "author": { + "name": "Kola Sailaja", + "github": "KolaSailaja" + }, + "githubUsername": "KolaSailaja", + "tags": [ + "Portfolio", + "Website Builder", + "Interactive", + "Utility", + "Glassmorphism" + ], + "technologies": [ + "HTML5", + "CSS3", + "JavaScript" + ], + "category": "Developer Tools / Portfolio", + "responsive": true, + "version": "1.0.0" +} diff --git a/projects/AI Portfolio Builder/script.js b/projects/AI Portfolio Builder/script.js new file mode 100644 index 0000000..a80978b --- /dev/null +++ b/projects/AI Portfolio Builder/script.js @@ -0,0 +1,2262 @@ +// ========================================== +// 1. DEFAULT DATA DEFINITIONS +// ========================================== +const DEFAULT_PORTFOLIO_DATA = { + personalInfo: { + name: "Alex Morgan", + title: "Senior Full Stack Engineer", + photo: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=300&q=80", + bio: "I am a passionate software engineer with 6+ years of experience building high-performance web applications and beautiful responsive interfaces. I specialize in React, Node.js, modern CSS layouts, and cloud architectures. Focused on clean code, performance optimization, and intuitive user experiences.", + email: "alex.morgan@dev.io", + phone: "+1 (555) 342-9871", + location: "Seattle, WA", + website: "https://alexmorgan.dev" + }, + socialLinks: { + github: "https://github.com/alexmorgan", + linkedin: "https://linkedin.com/in/alexmorgan", + twitter: "https://twitter.com/alexmorgan_dev", + youtube: "https://youtube.com/@alexmorgancodes" + }, + skills: [ + { name: "JavaScript (ES6+)", level: "Expert" }, + { name: "TypeScript", level: "Expert" }, + { name: "HTML5 & CSS3", level: "Expert" }, + { name: "React & Next.js", level: "Advanced" }, + { name: "Node.js & Express", level: "Advanced" }, + { name: "GraphQL & REST APIs", level: "Advanced" }, + { name: "PostgreSQL & MongoDB", level: "Intermediate" }, + { name: "Docker & AWS", level: "Intermediate" } + ], + experience: [ + { + company: "TechNova Solutions", + position: "Lead Software Engineer", + duration: "2023 - Present", + description: "Leading a team of 5 engineers to rebuild the core enterprise dashboard. Optimized web performance, cutting page load times by 40%. Implemented shared design tokens and modular UI components." + }, + { + company: "PixelPerfect Web Studio", + position: "Senior Frontend Developer", + duration: "2020 - 2023", + description: "Crafted stunning responsive interfaces and custom interaction systems for international clients. Managed modular state lifecycles and established CSS layout architecture guidelines." + } + ], + education: [ + { + institution: "University of Washington", + degree: "B.S. in Computer Science", + duration: "2016 - 2020", + description: "Focused on human-computer interaction, web architectures, and algorithms. Graduated with Honors." + } + ], + projects: [ + { + title: "DevFlow Project Planner", + description: "A collaborative project management application featuring interactive Kanban boards, live progress timelines, and visual team workload analysis dashboards.", + technologies: "TypeScript, React, Node.js, Socket.io", + github: "https://github.com/alexmorgan/devflow", + demo: "https://devflow-planner.demo" + }, + { + title: "Quantum CSS Library", + description: "A lightweight, zero-dependency utility CSS module optimized for fast rendering, micro-interactions, and glassmorphic designs.", + technologies: "JavaScript, CSS3, HTML5", + github: "https://github.com/alexmorgan/quantum-css", + demo: "https://quantum-css.org" + } + ], + certifications: [ + { + title: "AWS Certified Solutions Architect", + issuer: "Amazon Web Services", + date: "2024-03" + }, + { + title: "Google Advanced UX Design Certificate", + issuer: "Google", + date: "2022-08" + } + ], + achievements: [ + { + title: "1st Place - TechNova Hackathon 2024", + description: "Designed and built an AI-powered code translation tool in under 48 hours, winning first place out of 60 teams." + }, + { + title: "Open Source Contributor", + description: "Contributed critical performance optimization patches to multiple popular frontend utilities and package projects." + } + ], + theme: { + template: "modern", + primaryColor: "#4f46e5", + accentColor: "#06b6d4", + fontFamily: "Outfit", + borderRadius: "12px", + darkMode: true, + visibility: { + skills: true, + experience: true, + education: true, + projects: true, + certifications: true, + achievements: true + } + } +}; + +// Application State +let appState = JSON.parse(JSON.stringify(DEFAULT_PORTFOLIO_DATA)); + +// ========================================== +// 2. INITIALIZATION & STATE SYNC +// ========================================== +document.addEventListener("DOMContentLoaded", () => { + initDashboardTheme(); + loadStateFromLocalStorage(); + bindStaticEventListeners(); + populateFormInputs(); + renderAllDynamicLists(); + triggerLivePreviewUpdate(); + + // Initialize Lucide Icons + if (window.lucide) { + window.lucide.createIcons(); + } +}); + +// Load state from localstorage +function loadStateFromLocalStorage() { + const savedData = localStorage.getItem("bv_ai_portfolio_data"); + if (savedData) { + try { + appState = JSON.parse(savedData); + + // Ensure theme object structure matches in case of updates + if (!appState.theme) appState.theme = JSON.parse(JSON.stringify(DEFAULT_PORTFOLIO_DATA.theme)); + if (!appState.theme.visibility) appState.theme.visibility = JSON.parse(JSON.stringify(DEFAULT_PORTFOLIO_DATA.theme.visibility)); + if (!appState.certifications) appState.certifications = []; + if (!appState.achievements) appState.achievements = []; + } catch (e) { + console.error("Error parsing LocalStorage portfolio data", e); + appState = JSON.parse(JSON.stringify(DEFAULT_PORTFOLIO_DATA)); + } + } +} + +// Save state to localstorage +function saveStateToLocalStorage() { + localStorage.setItem("bv_ai_portfolio_data", JSON.stringify(appState)); +} + +// Reset data +function resetPortfolioData() { + if (confirm("Are you sure you want to reset all portfolio fields to default? This cannot be undone.")) { + appState = JSON.parse(JSON.stringify(DEFAULT_PORTFOLIO_DATA)); + saveStateToLocalStorage(); + populateFormInputs(); + renderAllDynamicLists(); + triggerLivePreviewUpdate(); + alert("Portfolio builder reset successfully!"); + } +} + +// Initialize Dashboard (Light/Dark mode) +function initDashboardTheme() { + const savedDashTheme = localStorage.getItem("bv_dashboard_theme") || "dark"; + if (savedDashTheme === "light") { + document.body.classList.remove("dark-theme"); + document.body.classList.add("light-theme"); + } else { + document.body.classList.add("dark-theme"); + document.body.classList.remove("light-theme"); + } +} + +// Toggle Dashboard Theme +function toggleDashboardTheme() { + if (document.body.classList.contains("dark-theme")) { + document.body.classList.remove("dark-theme"); + document.body.classList.add("light-theme"); + localStorage.setItem("bv_dashboard_theme", "light"); + } else { + document.body.classList.add("dark-theme"); + document.body.classList.remove("light-theme"); + localStorage.setItem("bv_dashboard_theme", "dark"); + } +} + +// ========================================== +// 3. FORM BINDING & ACCORDIONS +// ========================================== +function bindStaticEventListeners() { + // Accordion Toggles + const accordions = document.querySelectorAll(".accordion-header"); + accordions.forEach(header => { + header.addEventListener("click", () => { + const item = header.parentElement; + const isActive = item.classList.contains("active"); + + // Close all accordions + document.querySelectorAll(".accordion-item").forEach(acc => { + acc.classList.remove("active"); + acc.querySelector(".accordion-header").setAttribute("aria-expanded", "false"); + }); + + if (!isActive) { + item.classList.add("active"); + header.setAttribute("aria-expanded", "true"); + } + }); + }); + + // Theme Preset Buttons + document.querySelectorAll(".preset-btn").forEach(btn => { + btn.addEventListener("click", () => { + const primary = btn.dataset.primary; + const accent = btn.dataset.accent; + const font = btn.dataset.font; + const radius = btn.dataset.radius; + + document.getElementById("theme-primary").value = primary; + document.getElementById("theme-accent").value = accent; + document.getElementById("theme-font").value = font; + document.getElementById("theme-radius").value = parseInt(radius); + + document.getElementById("primary-hex").textContent = primary; + document.getElementById("accent-hex").textContent = accent; + document.getElementById("radius-val").textContent = radius; + + appState.theme.primaryColor = primary; + appState.theme.accentColor = accent; + appState.theme.fontFamily = font; + appState.theme.borderRadius = radius; + + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + }); + + // Color Pickers + const primaryPicker = document.getElementById("theme-primary"); + primaryPicker.addEventListener("input", (e) => { + const val = e.target.value; + document.getElementById("primary-hex").textContent = val; + appState.theme.primaryColor = val; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + + const accentPicker = document.getElementById("theme-accent"); + accentPicker.addEventListener("input", (e) => { + const val = e.target.value; + document.getElementById("accent-hex").textContent = val; + appState.theme.accentColor = val; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + + // Font Family Selector + const fontSelector = document.getElementById("theme-font"); + fontSelector.addEventListener("change", (e) => { + appState.theme.fontFamily = e.target.value; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + + // Border Radius Slider + const radiusSlider = document.getElementById("theme-radius"); + radiusSlider.addEventListener("input", (e) => { + const val = e.target.value + "px"; + document.getElementById("radius-val").textContent = val; + appState.theme.borderRadius = val; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + + // Output Dark Mode Toggle + const portfolioDarkCheckbox = document.getElementById("portfolio-dark-mode"); + portfolioDarkCheckbox.addEventListener("change", (e) => { + appState.theme.darkMode = e.target.checked; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + + // Section Visibilities + const visCheckboxes = ["skills", "experience", "education", "projects", "certifications", "achievements"]; + visCheckboxes.forEach(sec => { + const el = document.getElementById(`vis-${sec}`); + if (el) { + el.addEventListener("change", (e) => { + appState.theme.visibility[sec] = e.target.checked; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + } + }); + + // Template Radio Buttons + const templateRadios = document.querySelectorAll('input[name="portfolio-template"]'); + templateRadios.forEach(radio => { + radio.addEventListener("change", (e) => { + appState.theme.template = e.target.value; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + }); + + // Personal Info Form Binding + const personalFields = { + "p-name": ["personalInfo", "name"], + "p-title": ["personalInfo", "title"], + "p-photo": ["personalInfo", "photo"], + "p-bio": ["personalInfo", "bio"], + "p-email": ["personalInfo", "email"], + "p-phone": ["personalInfo", "phone"], + "p-location": ["personalInfo", "location"], + "p-website": ["personalInfo", "website"] + }; + bindFormInputs(personalFields); + + // Social Links Form Binding + const socialFields = { + "s-github": ["socialLinks", "github"], + "s-linkedin": ["socialLinks", "linkedin"], + "s-twitter": ["socialLinks", "twitter"], + "s-youtube": ["socialLinks", "youtube"] + }; + bindFormInputs(socialFields); + + // Dynamic Add Item Buttons + document.getElementById("add-skill-btn").addEventListener("click", () => addDynamicItem("skills", { name: "", level: "Intermediate" })); + document.getElementById("add-experience-btn").addEventListener("click", () => addDynamicItem("experience", { company: "", position: "", duration: "", description: "" })); + document.getElementById("add-education-btn").addEventListener("click", () => addDynamicItem("education", { institution: "", degree: "", duration: "", description: "" })); + document.getElementById("add-project-btn").addEventListener("click", () => addDynamicItem("projects", { title: "", description: "", technologies: "", github: "", demo: "" })); + document.getElementById("add-certification-btn").addEventListener("click", () => addDynamicItem("certifications", { title: "", issuer: "", date: "" })); + document.getElementById("add-achievement-btn").addEventListener("click", () => addDynamicItem("achievements", { title: "", description: "" })); + + // Dashboard Theme Toggle & Reset Actions + document.getElementById("theme-toggle-btn").addEventListener("click", toggleDashboardTheme); + document.getElementById("reset-btn").addEventListener("click", resetPortfolioData); + + // Exporters + document.getElementById("export-html-btn").addEventListener("click", exportHTMLOnly); + document.getElementById("export-css-btn").addEventListener("click", exportCSSOnly); + document.getElementById("export-zip-btn").addEventListener("click", exportZipBundle); + document.getElementById("print-pdf-btn").addEventListener("click", printPortfolioPDF); + + // Device sizer + const deviceButtons = document.querySelectorAll(".device-btn"); + const wrapper = document.getElementById("iframe-wrapper"); + deviceButtons.forEach(btn => { + btn.addEventListener("click", () => { + deviceButtons.forEach(b => b.classList.remove("active")); + btn.classList.add("active"); + + const device = btn.dataset.device; + wrapper.className = `iframe-wrapper device-${device}`; + }); + }); +} + +// Binds basic keyup/change input events to state +function bindFormInputs(mapping) { + Object.keys(mapping).forEach(id => { + const el = document.getElementById(id); + if (el) { + el.addEventListener("input", (e) => { + const path = mapping[id]; + appState[path[0]][path[1]] = e.target.value; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); + }); + } + }); +} + +// Populate basic form input values from state +function populateFormInputs() { + // Template Select + const templateRadio = document.querySelector(`input[name="portfolio-template"][value="${appState.theme.template}"]`); + if (templateRadio) templateRadio.checked = true; + + // Personal Info + document.getElementById("p-name").value = appState.personalInfo.name || ""; + document.getElementById("p-title").value = appState.personalInfo.title || ""; + document.getElementById("p-photo").value = appState.personalInfo.photo || ""; + document.getElementById("p-bio").value = appState.personalInfo.bio || ""; + document.getElementById("p-email").value = appState.personalInfo.email || ""; + document.getElementById("p-phone").value = appState.personalInfo.phone || ""; + document.getElementById("p-location").value = appState.personalInfo.location || ""; + document.getElementById("p-website").value = appState.personalInfo.website || ""; + + // Socials + document.getElementById("s-github").value = appState.socialLinks.github || ""; + document.getElementById("s-linkedin").value = appState.socialLinks.linkedin || ""; + document.getElementById("s-twitter").value = appState.socialLinks.twitter || ""; + document.getElementById("s-youtube").value = appState.socialLinks.youtube || ""; + + // Theme customizer values + document.getElementById("theme-primary").value = appState.theme.primaryColor || "#4f46e5"; + document.getElementById("theme-accent").value = appState.theme.accentColor || "#06b6d4"; + document.getElementById("theme-font").value = appState.theme.fontFamily || "Outfit"; + document.getElementById("theme-radius").value = parseInt(appState.theme.borderRadius) || 12; + document.getElementById("portfolio-dark-mode").checked = appState.theme.darkMode !== false; + + document.getElementById("primary-hex").textContent = appState.theme.primaryColor || "#4f46e5"; + document.getElementById("accent-hex").textContent = appState.theme.accentColor || "#06b6d4"; + document.getElementById("radius-val").textContent = appState.theme.borderRadius || "12px"; + + // Section Visibilities + const visCheckboxes = ["skills", "experience", "education", "projects", "certifications", "achievements"]; + visCheckboxes.forEach(sec => { + const el = document.getElementById(`vis-${sec}`); + if (el) { + el.checked = appState.theme.visibility[sec] !== false; + } + }); +} + +// ========================================== +// 4. DYNAMIC LIST MANAGEMENT +// ========================================== +function renderAllDynamicLists() { + renderDynamicList("skills", renderSkillItemDOM); + renderDynamicList("experience", renderExperienceItemDOM); + renderDynamicList("education", renderEducationItemDOM); + renderDynamicList("projects", renderProjectItemDOM); + renderDynamicList("certifications", renderCertificationItemDOM); + renderDynamicList("achievements", renderAchievementItemDOM); +} + +function renderDynamicList(key, domGenerator) { + const container = document.getElementById(`${key}-list`); + if (!container) return; + + container.innerHTML = ""; + const list = appState[key] || []; + + list.forEach((item, index) => { + const dom = domGenerator(index, item); + container.appendChild(dom); + }); +} + +function addDynamicItem(key, defaultObj) { + if (!appState[key]) appState[key] = []; + appState[key].push(defaultObj); + saveStateToLocalStorage(); + renderAllDynamicLists(); + triggerLivePreviewUpdate(); +} + +function removeDynamicItem(key, index) { + if (!appState[key]) return; + appState[key].splice(index, 1); + saveStateToLocalStorage(); + renderAllDynamicLists(); + triggerLivePreviewUpdate(); +} + +function updateDynamicField(key, index, field, value) { + if (!appState[key] || !appState[key][index]) return; + appState[key][index][field] = value; + saveStateToLocalStorage(); + triggerLivePreviewUpdate(); +} + +// Generator - Skill DOM +function renderSkillItemDOM(index, skill) { + const div = document.createElement("div"); + div.className = "dynamic-item-card"; + div.innerHTML = ` +
+ Skill #${index + 1} + +
+
+
+ + +
+
+ + +
+
+ `; + + // Attach listeners + div.querySelector(".btn-remove-item").addEventListener("click", () => removeDynamicItem("skills", index)); + div.querySelector(".skill-name-input").addEventListener("input", (e) => updateDynamicField("skills", index, "name", e.target.value)); + div.querySelector(".skill-level-input").addEventListener("change", (e) => updateDynamicField("skills", index, "level", e.target.value)); + + if (window.lucide) window.lucide.createIcons({ src: div }); + return div; +} + +// Generator - Experience DOM +function renderExperienceItemDOM(index, exp) { + const div = document.createElement("div"); + div.className = "dynamic-item-card"; + div.innerHTML = ` +
+ Experience #${index + 1} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + // Attach listeners + div.querySelector(".btn-remove-item").addEventListener("click", () => removeDynamicItem("experience", index)); + div.querySelector(".exp-company-input").addEventListener("input", (e) => updateDynamicField("experience", index, "company", e.target.value)); + div.querySelector(".exp-position-input").addEventListener("input", (e) => updateDynamicField("experience", index, "position", e.target.value)); + div.querySelector(".exp-duration-input").addEventListener("input", (e) => updateDynamicField("experience", index, "duration", e.target.value)); + div.querySelector(".exp-desc-input").addEventListener("input", (e) => updateDynamicField("experience", index, "description", e.target.value)); + + if (window.lucide) window.lucide.createIcons({ src: div }); + return div; +} + +// Generator - Education DOM +function renderEducationItemDOM(index, edu) { + const div = document.createElement("div"); + div.className = "dynamic-item-card"; + div.innerHTML = ` +
+ Education #${index + 1} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + // Attach listeners + div.querySelector(".btn-remove-item").addEventListener("click", () => removeDynamicItem("education", index)); + div.querySelector(".edu-inst-input").addEventListener("input", (e) => updateDynamicField("education", index, "institution", e.target.value)); + div.querySelector(".edu-degree-input").addEventListener("input", (e) => updateDynamicField("education", index, "degree", e.target.value)); + div.querySelector(".edu-duration-input").addEventListener("input", (e) => updateDynamicField("education", index, "duration", e.target.value)); + div.querySelector(".edu-desc-input").addEventListener("input", (e) => updateDynamicField("education", index, "description", e.target.value)); + + if (window.lucide) window.lucide.createIcons({ src: div }); + return div; +} + +// Generator - Project DOM +function renderProjectItemDOM(index, proj) { + const div = document.createElement("div"); + div.className = "dynamic-item-card"; + div.innerHTML = ` +
+ Project #${index + 1} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + // Attach listeners + div.querySelector(".btn-remove-item").addEventListener("click", () => removeDynamicItem("projects", index)); + div.querySelector(".proj-title-input").addEventListener("input", (e) => updateDynamicField("projects", index, "title", e.target.value)); + div.querySelector(".proj-desc-input").addEventListener("input", (e) => updateDynamicField("projects", index, "description", e.target.value)); + div.querySelector(".proj-tech-input").addEventListener("input", (e) => updateDynamicField("projects", index, "technologies", e.target.value)); + div.querySelector(".proj-git-input").addEventListener("input", (e) => updateDynamicField("projects", index, "github", e.target.value)); + div.querySelector(".proj-demo-input").addEventListener("input", (e) => updateDynamicField("projects", index, "demo", e.target.value)); + + if (window.lucide) window.lucide.createIcons({ src: div }); + return div; +} + +// Generator - Certification DOM +function renderCertificationItemDOM(index, cert) { + const div = document.createElement("div"); + div.className = "dynamic-item-card"; + div.innerHTML = ` +
+ Certification #${index + 1} + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + // Attach listeners + div.querySelector(".btn-remove-item").addEventListener("click", () => removeDynamicItem("certifications", index)); + div.querySelector(".cert-title-input").addEventListener("input", (e) => updateDynamicField("certifications", index, "title", e.target.value)); + div.querySelector(".cert-issuer-input").addEventListener("input", (e) => updateDynamicField("certifications", index, "issuer", e.target.value)); + div.querySelector(".cert-date-input").addEventListener("input", (e) => updateDynamicField("certifications", index, "date", e.target.value)); + + if (window.lucide) window.lucide.createIcons({ src: div }); + return div; +} + +// Generator - Achievement DOM +function renderAchievementItemDOM(index, ach) { + const div = document.createElement("div"); + div.className = "dynamic-item-card"; + div.innerHTML = ` +
+ Achievement #${index + 1} + +
+
+
+ + +
+
+ + +
+
+ `; + + // Attach listeners + div.querySelector(".btn-remove-item").addEventListener("click", () => removeDynamicItem("achievements", index)); + div.querySelector(".ach-title-input").addEventListener("input", (e) => updateDynamicField("achievements", index, "title", e.target.value)); + div.querySelector(".ach-desc-input").addEventListener("input", (e) => updateDynamicField("achievements", index, "description", e.target.value)); + + if (window.lucide) window.lucide.createIcons({ src: div }); + return div; +} + +// ========================================== +// 5. LIVE PREVIEW UPDATE WITH TEMPLATES +// ========================================== +let renderDebounceTimer; +function triggerLivePreviewUpdate() { + clearTimeout(renderDebounceTimer); + renderDebounceTimer = setTimeout(compileAndLoadPreview, 150); +} + +// Renders the chosen template with current custom colors/radius/visibility to the preview iframe +function compileAndLoadPreview() { + const previewIframe = document.getElementById("portfolio-preview"); + if (!previewIframe) return; + + const generatedHTML = compilePortfolioFullHTML(true); // true = preview mode (keeps absolute urls, etc.) + + const doc = previewIframe.contentDocument || previewIframe.contentWindow.document; + doc.open(); + doc.write(generatedHTML); + doc.close(); +} + +// Main HTML compiler +function compilePortfolioFullHTML(isPreviewMode = false) { + const t = appState.theme; + const p = appState.personalInfo; + + // Custom Styles + const templateCSS = compileTemplateCSS(t.template); + + // Generate section markups conditionally based on visibility + const skillsHTML = t.visibility.skills ? compileSkillsSection() : ""; + const experienceHTML = t.visibility.experience ? compileExperienceSection() : ""; + const educationHTML = t.visibility.education ? compileEducationSection() : ""; + const projectsHTML = t.visibility.projects ? compileProjectsSection() : ""; + const certificationsHTML = t.visibility.certifications ? compileCertificationsSection() : ""; + const achievementsHTML = t.visibility.achievements ? compileAchievementsSection() : ""; + + // Dynamic social links rendering + const socialItemsHTML = compileSocialsList(); + + // Fallback for photo + const photoURL = p.photo || "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=150&q=80"; + + return ` + + + + + ${escapeHTML(p.name)} | ${escapeHTML(p.title)} Portfolio + + + + + + + + + + +
+ + +
+
+
+
+ ${escapeHTML(p.name)} +
+
+

${escapeHTML(p.name)}

+

${escapeHTML(p.title)}

+ ${p.location ? `

${escapeHTML(p.location)}

` : ""} +
+
+ +

${escapeHTML(p.bio)}

+ +
+ Get In Touch + ${p.website ? ` Website` : ""} +
+ + ${socialItemsHTML ? `` : ""} +
+
+ + +
+ + + ${skillsHTML} + + + ${experienceHTML} + + + ${educationHTML} + + + ${projectsHTML} + + + ${certificationsHTML} + + + ${achievementsHTML} + + +
+

Contact Details

+
+

Feel free to reach out for project opportunities, open roles, or just to say hello!

+
+
+ + +
+ ${p.phone ? ` +
+ + +
+ ` : ""} + ${p.location ? ` +
+ +
+ Location + ${escapeHTML(p.location)} +
+
+ ` : ""} + ${p.website ? ` +
+ +
+ Website + ${escapeHTML(p.website)} +
+
+ ` : ""} +
+
+
+ +
+ + + + +
+ + + + + +`; +} + +// Compile Section: Skills +function compileSkillsSection() { + if (!appState.skills || appState.skills.length === 0) return ""; + + const skillCards = appState.skills.map(s => { + if (!s.name) return ""; + let lvlClass = "lvl-intermediate"; + if (s.level === "Beginner") lvlClass = "lvl-beginner"; + if (s.level === "Advanced") lvlClass = "lvl-advanced"; + if (s.level === "Expert") lvlClass = "lvl-expert"; + + return `
+ ${escapeHTML(s.name)} + ${escapeHTML(s.level)} +
`; + }).join("\n"); + + return `
+

Skills & Expertise

+
+ ${skillCards} +
+
`; +} + +// Compile Section: Experience +function compileExperienceSection() { + const validExp = (appState.experience || []).filter(e => e.company && e.position); + if (validExp.length === 0) return ""; + + const timelineHTML = validExp.map((exp, idx) => { + return `
+
+
+
+
+

${escapeHTML(exp.position)}

+

${escapeHTML(exp.company)}

+
+ ${escapeHTML(exp.duration)} +
+

${escapeHTML(exp.description)}

+
+
`; + }).join("\n"); + + return `
+

Work Experience

+
+ ${timelineHTML} +
+
`; +} + +// Compile Section: Education +function compileEducationSection() { + const validEdu = (appState.education || []).filter(e => e.institution && e.degree); + if (validEdu.length === 0) return ""; + + const itemsHTML = validEdu.map(edu => { + return `
+
+
+

${escapeHTML(edu.degree)}

+

${escapeHTML(edu.institution)}

+
+ ${escapeHTML(edu.duration)} +
+ ${edu.description ? `

${escapeHTML(edu.description)}

` : ""} +
`; + }).join("\n"); + + return `
+

Education

+
+ ${itemsHTML} +
+
`; +} + +// Compile Section: Projects +function compileProjectsSection() { + const validProj = (appState.projects || []).filter(p => p.title && p.description); + if (validProj.length === 0) return ""; + + const cardsHTML = validProj.map(proj => { + const techBadges = proj.technologies ? proj.technologies.split(",").map(t => `${escapeHTML(t.trim())}`).join("") : ""; + + return `
+
+

${escapeHTML(proj.title)}

+

${escapeHTML(proj.description)}

+ ${techBadges ? `
${techBadges}
` : ""} +
+ +
`; + }).join("\n"); + + return `
+

Featured Projects

+
+ ${cardsHTML} +
+
`; +} + +// Compile Section: Certifications +function compileCertificationsSection() { + const validCert = (appState.certifications || []).filter(c => c.title && c.issuer); + if (validCert.length === 0) return ""; + + const listHTML = validCert.map(cert => { + return `
+
+ +
+
+

${escapeHTML(cert.title)}

+

${escapeHTML(cert.issuer)} ${cert.date ? `• ${escapeHTML(cert.date)}` : ""}

+
+
`; + }).join("\n"); + + return `
+

Certifications

+
+ ${listHTML} +
+
`; +} + +// Compile Section: Achievements +function compileAchievementsSection() { + const validAch = (appState.achievements || []).filter(a => a.title && a.description); + if (validAch.length === 0) return ""; + + const cardsHTML = validAch.map(ach => { + return `
+
+ +
+
+

${escapeHTML(ach.title)}

+

${escapeHTML(ach.description)}

+
+
`; + }).join("\n"); + + return `
+

Achievements

+
+ ${cardsHTML} +
+
`; +} + +// Compile Social Links list +function compileSocialsList() { + const s = appState.socialLinks; + let html = ""; + if (s.github) html += ``; + if (s.linkedin) html += ``; + if (s.twitter) html += ``; + if (s.youtube) html += ``; + return html; +} + +// ========================================== +// 6. TEMPLATES INDIVIDUAL CSS STYLINGS +// ========================================== +function compileTemplateCSS(template) { + const commonCSS = ` + /* Common Reset & Styles inside preview */ + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + background-color: var(--bg-body); + color: var(--text-main); + font-family: var(--font-family); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + transition: background 0.3s, color 0.3s; + } + img { + max-width: 100%; + height: auto; + display: block; + } + a { + color: var(--primary); + text-decoration: none; + transition: color 0.2s; + } + a:hover { + color: var(--accent); + } + + .portfolio-container { + max-width: 1000px; + margin: 0 auto; + padding: 2rem 1.5rem; + } + + /* Section Defaults */ + .section { + margin-bottom: 4rem; + } + .section-title { + font-size: 1.65rem; + font-weight: 700; + margin-bottom: 1.75rem; + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; + } + .section-title svg { + width: 1.35rem; + height: 1.35rem; + color: var(--primary); + } + + /* Skills styling */ + .skills-grid { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + } + .skill-badge { + display: flex; + flex-direction: column; + padding: 0.5rem 1rem; + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + } + .skill-name { + font-size: 0.9rem; + font-weight: 600; + } + .skill-level { + font-size: 0.7rem; + color: var(--text-muted); + } + + /* Timeline styling */ + .timeline { + position: relative; + border-left: 2px solid var(--border-color); + margin-left: 0.5rem; + padding-left: 1.5rem; + display: flex; + flex-direction: column; + gap: 2rem; + } + .timeline-item { + position: relative; + } + .timeline-indicator { + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--primary); + left: calc(-1.5rem - 7px); + top: 6px; + box-shadow: 0 0 0 4px var(--bg-body); + } + .timeline-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 1.5rem; + box-shadow: var(--card-shadow); + } + .timeline-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 0.75rem; + } + .role-title { + font-size: 1.1rem; + font-weight: 700; + } + .company-name { + font-size: 0.95rem; + color: var(--primary); + font-weight: 500; + } + .timeline-duration { + font-size: 0.8rem; + padding: 0.2rem 0.6rem; + background: var(--bg-surface-accent); + border-radius: var(--radius); + color: var(--text-muted); + white-space: nowrap; + } + .timeline-desc { + font-size: 0.9rem; + color: var(--text-muted); + } + + /* Education styling */ + .edu-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + } + .edu-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 1.5rem; + box-shadow: var(--card-shadow); + } + .edu-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 0.5rem; + } + .edu-degree { + font-size: 1.1rem; + font-weight: 700; + } + .edu-school { + font-size: 0.95rem; + color: var(--primary); + font-weight: 500; + } + .edu-duration { + font-size: 0.8rem; + padding: 0.2rem 0.6rem; + background: var(--bg-surface-accent); + border-radius: var(--radius); + color: var(--text-muted); + } + .edu-desc { + font-size: 0.9rem; + color: var(--text-muted); + margin-top: 0.5rem; + } + + /* Projects styling */ + .projects-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + } + .project-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; + box-shadow: var(--card-shadow); + transition: transform 0.2s; + } + .project-card:hover { + transform: translateY(-4px); + } + .project-info { + padding: 1.5rem; + } + .project-name { + font-size: 1.15rem; + font-weight: 700; + margin-bottom: 0.5rem; + } + .project-desc { + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 1rem; + } + .project-badges { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + .proj-badge { + font-size: 0.75rem; + padding: 0.15rem 0.45rem; + background: var(--bg-surface-accent); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-muted); + } + .project-footer-links { + display: flex; + border-top: 1px solid var(--border-color); + background: var(--bg-surface-accent); + } + .proj-link { + flex: 1; + padding: 0.75rem 0.5rem; + text-align: center; + font-size: 0.8rem; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + border-right: 1px solid var(--border-color); + } + .proj-link:last-child { + border-right: none; + } + .proj-link svg { + width: 0.9rem; + height: 0.9rem; + } + + /* Certifications styling */ + .certifications-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1rem; + } + .cert-item-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + box-shadow: var(--card-shadow); + } + .cert-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border-radius: 50%; + background: rgba(var(--primary-rgb), 0.1); + color: var(--primary); + flex-shrink: 0; + } + .cert-icon-wrapper svg { + width: 1.1rem; + height: 1.1rem; + } + .cert-name { + font-size: 0.95rem; + font-weight: 600; + } + .cert-meta { + font-size: 0.75rem; + color: var(--text-muted); + } + + /* Achievements styling */ + .achievements-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + } + .achievement-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 1.25rem; + display: flex; + gap: 0.75rem; + box-shadow: var(--card-shadow); + } + .ach-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border-radius: 50%; + background: rgba(var(--accent-rgb), 0.1); + color: var(--accent); + flex-shrink: 0; + } + .ach-icon-wrapper svg { + width: 1.1rem; + height: 1.1rem; + } + .achievement-name { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.25rem; + } + .achievement-desc { + font-size: 0.85rem; + color: var(--text-muted); + } + + /* Contact and Footer */ + .contact-card { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 2rem; + box-shadow: var(--card-shadow); + } + .contact-pitch { + font-size: 1rem; + margin-bottom: 1.5rem; + color: var(--text-muted); + } + .contact-details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + } + .contact-item { + display: flex; + align-items: flex-start; + gap: 0.75rem; + } + .contact-icon { + width: 1.1rem; + height: 1.1rem; + color: var(--primary); + margin-top: 0.2rem; + } + .contact-label { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + } + .contact-value { + font-size: 0.95rem; + font-weight: 600; + color: var(--text-main); + } + .portfolio-footer { + text-align: center; + padding: 2rem 0; + border-top: 1px solid var(--border-color); + margin-top: 4rem; + font-size: 0.8rem; + color: var(--text-muted); + } + + /* Print styles */ + @media print { + body { + background: white !important; + color: black !important; + } + .portfolio-container { + padding: 0 !important; + margin: 0 !important; + } + .btn-cta, .btn-sec, .project-footer-links { + display: none !important; + } + .project-card, .timeline-card, .edu-card, .contact-card, .skill-badge { + box-shadow: none !important; + border: 1px solid #ccc !important; + background: white !important; + } + .section { + page-break-inside: avoid; + margin-bottom: 2rem !important; + } + } + `; + + let specificCSS = ""; + + if (template === "modern") { + specificCSS = ` + /* Modern: Sleek gradients & modern shadows */ + .portfolio-hero { + padding: 4rem 2rem; + background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.08), rgba(var(--accent-rgb), 0.05)); + border: 1px solid var(--border-color); + border-radius: var(--radius); + margin-bottom: 4rem; + box-shadow: var(--card-shadow); + } + .hero-intro { + display: flex; + align-items: center; + gap: 1.5rem; + margin-bottom: 1.5rem; + } + .profile-pic { + width: 100px; + height: 100px; + border-radius: 50%; + border: 3px solid var(--primary); + box-shadow: 0 0 15px rgba(var(--primary-rgb), 0.25); + object-fit: cover; + } + .dev-name { + font-size: 2.5rem; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.1; + } + .dev-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--accent); + } + .dev-location { + font-size: 0.85rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 0.25rem; + margin-top: 0.25rem; + } + .dev-location svg { + width: 0.85rem; + height: 0.85rem; + } + .dev-bio { + font-size: 1.05rem; + color: var(--text-muted); + max-width: 750px; + margin-bottom: 2rem; + } + .hero-actions { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; + } + .btn-cta, .btn-sec { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + font-weight: 600; + font-size: 0.9rem; + border-radius: var(--radius); + transition: all 0.2s; + cursor: pointer; + } + .btn-cta { + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: white; + box-shadow: 0 4px 10px rgba(var(--primary-rgb), 0.3); + } + .btn-cta:hover { + transform: translateY(-1px); + box-shadow: 0 6px 15px rgba(var(--primary-rgb), 0.4); + } + .btn-sec { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-main); + } + .btn-sec:hover { + background: var(--bg-surface-accent); + } + .social-links { + display: flex; + gap: 0.75rem; + border-top: 1px solid var(--border-color); + padding-top: 1.25rem; + margin-top: 1rem; + } + .social-links a { + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border-radius: 50%; + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-muted); + transition: all 0.2s; + } + .social-links a:hover { + color: var(--primary); + border-color: var(--primary); + transform: translateY(-2px); + } + .social-links a svg { + width: 1.05rem; + height: 1.05rem; + } + + /* Level styling badges */ + .lvl-expert { border-left: 3px solid var(--primary); } + .lvl-advanced { border-left: 3px solid var(--accent); } + .lvl-intermediate { border-left: 3px solid var(--text-muted); } + .lvl-beginner { border-left: 3px solid transparent; } + + @media (max-width: 768px) { + .hero-intro { + flex-direction: column; + align-items: flex-start; + } + .dev-name { + font-size: 2rem; + } + } + `; + } else if (template === "minimal") { + specificCSS = ` + /* Minimal: Fine typography, massive whitespace, raw borders */ + .portfolio-hero { + padding: 4rem 0; + border-bottom: 2px solid var(--text-main); + margin-bottom: 4rem; + } + .hero-intro { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1.5rem; + } + .profile-pic { + width: 80px; + height: 80px; + border-radius: 4px; + object-fit: cover; + margin-bottom: 0.5rem; + } + .dev-name { + font-size: 3rem; + font-weight: 700; + letter-spacing: -0.04em; + line-height: 1.05; + } + .dev-title { + font-size: 1.35rem; + font-weight: 400; + color: var(--text-muted); + } + .dev-location { + font-size: 0.85rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 0.25rem; + } + .dev-location svg { width: 0.85rem; height: 0.85rem; } + .dev-bio { + font-size: 1.1rem; + color: var(--text-main); + max-width: 720px; + margin-bottom: 2rem; + line-height: 1.7; + } + .hero-actions { + display: flex; + gap: 1.5rem; + margin-bottom: 1.5rem; + } + .btn-cta, .btn-sec { + font-weight: 700; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0.5rem 0; + display: inline-flex; + align-items: center; + gap: 0.4rem; + } + .btn-cta { + color: var(--primary); + border-bottom: 2px solid var(--primary); + } + .btn-cta:hover { + color: var(--accent); + border-bottom-color: var(--accent); + } + .btn-sec { + color: var(--text-muted); + border-bottom: 2px solid transparent; + } + .btn-sec:hover { + color: var(--text-main); + border-bottom-color: var(--text-main); + } + .social-links { + display: flex; + gap: 1.25rem; + margin-top: 1.5rem; + } + .social-links a { + color: var(--text-muted); + font-size: 0.85rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; + } + .social-links a:hover { + color: var(--text-main); + } + .social-links a svg { + width: 1rem; + height: 1rem; + } + + /* Section adjustments */ + .section-title { + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.5rem; + } + .section-title::after { + content: ""; + position: absolute; + width: 40px; + height: 2px; + background: var(--primary); + bottom: -1px; + left: 0; + } + + /* Reset card styling to transparent borders */ + .timeline-card, .edu-card, .project-card, .contact-card { + background: transparent; + border: none; + border-bottom: 1px solid var(--border-color); + border-radius: 0 !important; + padding: 1.5rem 0; + box-shadow: none; + } + .project-card:hover { + transform: none; + } + .project-footer-links { + background: transparent; + border: none; + justify-content: flex-start; + gap: 1.5rem; + margin-top: 1rem; + } + .proj-link { + flex: none; + padding: 0; + color: var(--primary); + font-weight: 700; + text-transform: uppercase; + font-size: 0.75rem; + border: none; + } + + .skill-badge { + background: transparent; + border: 1px solid var(--border-color); + border-radius: 0; + } + `; + } else if (template === "creative") { + specificCSS = ` + /* Creative: Bold, asymmetry, unique timelines, custom shapes */ + .portfolio-hero { + padding: 5rem 2.5rem; + background: var(--bg-surface); + border-radius: var(--radius); + margin-bottom: 4rem; + position: relative; + overflow: hidden; + border: 2px solid var(--primary); + box-shadow: 10px 10px 0px var(--primary); + } + .portfolio-hero::before { + content: ""; + position: absolute; + width: 300px; + height: 300px; + background: radial-gradient(circle, rgba(var(--accent-rgb), 0.15) 0%, transparent 70%); + top: -150px; + right: -100px; + z-index: 0; + } + .hero-content { + position: relative; + z-index: 1; + } + .hero-intro { + display: flex; + flex-direction: row; + align-items: center; + gap: 2rem; + margin-bottom: 2rem; + } + .profile-pic { + width: 130px; + height: 130px; + border-radius: 20px; + transform: rotate(-3deg); + border: 4px solid var(--accent); + object-fit: cover; + box-shadow: var(--shadow-lg); + transition: transform 0.3s; + } + .profile-pic:hover { + transform: rotate(3deg) scale(1.05); + } + .dev-name { + font-size: 3.25rem; + font-weight: 800; + line-height: 1; + letter-spacing: -0.02em; + text-transform: uppercase; + background: linear-gradient(90deg, var(--primary), var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + .dev-title { + font-size: 1.4rem; + font-weight: 700; + color: var(--text-main); + margin-top: 0.5rem; + } + .dev-location { + font-size: 0.9rem; + color: var(--text-muted); + display: inline-flex; + align-items: center; + gap: 0.25rem; + background: var(--bg-surface-accent); + padding: 0.25rem 0.75rem; + border-radius: 20px; + border: 1px solid var(--border-color); + margin-top: 0.5rem; + } + .dev-location svg { width: 0.9rem; height: 0.9rem; } + .dev-bio { + font-size: 1.1rem; + color: var(--text-muted); + margin-bottom: 2.5rem; + border-left: 4px solid var(--accent); + padding-left: 1.25rem; + } + .hero-actions { + display: flex; + gap: 1.25rem; + margin-bottom: 2rem; + flex-wrap: wrap; + } + .btn-cta, .btn-sec { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.9rem 1.8rem; + font-weight: 700; + border-radius: var(--radius); + transition: all 0.2s; + } + .btn-cta { + background: var(--primary); + color: white; + border: 2px solid var(--text-main); + box-shadow: 4px 4px 0 var(--text-main); + } + .btn-cta:hover { + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0 var(--text-main); + } + .btn-sec { + background: var(--bg-body); + color: var(--text-main); + border: 2px solid var(--border-color); + } + .btn-sec:hover { + background: var(--bg-surface); + border-color: var(--text-main); + } + .social-links { + display: flex; + gap: 1rem; + } + .social-links a { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 10px; + background: var(--bg-body); + border: 2px solid var(--border-color); + color: var(--text-main); + transition: all 0.2s; + } + .social-links a:hover { + border-color: var(--primary); + background: var(--primary); + color: white; + transform: translateY(-3px) rotate(5deg); + } + .social-links a svg { width: 1.1rem; height: 1.1rem; } + + /* Skill layout styling */ + .skills-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1rem; + } + .skill-badge { + padding: 1rem; + align-items: center; + text-align: center; + border: 2px solid var(--border-color); + border-radius: var(--radius); + transition: all 0.2s; + } + .skill-badge:hover { + border-color: var(--accent); + transform: translateY(-2px); + } + .lvl-expert { background: rgba(var(--primary-rgb), 0.05); } + .lvl-advanced { background: rgba(var(--accent-rgb), 0.05); } + + /* Project cards styling - asymmetric layout */ + .project-card { + border: 2px solid var(--border-color); + transition: all 0.2s; + } + .project-card:hover { + border-color: var(--accent); + box-shadow: 6px 6px 0 rgba(var(--accent-rgb), 0.2); + } + + @media (max-width: 768px) { + .hero-intro { + flex-direction: column; + text-align: center; + } + .dev-name { + font-size: 2.5rem; + } + .dev-bio { + border-left: none; + border-top: 3px solid var(--accent); + padding-left: 0; + padding-top: 1rem; + } + } + `; + } + + return commonCSS + "\n" + specificCSS; +} + +// Helper: Escape HTML +function escapeHTML(str) { + if (!str) return ""; + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Helper: Hex to RGB string for custom alphas +function hexToRgb(hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + const fullHex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b); + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex); + return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : "79, 70, 229"; +} + +// ========================================== +// 7. EXPORT PROCEDURES +// ========================================== + +// Helper to trigger direct text file download +function downloadFile(content, filename, contentType) { + const blob = new Blob([content], { type: contentType }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 100); +} + +// Exporter: Standing HTML +function exportHTMLOnly() { + const fullHTML = compilePortfolioFullHTML(false); // False = production mode + downloadFile(fullHTML, "index.html", "text/html;charset=utf-8"); +} + +// Exporter: Standing CSS +function exportCSSOnly() { + const css = compileTemplateCSS(appState.theme.template); + + // Inject customized theme variables block on top for export + const t = appState.theme; + const finalCSS = `:root { + --primary: ${t.primaryColor}; + --primary-rgb: ${hexToRgb(t.primaryColor)}; + --accent: ${t.accentColor}; + --accent-rgb: ${hexToRgb(t.accentColor)}; + --font-family: '${t.fontFamily}', sans-serif; + --radius: ${t.borderRadius}; + + /* Dark/Light mode tokens */ + ${t.darkMode ? ` + --bg-body: #0a0915; + --bg-surface: #121124; + --bg-surface-accent: rgba(255, 255, 255, 0.03); + --border-color: rgba(255, 255, 255, 0.06); + --text-main: #f3f4f6; + --text-muted: #9ca3af; + --card-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.4); + ` : ` + --bg-body: #f8fafc; + --bg-surface: #ffffff; + --bg-surface-accent: rgba(0, 0, 0, 0.02); + --border-color: rgba(0, 0, 0, 0.08); + --text-main: #0f172a; + --text-muted: #475569; + --card-shadow: 0 4px 15px -3px rgba(0, 0, 0, 0.05); + `} +} + +${css}`; + + downloadFile(finalCSS, "style.css", "text/css;charset=utf-8"); +} + +// Exporter: Unified ZIP structure +function exportZipBundle() { + if (typeof JSZip === "undefined") { + alert("ZIP generator library is loading, please try again in a moment."); + return; + } + + const p = appState.personalInfo; + const t = appState.theme; + + // Build the clean index.html file linking to style.css + const skillsHTML = t.visibility.skills ? compileSkillsSection() : ""; + const experienceHTML = t.visibility.experience ? compileExperienceSection() : ""; + const educationHTML = t.visibility.education ? compileEducationSection() : ""; + const projectsHTML = t.visibility.projects ? compileProjectsSection() : ""; + const certificationsHTML = t.visibility.certifications ? compileCertificationsSection() : ""; + const achievementsHTML = t.visibility.achievements ? compileAchievementsSection() : ""; + const socialItemsHTML = compileSocialsList(); + + const photoURL = p.photo || "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=150&q=80"; + + const zipIndexHTML = ` + + + + + ${escapeHTML(p.name)} | ${escapeHTML(p.title)} Portfolio + + + + + + + + + + +
+ +
+
+
+
+ ${escapeHTML(p.name)} +
+
+

${escapeHTML(p.name)}

+

${escapeHTML(p.title)}

+ ${p.location ? `

${escapeHTML(p.location)}

` : ""} +
+
+ +

${escapeHTML(p.bio)}

+ +
+ Get In Touch + ${p.website ? ` Website` : ""} +
+ + ${socialItemsHTML ? `` : ""} +
+
+ +
+ ${skillsHTML} + ${experienceHTML} + ${educationHTML} + ${projectsHTML} + ${certificationsHTML} + ${achievementsHTML} + +
+

Contact Details

+
+

Feel free to reach out for project opportunities, open roles, or just to say hello!

+
+
+ + +
+ ${p.phone ? ` +
+ + +
+ ` : ""} + ${p.location ? ` +
+ +
+ Location + ${escapeHTML(p.location)} +
+
+ ` : ""} + ${p.website ? ` +
+ +
+ Website + ${escapeHTML(p.website)} +
+
+ ` : ""} +
+
+
+ +
+ + + +
+ + + + + +`; + + const templateCSS = compileTemplateCSS(t.template); + const zipStyleCSS = `:root { + --primary: ${t.primaryColor}; + --primary-rgb: ${hexToRgb(t.primaryColor)}; + --accent: ${t.accentColor}; + --accent-rgb: ${hexToRgb(t.accentColor)}; + --font-family: '${t.fontFamily}', sans-serif; + --radius: ${t.borderRadius}; + + /* Dark/Light mode tokens */ + ${t.darkMode ? ` + --bg-body: #0a0915; + --bg-surface: #121124; + --bg-surface-accent: rgba(255, 255, 255, 0.03); + --border-color: rgba(255, 255, 255, 0.06); + --text-main: #f3f4f6; + --text-muted: #9ca3af; + --card-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.4); + ` : ` + --bg-body: #f8fafc; + --bg-surface: #ffffff; + --bg-surface-accent: rgba(0, 0, 0, 0.02); + --border-color: rgba(0, 0, 0, 0.08); + --text-main: #0f172a; + --text-muted: #475569; + --card-shadow: 0 4px 15px -3px rgba(0, 0, 0, 0.05); + `} +} + +${templateCSS}`; + + const readmeContent = `# Generated Static Portfolio Website + +This portfolio was generated using the BuildVerse AI Portfolio Builder. + +## Files Included + +- \`index.html\` - The semantic HTML skeleton containing your portfolio sections. +- \`style.css\` - Customized stylesheet styling matching your theme options. + +## Running Locally + +To view the generated portfolio, double-click \`index.html\` to open it directly in any modern browser. + +## Customization + +You can open the \`style.css\` file and adjust primary colors or fonts directly if you wish to do further customization. +`; + + // Create zip + const zip = new JSZip(); + zip.file("index.html", zipIndexHTML); + zip.file("style.css", zipStyleCSS); + zip.file("README.md", readmeContent); + + zip.generateAsync({ type: "blob" }).then((content) => { + const link = document.createElement("a"); + link.href = URL.createObjectURL(content); + link.download = `${p.name.toLowerCase().replace(/\s+/g, "-")}-portfolio.zip`; + link.click(); + setTimeout(() => URL.revokeObjectURL(link.href), 100); + }).catch((err) => { + console.error("ZIP Generation Failed", err); + alert("ZIP Generation Failed, please export HTML & CSS separately."); + }); +} + +// Print to PDF (calls print on iframe) +function printPortfolioPDF() { + const iframe = document.getElementById("portfolio-preview"); + if (iframe && iframe.contentWindow) { + iframe.contentWindow.focus(); + iframe.contentWindow.print(); + } else { + alert("Preview frame is unavailable. Please try again."); + } +} diff --git a/projects/AI Portfolio Builder/style.css b/projects/AI Portfolio Builder/style.css new file mode 100644 index 0000000..664f1b3 --- /dev/null +++ b/projects/AI Portfolio Builder/style.css @@ -0,0 +1,1047 @@ +/* ========================================== + 1. VARIABLES & THEMING + ========================================== */ +:root { + /* Common Brand Colors */ + --brand-primary: #4f46e5; + --brand-primary-hover: #4338ca; + --brand-accent: #06b6d4; + --brand-success: #10b981; + --brand-danger: #ef4444; + --brand-warning: #f59e0b; + + /* Font Stacks */ + --font-dashboard: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Transition Speeds */ + --transition-fast: 0.15s ease; + --transition-normal: 0.25s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --shadow-glass: 0 8px 32px 0 rgba(0, 0, 0, 0.2); +} + +/* Dark Theme (Default) */ +body.dark-theme { + --bg-app: radial-gradient(circle at 10% 20%, rgba(18, 16, 32, 1) 0%, rgba(8, 7, 16, 1) 90%); + --bg-panel: rgba(20, 18, 33, 0.65); + --bg-panel-solid: #141221; + --bg-panel-border: rgba(255, 255, 255, 0.07); + --bg-input: rgba(255, 255, 255, 0.03); + --bg-input-border: rgba(255, 255, 255, 0.1); + --bg-input-focus: rgba(79, 70, 229, 0.12); + --color-text-main: #f3f4f6; + --color-text-muted: #9ca3af; + --color-text-inverse: #0f172a; + --bg-card: rgba(255, 255, 255, 0.02); + --bg-card-border: rgba(255, 255, 255, 0.05); + --bg-header: rgba(13, 11, 23, 0.8); + --bg-footer: rgba(8, 7, 16, 0.9); + --scrollbar-thumb: rgba(255, 255, 255, 0.15); + --scrollbar-track: rgba(0, 0, 0, 0.2); +} + +/* Light Theme */ +body.light-theme { + --bg-app: radial-gradient(circle at 12% 24%, #f1f5f9 0%, #e2e8f0 100%); + --bg-panel: rgba(255, 255, 255, 0.7); + --bg-panel-solid: #ffffff; + --bg-panel-border: rgba(0, 0, 0, 0.08); + --bg-input: rgba(255, 255, 255, 0.8); + --bg-input-border: rgba(0, 0, 0, 0.12); + --bg-input-focus: rgba(79, 70, 229, 0.06); + --color-text-main: #0f172a; + --color-text-muted: #475569; + --color-text-inverse: #ffffff; + --bg-card: rgba(0, 0, 0, 0.02); + --bg-card-border: rgba(0, 0, 0, 0.06); + --bg-header: rgba(255, 255, 255, 0.85); + --bg-footer: rgba(226, 232, 240, 0.9); + --scrollbar-thumb: rgba(0, 0, 0, 0.2); + --scrollbar-track: rgba(0, 0, 0, 0.05); +} + +/* ========================================== + 2. RESET & GLOBAL STYLES + ========================================== */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-dashboard); + color: var(--color-text-main); + background: var(--bg-app); + background-attachment: fixed; + line-height: 1.5; + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-x: hidden; + transition: background 0.3s ease, color 0.3s ease; +} + +input, textarea, select, button { + font-family: inherit; + color: inherit; +} + +/* Custom Scrollbars */ +.scrollbar-styled::-webkit-scrollbar { + width: 6px; +} +.scrollbar-styled::-webkit-scrollbar-track { + background: var(--scrollbar-track); +} +.scrollbar-styled::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 4px; +} +.scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: rgba(79, 70, 229, 0.4); +} + +/* Keyboard Accessibility Focus styling */ +*:focus-visible { + outline: 2px solid var(--brand-primary); + outline-offset: 2px; +} + +/* ========================================== + 3. APP CONTAINER & LAYOUT + ========================================== */ +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* App Header */ +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background: var(--bg-header); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--bg-panel-border); + position: sticky; + top: 0; + z-index: 100; + box-shadow: var(--shadow-sm); +} + +.header-logo { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.logo-icon { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background: linear-gradient(135deg, var(--brand-primary), var(--brand-accent)); + border-radius: 8px; + color: white; +} + +.header-logo h1 { + font-size: 1.25rem; + font-weight: 700; + letter-spacing: -0.025em; + background: linear-gradient(135deg, var(--color-text-main), var(--color-text-muted)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.header-logo .subtitle { + font-size: 0.75rem; + color: var(--color-text-muted); +} + +.header-actions { + display: flex; + align-items: center; + gap: 0.75rem; +} + +/* Workspace layout */ +.workspace { + flex: 1; + display: grid; + grid-template-columns: 460px 1fr; + height: calc(100vh - 72px - 50px); /* header + footer offset */ + overflow: hidden; +} + +/* Editor Panel (Left) */ +.editor-panel { + border-right: 1px solid var(--bg-panel-border); + background: var(--bg-panel); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + overflow-y: auto; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Live Preview Panel (Right) */ +.preview-panel { + background: rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ========================================== + 4. ACCORDIONS & EDITOR FORMS + ========================================== */ +.accordion-item { + background: var(--bg-card); + border: 1px solid var(--bg-card-border); + border-radius: 12px; + overflow: hidden; + transition: all var(--transition-normal); +} + +.accordion-item:hover { + border-color: rgba(79, 70, 229, 0.25); + box-shadow: var(--shadow-sm); +} + +.accordion-item.active { + border-color: rgba(79, 70, 229, 0.35); + background: rgba(79, 70, 229, 0.02); +} + +.accordion-header { + width: 100%; + padding: 1rem 1.25rem; + background: none; + border: none; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + text-align: left; + transition: background var(--transition-fast); +} + +.accordion-header:hover { + background: rgba(255, 255, 255, 0.02); +} + +.accordion-title { + font-size: 0.95rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.75rem; + color: var(--color-text-main); +} + +.accordion-title svg { + width: 1.1rem; + height: 1.1rem; + color: var(--brand-primary); +} + +.accordion-chevron { + width: 1.1rem; + height: 1.1rem; + color: var(--color-text-muted); + transition: transform var(--transition-normal); +} + +.accordion-item.active .accordion-chevron { + transform: rotate(180deg); + color: var(--brand-primary); +} + +.accordion-content { + display: none; + padding: 0 1.25rem 1.25rem 1.25rem; + border-top: 1px solid rgba(255, 255, 255, 0.03); +} + +.accordion-item.active .accordion-content { + display: block; +} + +/* Optional badge */ +.badge-optional { + font-size: 0.7rem; + font-weight: 500; + padding: 0.1rem 0.4rem; + background: rgba(255, 255, 255, 0.08); + border-radius: 4px; + color: var(--color-text-muted); + margin-left: 0.25rem; +} + +/* Grids and Forms */ +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-top: 1rem; +} + +.col-span-2 { + grid-column: span 2; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.form-group label { + font-size: 0.8rem; + font-weight: 500; + color: var(--color-text-muted); + display: flex; + align-items: center; + gap: 0.25rem; +} + +.form-group label .required { + color: var(--brand-danger); +} + +.inline-icon { + width: 0.85rem; + height: 0.85rem; +} + +input[type="text"], +input[type="email"], +input[type="url"], +input[type="tel"], +input[type="date"], +select, +textarea { + background: var(--bg-input); + border: 1px solid var(--bg-input-border); + border-radius: 8px; + padding: 0.6rem 0.8rem; + font-size: 0.875rem; + transition: all var(--transition-fast); + color: var(--color-text-main); + outline: none; +} + +input[type="text"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="tel"]:focus, +input[type="date"]:focus, +select:focus, +textarea:focus { + border-color: var(--brand-primary); + background: var(--bg-input-focus); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.2); +} + +textarea { + resize: vertical; +} + +/* Dynamic list templates (Experience, Education, etc.) */ +.dynamic-list-container { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 1rem; +} + +.dynamic-item-card { + background: rgba(255, 255, 255, 0.01); + border: 1px solid rgba(255, 255, 255, 0.04); + border-radius: 8px; + padding: 1rem; + position: relative; + transition: border var(--transition-fast); +} + +.dynamic-item-card:hover { + border-color: rgba(255, 255, 255, 0.08); +} + +.item-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.item-index-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--brand-primary); +} + +.btn-remove-item { + color: var(--brand-danger); + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + border-radius: 4px; + transition: background var(--transition-fast); +} + +.btn-remove-item:hover { + background: rgba(239, 68, 68, 0.1); +} + +.btn-remove-item svg { + width: 1rem; + height: 1rem; +} + +/* ========================================== + 5. TEMPLATE & CUSTOMIZER ITEMS + ========================================== */ +.template-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + margin-top: 0.75rem; +} + +.template-card { + cursor: pointer; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.template-card input[type="radio"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.template-preview-box { + width: 100%; + height: 70px; + border-radius: 8px; + border: 2px solid var(--bg-input-border); + background: #0d0c15; + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px; + overflow: hidden; + transition: all var(--transition-normal); + position: relative; +} + +.template-card:hover .template-preview-box { + border-color: rgba(79, 70, 229, 0.5); + transform: translateY(-2px); +} + +.template-card input[type="radio"]:checked + .template-preview-box { + border-color: var(--brand-primary); + box-shadow: 0 0 10px rgba(79, 70, 229, 0.35); + background: #11101e; +} + +.template-name { + font-size: 0.75rem; + font-weight: 500; + color: var(--color-text-muted); + transition: color var(--transition-fast); +} + +.template-card input[type="radio"]:checked ~ .template-name { + color: var(--color-text-main); + font-weight: 600; +} + +/* Preview layouts indicators */ +.mock-hero { + height: 14px; + background: var(--brand-primary); + opacity: 0.5; + border-radius: 3px; +} +.mock-grid { + display: flex; + gap: 4px; +} +.mock-grid div { + flex: 1; + height: 24px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; +} +.mock-line { + height: 8px; + width: 50%; + background: var(--brand-accent); + opacity: 0.6; + border-radius: 2px; +} +.mock-text { + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} +.mock-circle { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--brand-primary); + opacity: 0.6; +} +.mock-timeline { + flex: 1; + border-left: 2px dotted rgba(255, 255, 255, 0.2); + margin-left: 6px; +} + +/* Theme Presets Selection */ +.preset-theme-selector { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.preset-btn { + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--bg-input-border); + border-radius: 8px; + padding: 0.5rem; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: all var(--transition-fast); +} + +.preset-btn:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.2); +} + +.preset-btn .dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; +} + +/* Color input customization styling */ +.color-picker-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; +} + +input[type="color"] { + -webkit-appearance: none; + border: none; + width: 32px; + height: 32px; + border-radius: 6px; + cursor: pointer; + background: none; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: 1px solid var(--bg-input-border); + border-radius: 6px; +} + +.hex-display { + font-size: 0.75rem; + font-family: 'JetBrains Mono', monospace; + color: var(--color-text-muted); + text-transform: uppercase; +} + +/* Range input styling */ +input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 6px; + border-radius: 3px; + background: var(--bg-input-border); + outline: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--brand-primary); + cursor: pointer; + transition: transform var(--transition-fast); +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +/* Toggle switch component styling */ +.toggle-control { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; +} + +.toggle-control input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.toggle-slider { + position: relative; + display: inline-block; + width: 36px; + height: 20px; + background-color: var(--bg-input-border); + border-radius: 20px; + transition: background-color var(--transition-normal); +} + +.toggle-slider::before { + content: ""; + position: absolute; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + transition: transform var(--transition-normal); +} + +.toggle-control input:checked + .toggle-slider { + background-color: var(--brand-primary); +} + +.toggle-control input:checked + .toggle-slider::before { + transform: translateX(16px); +} + +.toggle-label { + font-size: 0.8rem; + font-weight: 500; + color: var(--color-text-muted); +} + +.toggle-control input:checked ~ .toggle-label { + color: var(--color-text-main); +} + +/* Checkbox visibility layout */ +.visibility-checkbox-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.checkbox-control { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-size: 0.75rem; + font-weight: 500; + color: var(--color-text-muted); + user-select: none; +} + +.checkbox-control input { + width: 14px; + height: 14px; + accent-color: var(--brand-primary); +} + +/* ========================================== + 6. BUTTONS & ACTIONS + ========================================== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.6rem 1rem; + font-size: 0.85rem; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + border: 1px solid transparent; + transition: all var(--transition-fast); + text-decoration: none; +} + +.btn svg { + width: 1.1rem; + height: 1.1rem; +} + +.btn-primary { + background: var(--brand-primary); + color: white; +} + +.btn-primary:hover { + background: var(--brand-primary-hover); +} + +.btn-primary-gradient { + background: linear-gradient(135deg, var(--brand-primary), var(--brand-accent)); + color: white; + border: none; +} + +.btn-primary-gradient:hover { + box-shadow: 0 0 12px rgba(79, 70, 229, 0.4); + transform: translateY(-1px); +} + +.btn-indigo { + background: #312e81; + color: #e0e7ff; + border: 1px solid #4338ca; +} + +.btn-indigo:hover { + background: #3730a3; +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--bg-input-border); + color: var(--color-text-main); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.09); + border-color: rgba(255, 255, 255, 0.2); +} + +.btn-icon { + width: 2.25rem; + height: 2.25rem; + padding: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--bg-input-border); + color: var(--color-text-main); +} + +.btn-icon:hover { + background: rgba(255, 255, 255, 0.08); +} + +.btn-sm { + padding: 0.4rem 0.75rem; + font-size: 0.75rem; +} + +.btn-add { + margin-top: 0.75rem; + width: 100%; +} + +/* Theme Toggle Button icon states */ +.sun-icon { + display: none; +} +.moon-icon { + display: block; +} + +body.light-theme .sun-icon { + display: block; +} +body.light-theme .moon-icon { + display: none; +} + +/* Export Panel Layout Card */ +.export-actions-card { + margin-top: 1rem; + padding: 1.25rem; + background: linear-gradient(135deg, rgba(79, 70, 229, 0.07), rgba(6, 182, 212, 0.04)); + border: 1px solid rgba(79, 70, 229, 0.15); + border-radius: 12px; +} + +.export-actions-card h3 { + font-size: 0.95rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.35rem; +} + +.export-actions-card h3 svg { + color: var(--brand-accent); + width: 1.1rem; + height: 1.1rem; +} + +.export-desc { + font-size: 0.75rem; + color: var(--color-text-muted); + margin-bottom: 1rem; +} + +.export-btn-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; +} + +.custom-css-icon { + font-style: normal; + font-weight: 800; + font-size: 0.75rem; +} + +/* ========================================== + 7. LIVE PREVIEW WRAPPER & CONTROLS + ========================================= */ +.preview-header { + padding: 1rem 1.5rem; + background: var(--bg-header); + border-bottom: 1px solid var(--bg-panel-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.preview-title { + font-size: 0.9rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.live-dot { + width: 8px; + height: 8px; + background: var(--brand-success); + border-radius: 50%; + box-shadow: 0 0 8px var(--brand-success); + display: inline-block; + animation: pulse-dot 1.8s infinite; +} + +.device-switcher { + display: flex; + background: rgba(0, 0, 0, 0.15); + border: 1px solid var(--bg-panel-border); + border-radius: 20px; + padding: 2px; +} + +.device-btn { + background: none; + border: none; + color: var(--color-text-muted); + width: 32px; + height: 32px; + border-radius: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); +} + +.device-btn:hover { + color: var(--color-text-main); + background: rgba(255, 255, 255, 0.03); +} + +.device-btn.active { + color: var(--brand-primary); + background: rgba(79, 70, 229, 0.1); + box-shadow: var(--shadow-sm); +} + +.device-btn svg { + width: 1.1rem; + height: 1.1rem; +} + +/* Preview Simulator Body */ +.preview-container { + flex: 1; + padding: 2rem; + overflow: auto; + display: flex; + justify-content: center; + align-items: center; +} + +.iframe-wrapper { + background: #ffffff; + border-radius: 12px; + overflow: hidden; + box-shadow: var(--shadow-xl); + transition: width var(--transition-slow), height var(--transition-slow), border-radius var(--transition-slow); + position: relative; + display: flex; + flex-direction: column; +} + +/* Device simulation frames styling */ +.device-desktop { + width: 100%; + height: 100%; + border: 1px solid var(--bg-panel-border); +} + +.device-tablet { + width: 768px; + height: 95%; + max-height: 900px; + border: 12px solid #222; + border-radius: 24px; +} + +.device-mobile { + width: 375px; + height: 90%; + max-height: 720px; + border: 14px solid #222; + border-radius: 36px; +} + +#portfolio-preview { + width: 100%; + height: 100%; + border: none; + background: #fff; +} + +/* ========================================== + 8. FOOTER + ========================================= */ +.app-footer { + text-align: center; + padding: 0.8rem 2rem; + background: var(--bg-footer); + border-top: 1px solid var(--bg-panel-border); + font-size: 0.75rem; + color: var(--color-text-muted); +} + +/* ========================================== + 9. ANIMATIONS + ========================================= */ +@keyframes pulse-dot { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); + } + 70% { + transform: scale(1); + box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); + } + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); + } +} + +/* ========================================== + 10. RESPONSIVE MEDIA QUERIES + ========================================= */ +@media (max-width: 1024px) { + .workspace { + grid-template-columns: 1fr; + height: auto; + overflow: visible; + } + + .editor-panel { + border-right: none; + border-bottom: 1px solid var(--bg-panel-border); + height: auto; + overflow-y: visible; + } + + .preview-panel { + height: 600px; + } + + .preview-container { + padding: 1rem; + } + + .device-tablet { + width: 100%; + max-width: 768px; + } +} + +@media (max-width: 768px) { + .app-header { + padding: 1rem; + } + + .header-logo .subtitle { + display: none; + } + + .template-grid { + grid-template-columns: 1fr; + } + + .device-switcher { + display: none; /* Hide device simulator on small screens */ + } + + .device-mobile { + width: 100%; + border: none; + border-radius: 8px; + height: 100%; + } +} diff --git a/projects/AI Travel Itinerary Planner/README.md b/projects/AI Travel Itinerary Planner/README.md new file mode 100644 index 0000000..7e0c88a --- /dev/null +++ b/projects/AI Travel Itinerary Planner/README.md @@ -0,0 +1,69 @@ +# Vagabond — AI Travel Itinerary Planner + +**Vagabond** is a premium, feature-rich, and fully client-side static web application designed to help travelers plan their vacations effortlessly. With custom destinations, dynamic itinerary timelines, responsive design themes, packing checklists, and interactive budget estimators, it provides a seamless user experience that runs directly in the browser. + +## Features + +- **Personalized Itinerary Generator**: Builds tailored day-by-day morning, midday, and night activities based on travel interests, vacation style, and duration. +- **Curated Attractions Database**: Features rich localized recommendations, estimated costs, and expert tips for popular destinations (Paris, Tokyo, New York, London, Dubai, Singapore, Bali, Goa). +- **Universal Custom Destination Engine**: Gracefully handles any custom destination with dynamic layout structures and custom fallback suggestions. +- **Budget Estimator Dashboard**: Displays granular accommodation, dining, transportation, and activity cost breakdowns visualized with progress bars and an animated circular utilization gauge. +- **Dynamic Packing Checklist**: Automatically generates categories of packing checklists based on destination, season, and travel style. Items can be checked off and persist in LocalStorage. +- **Saved Trips Log**: LocalStorage-backed trip directory allowing users to save, delete, reload, and clear their vacation itineraries. +- **Adaptive Light/Dark Themes**: Modern aesthetics utilizing glassmorphism panels, soft glowing gradients, smooth micro-interactions, and theme persistence. +- **Accessibility & Responsive Layouts**: Built using semantic HTML5, high-contrast text ratios, visible focus styling, keyboard-friendly navigation, and adaptive layouts for all viewports (from 320px to 1440px+). + +## Technologies + +- **HTML5**: Semantic tags, accessible layout forms, and ARIA attributes. +- **CSS3 Variables**: Custom themes, media queries, grid structures, and backdrop filters. +- **Vanilla JavaScript (ES6)**: State management, local storage manipulation, animated counters, and DOM rendering. +- **Lucide Icons**: Modern vector icon support. +- **Google Fonts**: Inter (body text) and Outfit (display titles). + +## Folder Structure + +```text +projects/ +└── AI Travel Itinerary Planner/ + ├── index.html + ├── style.css + ├── script.js + ├── README.md + ├── project.json + └── assets/ + ├── images/ + ├── icons/ + └── screenshots/ +``` + +## Installation + +This is a standalone static web application that does not require any backend services or npm package installs. + +1. Clone or download the BuildVerse repository. +2. Locate the project directory: `projects/AI Travel Itinerary Planner/`. +3. Open `index.html` in any web browser (Chrome, Safari, Firefox, Edge, etc.) to run the application instantly. + +## Usage + +1. **Select Destination**: Choose a curated city from the dropdown list, or choose "Custom Destination" and write in your preferred city. +2. **Configure Details**: Input the trip duration (1-30 days), start date, and total budget. +3. **Customize Style**: Select your travel style (Solo, Couple, Family, etc.), preferred transport type, accommodation tier, and checkboxes matching your personal interests (Nature, Food, Photography, etc.). +4. **Generate**: Click **Generate Itinerary** to animate the stats dashboard, load the weather forecast, display local attractions, generate the packing list, and inject the daily timeline nodes. +5. **Interact**: Check off items in the checklist, toggle day cards to view slot schedules, and see if your estimated cost fits inside your circle budget gauge. +6. **Save & Reload**: Click **Save Itinerary** to store it in local storage. Scroll to the bottom to view, reload, or delete past planned trips. + +## Screenshots + +*(Screenshots will be captured and placed inside the `assets/screenshots/` folder)* + +## Future Enhancements + +- **Interactive Map Routing**: Embed OpenStreetMap nodes to show optimal travel paths between morning and afternoon activities. +- **Live APIs**: Integrate live weather forecast APIs and real-time exchange rate calculators. +- **Export to PDF**: Allow users to download a beautifully styled PDF brochure of their itinerary and packing checklist. + +## License + +This project is licensed under the MIT License - see the main repository license details. diff --git a/projects/AI Travel Itinerary Planner/assets/icons/.gitkeep b/projects/AI Travel Itinerary Planner/assets/icons/.gitkeep new file mode 100644 index 0000000..fb12942 --- /dev/null +++ b/projects/AI Travel Itinerary Planner/assets/icons/.gitkeep @@ -0,0 +1 @@ +# gitkeep diff --git a/projects/AI Travel Itinerary Planner/assets/images/.gitkeep b/projects/AI Travel Itinerary Planner/assets/images/.gitkeep new file mode 100644 index 0000000..fb12942 --- /dev/null +++ b/projects/AI Travel Itinerary Planner/assets/images/.gitkeep @@ -0,0 +1 @@ +# gitkeep diff --git a/projects/AI Travel Itinerary Planner/assets/screenshots/.gitkeep b/projects/AI Travel Itinerary Planner/assets/screenshots/.gitkeep new file mode 100644 index 0000000..fb12942 --- /dev/null +++ b/projects/AI Travel Itinerary Planner/assets/screenshots/.gitkeep @@ -0,0 +1 @@ +# gitkeep diff --git a/projects/AI Travel Itinerary Planner/index.html b/projects/AI Travel Itinerary Planner/index.html new file mode 100644 index 0000000..87c4c24 --- /dev/null +++ b/projects/AI Travel Itinerary Planner/index.html @@ -0,0 +1,505 @@ + + + + + + Vagabond — AI Travel Itinerary Planner + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+

Vagabond

+

AI Travel Itinerary Planner

+
+
+ +
+
+ "Your journey, perfectly planned." +
+ +
+
+
+ + +
+ + +
+
+

Wander Smart. Plan Effortlessly.

+

Craft your perfect getaway with AI-inspired recommendations. Select your target destination, set your budget, define your travel preferences, and instantly receive a curated day-by-day plan complete with activities, weather tips, packing checklists, and local secrets.

+
+
+
+ 0 + Trips Saved +
+
+ 8+ + Curated Cities +
+
+
+ + +
+ + +
+

+ + Create New Itinerary +

+ +
+
+ +
+ + +
+
+ + + + +
+
+ + + Up to 30 days. +
+ +
+ + +
+
+ +
+ +
+ $ + +
+ Total amount in USD. +
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+ +
+
+ Interests (Select all that apply) +
+ + + + + + + + + +
+
+
+ + + + +
+ + +
+
+
+ + +
+ + +
+
+ +
+

Your Itinerary Awaits

+

Fill out the trip planner form on the left with your destination, budget, and styles to generate an AI-inspired, tailored travel itinerary with interactive dashboards.

+
+ + + +
+
+ + +
+
+

+ + My Saved Itineraries +

+
+ +
+
+ +
+ +

No saved itineraries found. Generate and click "Save Itinerary" to store your vacation details.

+
+
+ +
+ + + + + + + + diff --git a/projects/AI Travel Itinerary Planner/project.json b/projects/AI Travel Itinerary Planner/project.json new file mode 100644 index 0000000..5ea3269 --- /dev/null +++ b/projects/AI Travel Itinerary Planner/project.json @@ -0,0 +1,24 @@ +{ + "title": "AI Travel Itinerary Planner", + "description": "A premium, feature-rich web application that helps users plan their custom travel itineraries with AI-inspired recommendations, budget visualizations, saved trips, checklists, and responsive designs.", + "author": { + "name": "Kola Sailaja", + "github": "KolaSailaja" + }, + "githubUsername": "KolaSailaja", + "tags": [ + "Travel", + "AI", + "Itinerary", + "Planner", + "Utility" + ], + "technologies": [ + "HTML5", + "CSS3", + "JavaScript" + ], + "category": "Travel / Utility", + "responsive": true, + "version": "1.0.0" +} diff --git a/projects/AI Travel Itinerary Planner/script.js b/projects/AI Travel Itinerary Planner/script.js new file mode 100644 index 0000000..c62331e --- /dev/null +++ b/projects/AI Travel Itinerary Planner/script.js @@ -0,0 +1,1641 @@ +/** + * Vagabond - AI Travel Itinerary Planner + * Core Application Script + */ + +// ========================================================================== +// 1. CURATED DESTINATIONS DATABASE +// ========================================================================== + +const DESTINATIONS_DATABASE = { + Paris: { + city: "Paris", + country: "France", + theme: "Romantic, Artistic, & Historical", + multipliers: { accommodation: 1.5, food: 1.4, transport: 1.2, activities: 1.3, misc: 1.2 }, + weather: { + Spring: { temp: "15°C", icon: "cloud-sun", desc: "Mild & Pleasant", advice: "Pack layers and an umbrella for occasional spring showers." }, + Summer: { temp: "25°C", icon: "sun", desc: "Warm & Sunny", advice: "Bring light clothing, sunglasses, and sunscreen. Perfect for cafe patios." }, + Autumn: { temp: "14°C", icon: "cloud-drizzle", desc: "Cool & Golden", advice: "Pack a trench coat, scarf, and comfortable walking boots." }, + Winter: { temp: "6°C", icon: "cloud-snow", desc: "Cold & Crisp", advice: "Bring a thick wool coat, gloves, thermal layers, and lip balm." } + }, + attractions: [ + { name: "Eiffel Tower", desc: "The iconic wrought-iron lattice monument.", cost: 30, tip: "Book sunset tickets 3 months in advance." }, + { name: "Louvre Museum", desc: "The world's largest art museum, home to the Mona Lisa.", cost: 22, tip: "Enter through the Carousel du Louvre entrance to skip main lines." }, + { name: "Sainte-Chapelle", desc: "A Gothic chapel with jaw-dropping 13th-century stained glass.", cost: 15, tip: "Visit on a sunny day morning for maximum stained glass glow." }, + { name: "Seine River Cruise", desc: "A scenic boat tour cruising past historical monuments.", cost: 18, tip: "Take a night cruise when the Eiffel Tower is illuminated." }, + { name: "Palace of Versailles", desc: "The opulent former royal residence of King Louis XIV.", cost: 25, tip: "Rent a bike in the gardens to explore the massive grounds." }, + { name: "Montmartre & Sacré-Cœur", desc: "A hilltop bohemian neighborhood with panoramic city views.", cost: 0, tip: "Watch out for street scammers near the lower steps." } + ], + restaurants: [ + { name: "Le Bistrot Paul Bert", style: "Traditional Bistro", desc: "Famed classic steak frites and grand soufflés.", avgCost: 45 }, + { name: "L'As du Fallafel", style: "Street Food", desc: "Renowned falafel wraps in the Jewish Quarter (Le Marais).", avgCost: 12 }, + { name: "Café de Flore", style: "Historical Cafe", desc: "Iconic coffee spot frequented by writers and philosophers.", avgCost: 22 }, + { name: "Epicure", style: "Fine Dining", desc: "3-Michelin-starred luxurious French culinary experience.", avgCost: 280 } + ], + activities: { + Nature: [ + { title: "Luxembourg Gardens Walk", desc: "Stroll along gravel paths, see the Medici Fountain, and watch vintage wooden sailboats on the grand pond." }, + { title: "Canal Saint-Martin Picnic", desc: "Join locals sitting on the canal edge with wine, cheese, and fresh baguettes." }, + { title: "Jardin des Plantes Botanical Gardens", desc: "Wander through historic glasshouses, alpine gardens, and cherry blossom trees." } + ], + Adventure: [ + { title: "Explore the Catacombs", desc: "Walk through the chilling underground ossuary housing the bones of millions." }, + { title: "Climb the Arc de Triomphe", desc: "Ascend the 284 spiral steps for an unmatched view of the twelve radiating avenues." }, + { title: "Verdon Gorge Day Trip", desc: "Climb, kayak, or hike the dramatic limestone canyon trails." } + ], + Food: [ + { title: "Cheese & Wine Masterclass", desc: "Taste aged artisanal cheeses paired with grand cru wines in a vaulted cellar." }, + { title: "Croissant Baking Workshop", desc: "Learn the secrets of folding puff pastry dough from a local Parisian chef." }, + { title: "Rue Montorgueil Tasting Stroll", desc: "Graze on fresh oysters, macarons, and warm escargots along a historic food street." } + ], + History: [ + { title: "Marais Guided Heritage Walk", desc: "Discover medieval houses, Jewish history, and royal plazas like Place des Vosges." }, + { title: "Conciergerie Revolutionary Tour", desc: "Walk through the medieval palace cells where Marie Antoinette was imprisoned." }, + { title: "Pere Lachaise Cemetery Tour", desc: "Find the graves of Oscar Wilde, Edith Piaf, and Jim Morrison." } + ], + Museums: [ + { title: "Musée de l'Orangerie", desc: "Sit in the oval rooms surrounding Claude Monet's massive Water Lilies canvases." }, + { title: "Centre Pompidou Art Exploration", desc: "Browse Europe's largest modern art collection inside an inside-out high-tech building." }, + { title: "Musée Rodin Sculpture Gardens", desc: "See 'The Thinker' set amongst lush flowerbeds and fountains." } + ], + Shopping: [ + { title: "Les Puces de Saint-Ouen Flea Market", desc: "Browse vintage trinkets, luxury antiques, and retro clothing in a massive market complex." }, + { title: "Galeries Lafayette Dome View", desc: "Shop under the spectacular neo-byzantine glass dome and walk the glass skywalk." }, + { title: "Rue du Faubourg Saint-Honoré", desc: "Window shop at some of the world's most exclusive haute couture fashion houses." } + ], + Nightlife: [ + { title: "Moulin Rouge Cabaret Show", desc: "Experience the world-famous French Cancan dance with champagne." }, + { title: "Caveau de la Huchette Jazz Club", desc: "Dance to live swing music in a medieval cellar that inspired La La Land." }, + { title: "Speakeasy Hunting in Bastille", desc: "Locate hidden doors leading to premium craft cocktail lounges." } + ], + Photography: [ + { title: "Trocadéro Sunrise Photo Session", desc: "Capture the golden hour lighting up the Eiffel Tower without crowds." }, + { title: "Rue de l'Université Shoot", desc: "Photograph the towering monument framed by cobblestones and classic Haussmann buildings." }, + { title: "Sinking House Illusion Capture", desc: "Take a clever perspective photo next to the lawns of Sacré-Cœur." } + ], + General: [ + { title: "Seine Riverbank Stroll", desc: "Walk along the UNESCO-listed banks, browsing booksellers (bouquinistes) and bridges." }, + { title: "Belleville Panoramic View", desc: "Take in a local, alternative view of the city skyline away from main tourist spots." }, + { title: "Place du Tertre Artist Watch", desc: "Watch street painters sketch portraits in the heart of Montmartre." } + ] + } + }, + Tokyo: { + city: "Tokyo", + country: "Japan", + theme: "Futuristic, Traditional, & Culinary", + multipliers: { accommodation: 1.3, food: 1.1, transport: 1.1, activities: 1.2, misc: 1.1 }, + weather: { + Spring: { temp: "16°C", icon: "cherry-blossom", desc: "Sakura Season / Mild", advice: "Book accommodation early. Carry a light cardigan." }, + Summer: { temp: "28°C", icon: "sun-dim", desc: "Hot & Humid", advice: "Wear breathable clothing. Keep hydrated and look for indoor AC spots." }, + Autumn: { temp: "18°C", icon: "leaf", desc: "Cool & Colorful foliage", advice: "Perfect weather for hiking and walking. Bring a light jacket." }, + Winter: { temp: "7°C", icon: "cloud-snow", desc: "Cold & Sunny", advice: "Dry air, clear skies (ideal for viewing Mt. Fuji). Pack a thick jacket." } + }, + attractions: [ + { name: "Shibuya Crossing & Hachiko", desc: "The world's busiest pedestrian scramble crossing.", cost: 0, tip: "Get a window seat at L'Occitane Cafe for great aerial videos." }, + { name: "Senso-ji Temple", desc: "Tokyo's oldest and most sacred Buddhist temple complex.", cost: 0, tip: "Visit at night when the lanterns and pagoda are illuminated and quiet." }, + { name: "Tokyo Skytree", desc: "The tallest structure in Japan, providing infinite views.", cost: 25, tip: "Check weather visibility parameters before purchasing tickets." }, + { name: "teamLab Planets", desc: "An immersive, body-on digital art museum walking through water.", cost: 28, tip: "Wear shorts/pants that roll up, as water depth reaches calf level." }, + { name: "Meiji Shrine", desc: "A peaceful shrine nestled inside a dense forest of 120,000 trees.", cost: 0, tip: "Look for traditional Shinto weddings walking through the courtyard." }, + { name: "Tsukiji Outer Market", desc: "A vibrant market packed with fresh sushi stalls and street food.", cost: 0, tip: "Arrive hungry around 8:00 AM; try the tamagoyaki (sweet omelette)." } + ], + restaurants: [ + { name: "Ichiran Ramen Shinjuku", style: "Casual Ramen", desc: "Tonkotsu ramen eaten in individual solo dining booths.", avgCost: 15 }, + { name: "Sukiyabashi Jiro", style: "Fine Dining", desc: "World-famous legendary sushi counter (reservation required).", avgCost: 350 }, + { name: "Shinjuku Omoide Yokocho", style: "Yakitori Stalls", desc: "Atmospheric alleyways serving grilled skewers over hot coals.", avgCost: 25 }, + { name: "Harajuku Gyoza Lou", style: "Dumpling Spot", desc: "Crispy pan-fried or steamed pork and chive gyoza.", avgCost: 10 } + ], + activities: { + Nature: [ + { title: "Shinjuku Gyoen Garden Stroll", desc: "Wander through French, English landscape, and traditional Japanese tea gardens." }, + { title: "Mount Takao Hiking", desc: "Hike just 50 mins from Tokyo for scenic forest paths and mountain temple shrines." }, + { title: "Ueno Park Boat Rowing", desc: "Rent a swan boat and paddle on Shinobazu Pond surrounded by lotus plants." } + ], + Adventure: [ + { title: "Go-Karting through City Streets", desc: "Drive custom go-karts in Akihabara dressed as your favorite characters (International License required)." }, + { title: "VR Park Shinjuku", desc: "Experience cutting-edge Japanese virtual reality simulations and bungee drops." }, + { title: "Bouldering in Akihabara", desc: "Try indoor rock climbing alongside Tokyo's hobbyists." } + ], + Food: [ + { title: "Sushi Making Workshop", desc: "Learn how to prepare seasoned shari rice and master slice-rolling techniques from a sushi chef." }, + { title: "Izakaya Hopping in Golden Gai", desc: "Explore 200 tiny matchbox bars, tasting highballs and small-plate otsumami snacks." }, + { title: "Depachika Food Hall Crawl", desc: "Sample gourmet sweets, bento boxes, and premium fruits in the basement of Mitsukoshi Ginza." } + ], + History: [ + { title: "Edo-Tokyo Museum Exploration", desc: "See life-sized replicas of historic houses and kabuki theatres from old Edo." }, + { title: "Imperial Palace Garden Tour", desc: "Walk past historical guardhouses, stone walls, and the double bridge." }, + { title: "Asakusa Rickshaw Ride", desc: "Ride in a hand-drawn rickshaw through retro streets while hearing neighborhood history." } + ], + Museums: [ + { title: "Ghibli Museum Mitaka", desc: "Step inside the whimsical world of Hayao Miyazaki (Tickets must be bought on the 10th of the previous month)." }, + { title: "National Museum of Nature & Science", desc: "See dinosaur skeletons and historical technological innovations in Ueno." }, + { title: "Yayoi Kusama Museum", desc: "Interact with the legendary artist's dot patterns and infinity mirror installations." } + ], + Shopping: [ + { title: "Akihabara Retro Gaming Quest", desc: "Shop for vintage consoles and collectibles at Super Potato and Mandarake." }, + { title: "Harajuku Takeshita Street fashion hunt", desc: "Browse colorful fashion, crazy socks, and buy a giant rainbow cotton candy." }, + { title: "Ginza Luxury Window Shopping", desc: "Visit massive flagship stores, stationery megastores (Itoya), and art gallery basements." } + ], + Nightlife: [ + { title: "Karaoke Kan Shinjuku", style: "Karaoke", desc: "Rent a private room with neon lights and tambourines, singing classic hits." }, + { title: "Roppongi Hills Clubbing", desc: "Dance at high-energy electronic clubs frequented by international visitors." }, + { title: "Yurakucho Girders Drinks", desc: "Enjoy beer and skewers under the active train tracks with local businessmen." } + ], + Photography: [ + { title: "Kabukicho Neon Night Shoot", desc: "Capture the glowing neon signs, Godzilla head, and busy street crossways." }, + { title: "Meguro River Sakura Photography", desc: "Photograph cherry blossoms draping over a quiet canal illuminated by pink lanterns." }, + { title: "Hie Shrine Torii Corridor", desc: "Get a scenic shot of red Torii gates winding up a hill in the middle of skyscrapers." } + ], + General: [ + { title: "Tsukiji Fish Breakfast", desc: "Graze on grilled wagyu skewers, raw sea urchin, strawberry mochi, and tamago." }, + { title: "Sensory Walk in Akihabara", desc: "Immerse yourself in arcade game soundscapes and anime billboard sights." }, + { title: "Senso-ji Fortunes (Omikuji)", desc: "Shake a wooden cylinder, draw a fortune slip, and tie it to the temple wires." } + ] + } + }, + "New York": { + city: "New York", + country: "USA", + theme: "Vibrant, Urban, & Cinematic", + multipliers: { accommodation: 1.8, food: 1.5, transport: 1.3, activities: 1.4, misc: 1.3 }, + weather: { + Spring: { temp: "14°C", icon: "cloud-sun", desc: "Crisp & Blooming", advice: "Central Park looks gorgeous. Carry a light jacket and comfortable walk sneakers." }, + Summer: { temp: "27°C", icon: "sun", desc: "Hot & Muggy", advice: "Drink plenty of water. Escape the heat in air-conditioned museums." }, + Autumn: { temp: "16°C", icon: "leaf", desc: "Breezy & Golden foliage", advice: "Best season for walks. Pack sweaters, leather jacket, and boots." }, + Winter: { temp: "2°C", icon: "snowflake", desc: "Very Cold & Snowy", advice: "Temperatures drop below freezing. Bring puffer coat, gloves, and earmuffs." } + }, + attractions: [ + { name: "Statue of Liberty & Ellis Island", desc: "The colossal neoclassical sculpture welcoming immigrants.", cost: 24, tip: "Take the earliest ferry to avoid long airport-style security queues." }, + { name: "Empire State Building", desc: "The legendary Art Deco skyscraper offering observation deck views.", cost: 44, tip: "Visit after 10 PM to see the city lights sparkle without lines." }, + { name: "Metropolitan Museum of Art", desc: "One of the world's finest art institutions spanning 5000 years.", cost: 30, tip: "Visit the rooftop garden for fantastic views over Central Park." }, + { name: "Top of the Rock", desc: "Rockefeller Center observation deck with clear views of Empire State.", cost: 40, tip: "Visit at sunset to capture both daytime and nighttime skyline photos." }, + { name: "High Line & Vessel", desc: "A linear public park built on a historic elevated freight rail line.", cost: 0, tip: "Walk south-to-north, starting at Gansevoort Street in Meatpacking." }, + { name: "9/11 Memorial & Museum", desc: "A poignant memorial centered inside the footprints of the Twin Towers.", cost: 28, tip: "Admission to the memorial pools is free; museum requires tickets." } + ], + restaurants: [ + { name: "Katz's Delicatessen", style: "Deli", desc: "Legendary, massive pastrami on rye served cafeteria-style.", avgCost: 30 }, + { name: "Joe's Pizza Greenwich Village", style: "Pizza", desc: "Famous, thin-crust classic New York street slices.", avgCost: 8 }, + { name: "Balthazar", style: "French Brasserie", desc: "High-energy SoHo spot serving oysters, steak frites, and pastries.", avgCost: 65 }, + { name: "Peter Luger Steak House", style: "Steakhouse", desc: "Historic Brooklyn venue serving dry-aged porterhouse steaks (Cash only).", avgCost: 110 } + ], + activities: { + Nature: [ + { title: "Central Park Bike Ride", desc: "Rent a bike and loop the rolling hills, stopping at Bethesda Fountain and Bow Bridge." }, + { title: "Walk the Brooklyn Bridge", desc: "Cross the wooden promenade from Manhattan to Brooklyn Heights for skyline views." }, + { title: "Kayaking on the Hudson River", desc: "Paddle for free at Pier 26, enjoying views of One World Trade." } + ], + Adventure: [ + { title: "Helicopter Skyline Flight", desc: "Fly high over the Statue of Liberty and Manhattan skyscrapers for epic aerial views." }, + { title: "Edge Observation Deck climb", desc: "Leap out onto the highest outdoor sky deck in the Western Hemisphere." }, + { title: "Roosevelt Island Tramway Ride", desc: "Ride the red aerial cable car floating parallel to the Queensboro Bridge." } + ], + Food: [ + { title: "Chelsea Market Tasting Crawl", desc: "Sample artisanal tacos, lobster rolls, and fresh gourmet donuts under one roof." }, + { title: "Chinatown & Little Italy Tour", desc: "Graze on steamed soup dumplings, fresh cannoli, and local bubble tea." }, + { title: "Smorgasburg Brooklyn Food Fest", desc: "Try crazy fusion street food from 100 local vendors (Summer weekends)." } + ], + History: [ + { title: "Tenement Museum Tour", desc: "Walk inside preserved historic apartments showing the lives of working-class immigrants." }, + { title: "Grand Central Terminal Tour", desc: "Learn secrets of the Whispering Gallery and the gold-painted celestial ceiling." }, + { title: "Federal Hall & Wall Street Walk", desc: "Stand where George Washington took the oath of office as president." } + ], + Museums: [ + { title: "Museum of Modern Art (MoMA)", desc: "See iconic works like Vincent van Gogh's 'The Starry Night' and Andy Warhol's soup cans." }, + { title: "American Museum of Natural History", desc: "Explore the giant dinosaur halls, ocean life exhibits, and Hayden Planetarium." }, + { title: "Guggenheim Museum Spiral Walk", desc: "Walk up the spiral gallery ramp inside Frank Lloyd Wright's masterpiece building." } + ], + Shopping: [ + { title: "Fifth Avenue Shopping Walk", desc: "Browse high-end fashion boutiques, Apple glass cube, and historic department stores." }, + { title: "SoHo Cobblestone Boutiques", desc: "Shop for trendy streetwear, designer labels, and indie cosmetics." }, + { title: "Brooklyn Flea Market Hunt", desc: "Shop for vintage records, retro maps, and handmade jewelry under DUMBO archway." } + ], + Nightlife: [ + { title: "Broadway Musical Show", desc: "See a world-class theatrical performance in the heart of Times Square." }, + { title: "Greenwich Village Jazz Tour", desc: "Listen to legendary saxophonists at Village Vanguard or Blue Note." }, + { title: "Rooftop Bar Hopping in Williamsburg", desc: "Sip craft cocktails overlooking the lit-up Manhattan skyline." } + ], + Photography: [ + { title: "DUMBO Washington Street Spot", desc: "Photograph the Manhattan Bridge framed perfectly between red-brick warehouses." }, + { title: "Times Square Night Lights capture", desc: "Capture the dizzying billboard glow using long-exposure camera settings." }, + { title: "The Flatiron Building Perspective", desc: "Photograph the triangular historical skyscraper framed by yellow taxis." } + ], + General: [ + { title: "Staten Island Ferry Cruise", desc: "Take the free commuter ferry sailing right past the Statue of Liberty." }, + { title: "High Line Park Walk", desc: "Stroll along the elevated gardens, enjoying street art and urban views." }, + { title: "Bryant Park Relaxing", desc: "Grab a green bistro chair, read a book, and watch locals play chess." } + ] + } + }, + London: { + city: "London", + country: "UK", + theme: "Royal, Historic, & Eclectic", + multipliers: { accommodation: 1.5, food: 1.3, transport: 1.4, activities: 1.2, misc: 1.2 }, + weather: { + Spring: { temp: "12°C", icon: "cloud-drizzle", desc: "Mild & Rainy", advice: "Carry an umbrella and wear waterproof boots. Gardens are lush." }, + Summer: { temp: "22°C", icon: "sun-dim", desc: "Warm & Pleasant", advice: "Enjoy pub gardens and picnics. Bring light wear and sunglasses." }, + Autumn: { temp: "13°C", icon: "cloud", desc: "Cool & Foggy", advice: "Wrap up in cozy layers. Perfect season for indoor museum crawls." }, + Winter: { temp: "5°C", icon: "cloud-rain", desc: "Cold & Wet", advice: "Gloomy days but beautiful festive lights. Warm coat and gloves are vital." } + }, + attractions: [ + { name: "Tower of London", desc: "The historic fortress housing the dazzling Crown Jewels.", cost: 35, tip: "Join the free tour led by the Yeoman Warders (Beefeaters)." }, + { name: "British Museum", desc: "A massive museum dedicated to history, art, and culture.", cost: 0, tip: "Entry is free, but booking a timed entry ticket online is highly recommended." }, + { name: "London Eye", desc: "The giant observation wheel rotating on the South Bank.", cost: 38, tip: "Book fast-track tickets online to bypass the massive queues." }, + { name: "Westminster Abbey", desc: "The historic royal church where coronations and weddings occur.", cost: 30, tip: "Attend Evensong service for free entry and beautiful choral music." }, + { name: "Buckingham Palace", desc: "The official administrative headquarters of the Monarch.", cost: 0, tip: "Check online for Changing of the Guard schedules before going." }, + { name: "Sky Garden", desc: "A landscaped botanical garden offering 360-degree city views.", cost: 0, tip: "Tickets are free but released every Monday morning. Book immediately." } + ], + restaurants: [ + { name: "Dishoom Covent Garden", style: "Indian Cafe", desc: "Highly popular Bombay street-style food and house black daal.", avgCost: 28 }, + { name: "Rules Restaurant", style: "Historic British", desc: "London's oldest restaurant serving classic game, pies, and puddings.", avgCost: 70 }, + { name: "Duck & Waffle", style: "Modern British", desc: "Dine on sweet-savory duck leg on a waffle, 40 floors up (Open 24/7).", avgCost: 55 }, + { name: "Poppies Fish & Chips", style: "Traditional Pub Fare", desc: "Classic retro chippy serving newspaper-wrapped cod and mushy peas.", avgCost: 18 } + ], + activities: { + Nature: [ + { title: "Hyde Park Row Boating", desc: "Rent a pedalo boat on the Serpentine Lake, feeding swans and ducks." }, + { title: "Kew Gardens Glasshouses", desc: "Explore the world's largest collection of living plants under historic Victorian iron domes." }, + { title: "Richmond Park Deer Spotting", desc: "Walk the massive oak forests looking for wild herds of red and fallow deer." } + ], + Adventure: [ + { title: "Up at The O2 Climb", desc: "Put on a climbing suit and walk over the roof of the giant dome structure." }, + { title: "Speedboat on the Thames", desc: "Ride a high-speed RIB boat zooming under Tower Bridge with music." }, + { title: "Slide at ArcelorMittal Orbit", desc: "Ride the world's tallest, longest tunnel slide in Queen Elizabeth Olympic Park." } + ], + Food: [ + { title: "Borough Market Stroll", desc: "Sample artisanal truffles, hot salt beef bagels, and giant cheese toasties." }, + { title: "Traditional Afternoon High Tea", desc: "Enjoy finger sandwiches, warm scones, clotted cream, and tea in a grand tea room." }, + { title: "Brick Lane Curry Feast", desc: "Taste authentic Bangladeshi curries in London's street art central." } + ], + History: [ + { title: "Jack the Ripper Night Walk", desc: "Explore the dark streets of Whitechapel tracking Victorian-era crimes." }, + { title: "Churchill War Rooms Tour", desc: "Walk through the underground bunker where WWII strategic decisions were made." }, + { title: "Globe Theatre Tour", desc: "Explore the reconstructed open-air Elizabethan theatre where Shakespeare's plays debuted." } + ], + Museums: [ + { title: "Natural History Museum", desc: "See the massive blue whale skeleton suspended under cathedral-like arches." }, + { title: "Victoria and Albert Museum (V&A)", desc: "Wander through the world's premier museum of art, design, and fashion." }, + { title: "Tate Modern Galleries", desc: "Explore contemporary international art housed in a colossal former power station." } + ], + Shopping: [ + { title: "Camden Market Punk Exploration", desc: "Shop for vintage leather jackets, gothic clothes, and retro vinyl records." }, + { title: "Harrods Department Store Wander", desc: "Explore the famous Food Halls, Toy Kingdom, and Egyptian Escalator." }, + { title: "Portobello Road Antiques Hunt", desc: "Browse a mile-long street market lined with colorful houses in Notting Hill." } + ], + Nightlife: [ + { title: "West End Theatre Show", desc: "See long-running hit musicals like Les Misérables or Phantom of the Opera." }, + { title: "Soho Pub Crawl", desc: "Sip pints at historic pubs once frequented by musicians, artists, and royalty." }, + { title: "Shoreditch Craft Cocktail hunting", desc: "Visit innovative cocktail bars hidden behind vintage shop fronts." } + ], + Photography: [ + { title: "Westminster Bridge Big Ben Capture", desc: "Photograph the iconic clock tower framed by red double-decker buses." }, + { title: "Notting Hill Colorful Houses", desc: "Take photos of pastel-colored terraced houses on Lancaster Road." }, + { title: "Leadenhall Market Photo Session", desc: "Capture the Victorian glass roof and gold-green architecture (Diagon Alley filming location)." } + ], + General: [ + { title: "Changing of the Guard Ceremony", desc: "Watch the Queen's guard march in red tunics and bearskin hats." }, + { title: "South Bank Walk", desc: "Walk past book stalls, street performers, food markets, and the National Theatre." }, + { title: "Double-Decker Route 15 Ride", desc: "Hop on a historic Routemaster bus driving past St Paul's Cathedral to Tower Hill." } + ] + } + }, + Dubai: { + city: "Dubai", + country: "UAE", + theme: "Luxurious, Modern, & Futuristic", + multipliers: { accommodation: 1.6, food: 1.3, transport: 1.2, activities: 1.5, misc: 1.3 }, + weather: { + Spring: { temp: "26°C", icon: "sun", desc: "Warm & Sunny", advice: "Great beach weather. Carry swimwear and sun lotion." }, + Summer: { temp: "41°C", icon: "thermometer", desc: "Extremely Hot", advice: "Avoid outdoor daytime activities. Malls and indoor parks are heavily air-conditioned." }, + Autumn: { temp: "29°C", icon: "sun-dim", desc: "Warm & Clear", advice: "Pleasant evening temperatures. Perfect for desert camps and marina walks." }, + Winter: { temp: "20°C", icon: "cloud-sun", desc: "Perfect / Mild", advice: "Best outdoor season. Cool breezes at night; carry a light jacket." } + }, + attractions: [ + { name: "Burj Khalifa", desc: "The world's tallest building, piercing the sky.", cost: 48, tip: "Book 'At the Top' tickets for 4:30 PM to catch both daylight and sunset views." }, + { name: "Dubai Mall & Fountain Show", desc: "A massive shopping center featuring a giant indoor aquarium.", cost: 0, tip: "Watch the water fountain show from the balcony of Apple Store." }, + { name: "Palm Jumeirah & Atlantis", desc: "A man-made archipelago shaped like a palm tree.", cost: 0, tip: "Ride the Palm Monorail to get the best views of the crescent villas." }, + { name: "Museum of the Future", desc: "An architectural marvel displaying future-focused technologies.", cost: 40, tip: "Tickets sell out weeks in advance; book immediately upon travel planning." }, + { name: "Miracle Garden", desc: "The world's largest natural flower garden with 150 million blooms.", cost: 25, tip: "Open only from November to April. Visit early in the day." }, + { name: "Desert Safari Camp", desc: "An excursion into golden dunes with camel rides and dinner.", cost: 60, tip: "Opt for a premium camp that includes dune bashing and a BBQ dinner." } + ], + restaurants: [ + { name: "Al Ustad Special Kebab", style: "Traditional Persian", desc: "Historic legendary restaurant famous for yogurt-marinated kebabs.", avgCost: 15 }, + { name: "Zuma Dubai", style: "Fine Dining Izakaya", desc: "Award-winning premium Japanese food in DIFC.", avgCost: 120 }, + { name: "Pierchic", style: "Seafood Fine Dining", desc: "Overwater wooden pier dining overlooking the Burj Al Arab.", avgCost: 180 }, + { name: "Arabian Tea House", style: "Traditional Emirati", desc: "Charming courtyard serving authentic Emirati breakfast platters.", avgCost: 22 } + ], + activities: { + Nature: [ + { title: "Dubai Miracle Garden stroll", desc: "Walk past floral castles, giant clocks, and life-size floral Emirates airplanes." }, + { title: "Ras Al Khor Wildlife Sanctuary", desc: "Watch thousands of pink flamingos wading in wetlands with the city skyline behind." }, + { title: "Hatta Dam Kayaking", desc: "Kayak on turquoise waters nestled within rugged Hajar Mountain valleys." } + ], + Adventure: [ + { title: "Desert Dune Bashing", desc: "Ride in a 4x4 vehicle slipping and sliding down steep sand dunes." }, + { title: "Skydive over Palm Jumeirah", desc: "Freefall from 13,000 feet over the famous palm-shaped island." }, + { title: "Deep Dive Dubai", desc: "Explore a sunken post-apocalyptic city inside the world's deepest pool (60 meters)." } + ], + Food: [ + { title: "Spice & Gold Souk Graze", desc: "Sample saffron tea, local dates, camel milk ice cream, and traditional spices." }, + { title: "Dubai Marina Dinner Cruise", desc: "Sip mocktails on a traditional wooden dhow while sailing past illuminated skyscrapers." }, + { title: "Global Village Street Food Hunt", desc: "Try Turkish baked potatoes, Emirati lugaimat dumplings, and Bosnian kebabs." } + ], + History: [ + { title: "Al Fahidi Historical Neighborhood Walk", desc: "Explore gypsum and coral houses with traditional wind towers dating to the 1890s." }, + { title: "Abra Boat Ride across Dubai Creek", desc: "Cross the historic creek on a traditional wooden water taxi for just 1 AED." }, + { title: "Dubai Museum Heritage Tour", desc: "See traditional pearl diving gears and interactive history in Al Fahidi Fort." } + ], + Museums: [ + { title: "Etihad Museum Exploration", desc: "Learn about the unification of the seven Emirates inside a sleek contemporary pavilion." }, + { title: "Louvre Abu Dhabi Day Excursion", desc: "Take a day taxi to Abu Dhabi to see the spectacular floating domed art museum." }, + { title: "Al Shindagha Perfume House", desc: "Discover the heritage of Arabian perfume making, smelling pure oud and frankincense." } + ], + Shopping: [ + { title: "Gold Souk Bargaining Quest", desc: "Walk through rows of shops displaying heavy gold necklaces, diamonds, and silver." }, + { title: "Dubai Mall Premium Walk", desc: "Browse high-end fashion avenues and watch the giant indoor waterfall." }, + { title: "Souk Madinat Jumeirah Wander", desc: "Shop for lamps, carpets, and spices in a modern bazaar built along internal waterways." } + ], + Nightlife: [ + { title: "Dubai Marina Yacht Party", desc: "Cruise alongside superyachts enjoying music, food, and views." }, + { title: "Sip Cocktails at Gevora Rooftop", desc: "Have drinks on top of the world's tallest hotel, overlooking Sheikh Zayed Road." }, + { title: "La Perle Dragone Show", desc: "Watch a high-tech aqua theatrical show featuring divers, acrobats, and motorcycles." } + ], + Photography: [ + { title: "Dubai Frame Observation Deck", desc: "Stand on the glass bridge, capturing 'Old Dubai' on one side and 'New Dubai' on the other." }, + { title: "Wings of Mexico Capture", desc: "Photograph yourself framed inside golden wings with the Burj Khalifa right behind." }, + { title: "The Pointe Palm Fountain Shoot", desc: "Photograph the massive fountain shoot with Atlantis Hotel in the background." } + ], + General: [ + { title: "Kite Beach Sunset stroll", desc: "Walk along the sandy beach with views of the sail-shaped Burj Al Arab." }, + { title: "Ski Dubai Indoor Slopes", desc: "Escape hot weather by skiing, snowboarding, or meeting penguins in sub-zero snow." }, + { title: "Sunset Camel Ride in Dunes", desc: "Ride a camel across quiet desert sands, taking photos of the golden dunes." } + ] + } + }, + Singapore: { + city: "Singapore", + country: "Singapore", + theme: "Green, Futuristic, & Multicultural", + multipliers: { accommodation: 1.4, food: 1.0, transport: 1.0, activities: 1.3, misc: 1.1 }, + weather: { + Spring: { temp: "28°C", icon: "cloud-sun-rain", desc: "Tropical & Humid", advice: "Afternoon thunder showers are common. Carry an umbrella." }, + Summer: { temp: "29°C", icon: "sun-dim", desc: "Hot & Breezy", advice: "Wear light linen shirts. Drink plenty of fresh coconut water." }, + Autumn: { temp: "28°C", icon: "cloud-rain", desc: "Humid & Showery", advice: "Enjoy indoor gardens, shopping malls, and underpass networks." }, + Winter: { temp: "27°C", icon: "cloud-drizzle", desc: "Monsoon Season / Wet", advice: "Northeast monsoon brings heavy downpours. Museums are ideal." } + }, + attractions: [ + { name: "Gardens by the Bay", desc: "Futuristic park featuring giant Supertree structures and glass conservatories.", cost: 24, tip: "Watch the free Garden Rhapsody light show at 7:45 PM and 8:45 PM." }, + { name: "Marina Bay Sands SkyPark", desc: "The iconic boat-shaped hotel rooftop observatory deck.", cost: 20, tip: "Get drinks at CÉ LA VI rooftop bar instead of buying observation deck tickets." }, + { name: "Sentosa Island & Universal Studios", desc: "A massive resort island featuring beaches and rollercoasters.", cost: 60, tip: "Take the Sentosa Express monorail or walk the boardwalk for free entry." }, + { name: "Singapore Zoo & Night Safari", desc: "A world-renowned open-concept zoo active at night.", cost: 35, tip: "Book the Night Safari tram ride in advance to secure seats." }, + { name: "Chinatown Heritage Centre", desc: "Shophouses showing the lives of Singapore's early Chinese migrants.", cost: 12, tip: "Dine at Chinatown Complex Food Centre after touring the temple." }, + { name: "Jewel Changi Airport", desc: "The spectacular dome housing the world's tallest indoor waterfall.", cost: 0, tip: "See it when you land or before you leave; light show runs hourly." } + ], + restaurants: [ + { name: "Liao Fan Hawker Chan", style: "Soya Sauce Chicken", desc: "World's cheapest Michelin-starred meal (original stall).", avgCost: 6 }, + { name: "Lau Pa Sat Hawker Centre", style: "Satay Street", desc: "Historic food pavilion closing down the street at night for satay grills.", avgCost: 15 }, + { name: "Jumbo Seafood East Coast", style: "Seafood Diner", desc: "Famous for sweet-savory Singaporean Chilli Crab with fried buns.", avgCost: 75 }, + { name: "Din Tai Fung Marina Bay", style: "Taiwanese Dumplings", desc: "Perfectly folded, delicate Xiao Long Bao soup dumplings.", avgCost: 24 } + ], + activities: { + Nature: [ + { title: "Cloud Forest Waterfall Walk", desc: "Explore the misty greenhouse with a 35-meter indoor waterfall and exotic plants." }, + { title: "MacRitchie Reservoir Treetop Walk", desc: "Hike through tropical rainforest, crossing a 250-meter suspension bridge above the canopy." }, + { title: "Singapore Botanic Gardens", desc: "Stroll the lush UNESCO World Heritage site, visiting the national orchid collection." } + ], + Adventure: [ + { title: "AJ Hackett Sentosa Bungy", desc: "Leap from a 50-meter-high beach tower over Siloso Beach sands." }, + { title: "Skyline Luge Sentosa", desc: "Zoom down winding tracks on a gravity-fueled kart ride." }, + { title: "Southern Ridges Walk", desc: "Hike the 10km trail crossing Henderson Waves bridge, the highest pedestrian bridge in the city." } + ], + Food: [ + { title: "Hawker Centre Safari", desc: "Taste Michelin-approved Hainanese chicken rice, char kway teow noodles, and laksa soup." }, + { title: "Sling Cocktail at Raffles Hotel", desc: "Sip the original Singapore Sling inside the historic colonial Long Bar, tossing peanut shells on the floor." }, + { title: "Little India Spice Tour", desc: "Sample crispy paper thosai, samosas, and pulled pulled tea (Teh Tarik)." } + ], + History: [ + { title: "Fort Canning Battlebox Tour", desc: "Go underground into the WWII British command center bunker where Singapore surrendered." }, + { title: "Chinatown Shophouse Heritage Walk", desc: "See beautifully restored shophouses and visit the Buddha Tooth Relic Temple." }, + { title: "Katong Peranakan House Stroll", desc: "Learn about Straits Chinese culture while viewing colorful, pastel-painted houses." } + ], + Museums: [ + { title: "ArtScience Museum Exploration", desc: "Walk through interactive digital installations inside a building shaped like a lotus flower." }, + { title: "National Gallery Singapore", desc: "Browse Southeast Asian modern art housed in the grand former Supreme Court building." }, + { title: "National Museum of Singapore", desc: "Interact with multimedia exhibits outlining Singapore's transformation story." } + ], + Shopping: [ + { title: "Orchard Road Malls Crawl", desc: "Explore miles of connected shopping malls packed with luxury brands and underground food halls." }, + { title: "Kampong Glam Boutique Hunting", desc: "Browse hipster shops, custom perfumes, and indie cafes on Haji Lane." }, + { title: "Mustafa Centre 24/7 Quest", desc: "Explore a massive discount department store selling everything from electronics to spices." } + ], + Nightlife: [ + { title: "Clarke Quay Clubbing", desc: "Dance at high-energy bars, clubs, and floating restaurants along the Singapore River." }, + { title: "Night Safari tram ride", desc: "View nocturnal animals active in naturalistic enclosures under the moonlight." }, + { title: "Speakeasy Bar Hopping", desc: "Locate hidden doors behind telephone booths or vintage toy shops in Chinatown." } + ], + Photography: [ + { title: "Supertree Grove Light Show capture", desc: "Capture long-exposure photos of illuminated supertrees glowing against the night sky." }, + { title: "Haji Lane Street Art Shoot", desc: "Photograph colorful murals, eccentric store fronts, and narrow alleys." }, + { title: "Merlion Park Marina Bay capture", desc: "Take a classic photo of the water-spouting Merlion statue with Marina Bay Sands in the frame." } + ], + General: [ + { title: "Singapore River Cruise", desc: "Ride a traditional wooden bumboat past historic quays, bridges, and skyscrapers." }, + { title: "Spectra Light & Water Show", desc: "Watch the free laser, light, and fountain show on the waters of Marina Bay." }, + { title: "Jewel Vortex Light Show", desc: "Watch colors dance down the rain vortex indoor waterfall." } + ] + } + }, + Bali: { + city: "Bali", + country: "Indonesia", + theme: "Tropical, Spiritual, & Relaxing", + multipliers: { accommodation: 0.6, food: 0.6, transport: 0.7, activities: 0.8, misc: 0.7 }, + weather: { + Spring: { temp: "27°C", icon: "cloud-sun", desc: "Dry & Warm", advice: "Ideal season. Bright sunny days with low humidity." }, + Summer: { temp: "26°C", icon: "sun", desc: "Sunny & Breezy", advice: "Great surfing conditions. High season, book transport ahead." }, + Autumn: { temp: "27°C", icon: "cloud-drizzle", desc: "Transition Season", advice: "Occasional showers. Warm and quiet tourist spots." }, + Winter: { temp: "28°C", icon: "cloud-rain", desc: "Wet Monsoon", advice: "Frequent tropical downpours. High humidity. Prepare rain gear." } + }, + attractions: [ + { name: "Ubud Monkey Forest", desc: "A sanctuary for grey long-tailed macaques among ancient banyan trees.", cost: 6, tip: "Keep your sunglasses and phone zipped up inside a bag." }, + { name: "Tanah Lot Temple", desc: "An ancient Hindu pilgrimage temple perched on a wave-swept rock offshore.", cost: 5, tip: "Go early to walk the reef, or stay for sunset when it is dramatically backlit." }, + { name: "Tegallalang Rice Terraces", desc: "Spectacular stepped green rice paddies using subak irrigation.", cost: 3, tip: "Wear stable shoes; walking the narrow clay paths can be slippery." }, + { name: "Mount Batur Volcano Trek", desc: "An active volcano hiked for sunrise views.", cost: 35, tip: "Hire a local guide; depart Ubud by 2:30 AM to reach the summit for sunrise." }, + { name: "Uluwatu Temple & Kecak Dance", desc: "A sea temple perched on a cliff edge presenting traditional dance.", cost: 10, tip: "Kecak fire dance starts around 6:00 PM; buy tickets at 5:00 PM." }, + { name: "Nusa Penida Day Trip", desc: "An island excursion to see Kelingking 'T-Rex' beach and cliffs.", cost: 45, tip: "Take a fast boat from Sanur. Roads on Nusa Penida are notoriously bumpy." } + ], + restaurants: [ + { name: "Warung Naughty Nuri's Ubud", style: "Indonesian BBQ", desc: "Famous for sticky glazed pork ribs and giant shaken martinis.", avgCost: 20 }, + { name: "Locavore Bali", style: "Fine Dining", desc: "Innovative tasting menu utilizing 100% local Indonesian ingredients.", avgCost: 95 }, + { name: "Potato Head Beach Club", style: "Beach Club Bistro", desc: "Oceanfront daybeds serving tropical cocktails and wood-fired pizzas.", avgCost: 35 }, + { name: "Warung Makan Bu Oki", style: "Local Warung", desc: "Authentic, super cheap Balinese Nasi Campur spicy rice platter.", avgCost: 4 } + ], + activities: { + Nature: [ + { title: "Tegenungan Waterfall Walk", desc: "Hike down jungle steps to swim in the pool beneath a roaring waterfall." }, + { title: "Campuhan Ridge Walk", desc: "Stroll a grassy path winding between river valleys in Ubud during sunrise." }, + { title: "West Bali National Park boat ride", desc: "Snorkel in pristine marine waters looking for green sea turtles and coral reef cliffs." } + ], + Adventure: [ + { title: "Ayung River White Water Rafting", desc: "Paddle through rapids, class II-III, passing stone carvings and jungle waterfalls." }, + { title: "ATV Quad Biking through caves", desc: "Drive a quad bike through muddy trails, rice fields, and dark gorilla caves." }, + { title: "Surfing at Canggu Beach", desc: "Rent a surfboard and catch waves at Batu Bolong beach with a local instructor." } + ], + Food: [ + { title: "Balinese Cooking Masterclass", desc: "Visit a local market, pick fresh galangal and spices, then prepare satay lilit in a village kitchen." }, + { title: "Coffee Plantation Tasting", desc: "Sample traditional ginger tea, lemongrass tea, and learn how Luwak coffee is roasted." }, + { title: "Jimbaran Bay Seafood BBQ", desc: "Dine on grilled snapper, clams, and prawns brushed in sambal on daybeds on the sand." } + ], + History: [ + { title: "Tirta Empul Holy Water Bathing", desc: "Participate in a traditional purification ritual, walking under stone spring spouts." }, + { title: "Goa Gajah (Elephant Cave) Tour", desc: "Explore a 9th-century cave entrance carved with menacing faces and ancient relics." }, + { title: "Klungkung Palace Heritage Walk", desc: "See traditional Kamasan paintings depicting historical court trials on royal pavilion ceilings." } + ], + Museums: [ + { title: "Blanco Renaissance Museum", desc: "Explore the eccentric hilltop mansion and artwork of the 'Salvador Dali of Bali'." }, + { title: "Museum Neka Art Gallery", desc: "Trace Balinese painting styles from traditional wayang puppets to modern art." }, + { title: "Museum Puri Lukisan", desc: "Browse beautiful wood carvings and historic paintings in the heart of Ubud gardens." } + ], + Shopping: [ + { title: "Ubud Art Market Hunt", desc: "Bargain for round rattan bags, linen shirts, batik sarongs, and wooden carvings." }, + { title: "Seminyak Designer Boutiques", desc: "Shop for upscale resort wear, designer swimwear, and organic skin care." }, + { title: "Sukawati Art Market Quest", desc: "Visit a local wholesale market for cheap souvenirs and handmade goods." } + ], + Nightlife: [ + { title: "La Brisa Canggu Sunset Drinks", desc: "Sip cocktails in a rustic beach club built from reclaimed fishing boats." }, + { title: "Single Fin Uluwatu Party", desc: "Listen to live acoustic sets and DJs overlooking the famous surf break." }, + { title: "Clubbing at ShiShi Seminyak", desc: "Dance on three floors presenting techno, hip hop, and house music with neon lights." } + ], + Photography: [ + { title: "Bali Swing over Tegallalang", desc: "Get a photo swinging out over the palm valleys wearing a long dress." }, + { title: "Lempuyang Temple Gates of Heaven", desc: "Photograph yourself framed by the grand stone gates with Mount Agung behind." }, + { title: "Handara Gate Capture", desc: "Photograph the iconic giant Balinese stone gate surrounded by mist and golf green lawns." } + ], + General: [ + { title: "Yoga Class in Yoga Barn", desc: "Take a calming vinyasa or sound healing class in a massive bamboo open-air studio." }, + { title: "Sanur Beach Bicycle Ride", desc: "Ride along the paved beach path, passing local fishing boats and cafes." }, + { title: "Massage in Canggu Spa", desc: "Enjoy a traditional full-body Balinese oil massage for a cheap price." } + ] + } + }, + Goa: { + city: "Goa", + country: "India", + theme: "Portuguese Heritage, Beaches, & Nightlife", + multipliers: { accommodation: 0.5, food: 0.5, transport: 0.6, activities: 0.7, misc: 0.6 }, + weather: { + Spring: { temp: "29°C", icon: "sun", desc: "Hot & Sunny", advice: "Wear hats and loose cotton clothing. Perfect for evening dips." }, + Summer: { temp: "32°C", icon: "sun-dim", desc: "Humid & Sultry", advice: "Avoid mid-day sun. Look for shade and drink refreshing nimbu pani." }, + Autumn: { temp: "27°C", icon: "cloud-sun", desc: "Warm & Lush green", advice: "Monsoon is ending; waterfalls are full and nature is spectacular." }, + Winter: { temp: "24°C", icon: "wind", desc: "Pleasant & Windy", advice: "Best season. Cool sea breezes. Carry light layers for chilly nights." } + }, + attractions: [ + { name: "Basilica of Bom Jesus", desc: "A UNESCO site housing the mortal remains of St. Francis Xavier.", cost: 0, tip: "Ensure shoulders and knees are covered when entering the church." }, + { name: "Fort Aguada & Lighthouse", desc: "A 17th-century Portuguese fortress overlooking the Arabian Sea.", cost: 3, tip: "Visit early morning to escape hot sun and crowds on the fort walls." }, + { name: "Dudhsagar Falls", desc: "A four-tiered waterfall looking like a sea of milk, accessible by jeep.", cost: 15, tip: "Jeep tours run from November to May. Wear swimwear underneath clothes." }, + { name: "Anjuna Flea Market", desc: "A massive weekly market packed with spices, jewelry, and clothes.", cost: 0, tip: "Runs only on Wednesdays. Start your bargaining at 50% of the asking price." }, + { name: "Fontainhas Latin Quarter", desc: "A colorful neighborhood with narrow streets and Portuguese villas.", cost: 0, tip: "Dine at a local cafe and take photographs of the bright yellow and blue walls." }, + { name: "Dona Paula Viewpoint", desc: "A scenic rocky headland offering views of Mormugao Harbor.", cost: 0, tip: "Go at sunset to watch local fishing boats return to the estuary." } + ], + restaurants: [ + { name: "Fisherman's Wharf Mobor", style: "Seafood Resto", desc: "Riverside dining serving spicy Goan fish curry rice and crab masala.", avgCost: 22 }, + { name: "Britto's Baga Beach", style: "Beach Shack", desc: "Iconic beach restaurant serving cold beers and vindaloo.", avgCost: 15 }, + { name: "Mum's Kitchen Panaji", style: "Traditional Goan", desc: "Homestyle family recipes preservation of Hindu & Christian cuisines.", avgCost: 25 }, + { name: "Gunpowder Assagao", style: "South Indian Fusion", desc: "Charming cottage garden serving spicy curries and flaky malabar parottas.", avgCost: 18 } + ], + activities: { + Nature: [ + { title: "Spice Plantation Tour", desc: "Walk past pepper vines, nutmeg trees, and watch elephants bathe. Enjoy a spice-infused buffet lunch." }, + { title: "Dolphin Spotting Cruise", desc: "Ride a wooden boat out from Sinquerim Beach to watch wild dolphins jump in the bay." }, + { title: "Sal Backwaters Kayaking", desc: "Paddle through quiet mangrove channels looking for kingfishers and otters." } + ], + Adventure: [ + { title: "Water Sports at Calangute", desc: "Go jet skiing, parasailing, and ride a banana boat towed by speedboats." }, + { title: "Scuba Diving at Grande Island", desc: "Try a beginner dive to see marine coral reefs and shipwrecks in Goa." }, + { title: "Trek to Arambol Sweet Lake", desc: "Hike past banyan trees to find a fresh-water lake nestled right next to the sea." } + ], + Food: [ + { title: "Feni Distillery Tour", desc: "Learn how cashew apple juice is traditionally stomped and distilled to make Goa's signature liquor (Feni)." }, + { title: "Goan Curry Masterclass", desc: "Learn how to grind fresh coconut masala paste for a classic prawns curry." }, + { title: "Baga Shack Dinner Crawl", desc: "Graze on fish tikka, butter garlic prawns, and local poee bread at sunset." } + ], + History: [ + { title: "Old Goa Heritage Walk", desc: "Visit massive churches like Se Cathedral and the ruins of St. Augustine Tower." }, + { title: "Reis Magos Fort Tour", desc: "Explore a restored hilltop fort that once defended the narrowest point of Mandovi River." }, + { title: "Cabo de Rama Fort Hike", desc: "Hike the ruins of a clifftop fort named after Lord Rama, enjoying views of the sea." } + ], + Museums: [ + { title: "Museum of Christian Art", desc: "Browse a unique collection of indo-portuguese sacred art inside Santa Monica convent." }, + { title: "Houses of Goa Museum", desc: "See an architectural building displaying the evolution of Goan residential style." }, + { title: "Big Foot Heritage Museum", desc: "Walk through a recreated historic village showing traditional Goan occupations." } + ], + Shopping: [ + { title: "Mapusa Friday Market Quest", desc: "Browse a local market packed with dried fish, home-made sausages, and local pottery." }, + { title: "Saturday Night Bazaar Arpora", desc: "Shop for designer clothes, boutique spices, and enjoy live music performances." }, + { title: "Panaji Market Spice Hunt", desc: "Buy premium cashew nuts, local feni bottle, and Goan spices." } + ], + Nightlife: [ + { title: "Beach Party at Curlies", desc: "Dance on the sand of Anjuna Beach under the stars to techno music." }, + { title: "Clubbing at Tito's Lane", desc: "Visit Goa's most famous club strip, dancing to Bollywood and EDM music." }, + { title: "Casino Night on Mandovi", desc: "Board a floating luxury casino vessel for drinks, dinner, and card games." } + ], + Photography: [ + { title: "Fontainhas Pastel Walls", desc: "Photograph yourself in front of yellow, blue, and orange tiled villas." }, + { title: "Arambol Beach Sunset drum circle", desc: "Capture photos of fire spinners and musicians gathering on the sand at sunset." }, + { title: "Fort Tiracol Ocean Capture", desc: "Photograph the white walls of the fort hotel overlooking the calm Terekhol river inlet." } + ], + General: [ + { title: "Sunset Cruise on Mandovi River", desc: "Ride a triple-deck cruise boat enjoying Goan folk dances and music." }, + { title: "Arambol Mud Bathing", desc: "Bathe in mineral-rich yellow mud pools located in Arambol hills." }, + { title: "Morjim Beach Nest Watch", desc: "Spot Olive Ridley sea turtle nesting sites protected by local conservationists." } + ] + } + } +}; + +// Universal/Fallback templates for unsupported destinations +const FALLBACK_DESTINATION = { + theme: "Custom Adventurer's Destination", + multipliers: { accommodation: 1.0, food: 1.0, transport: 1.0, activities: 1.0, misc: 1.0 }, + weather: { + Spring: { temp: "18°C", icon: "cloud-sun", desc: "Mild & Breezy", advice: "Perfect walking conditions. Pack layers." }, + Summer: { temp: "26°C", icon: "sun", desc: "Warm & Sunny", advice: "Bring sunscreen and stay hydrated." }, + Autumn: { temp: "15°C", icon: "leaf", desc: "Cool & Golden", advice: "Expect chilly winds. Wear sweaters." }, + Winter: { temp: "8°C", icon: "snowflake", desc: "Cold & Crisp", advice: "Wrap up in warm thermal layers." } + }, + attractions: [ + { name: "Downtown Historical Square", desc: "The cultural hub featuring local architecture and statues.", cost: 0, tip: "Great starting point for walking tours." }, + { name: "Central Heritage Museum", desc: "Displays art and relics depicting the region's rich timeline.", cost: 15, tip: "Check out the temporary exhibitions." }, + { name: "City Botanical Garden", desc: "A green oasis boasting rare local floral species.", cost: 8, tip: "Ideal spot for a quiet picnic." } + ], + restaurants: [ + { name: "The Local Bistro", style: "Traditional Bistro", desc: "Serves regional favorites made from local market ingredients.", avgCost: 25 }, + { name: "Street Food Plaza", style: "Street Food", desc: "An open market cluster serving local delicacies.", avgCost: 10 }, + { name: "Panorama Fine Dining", style: "Fine Dining", desc: "Rooftop dining featuring gourmet culinary fusions.", avgCost: 80 } + ], + activities: { + Nature: [ + { title: "Scenic Park Trek", desc: "Hike the local trail winding through forests and views." }, + { title: "Riverfront Bike Ride", desc: "Rent a bike and ride along the scenic local riverbanks." } + ], + Adventure: [ + { title: "City Rooftop Tour", desc: "Get an adventurous view of the skyline from climbing points." }, + { title: "Eco Forest Exploration", desc: "Try zip-lining and canopy walking in nearby nature parks." } + ], + Food: [ + { title: "Local Culinary Tour", desc: "Sample traditional delicacies at three different heritage eateries." }, + { title: "Regional Coffee Tasting", desc: "Taste specialty coffees and pastries popular in the region." } + ], + History: [ + { title: "Heritage Building Tour", desc: "Explore local monuments and discover the foundation history." }, + { title: "Historic District Guided Walk", desc: "Hear stories of early founders and landmarks." } + ], + Museums: [ + { title: "Modern Art Centre", desc: "Browse contemporary pieces created by regional and national artists." }, + { title: "Cultural History Archives", desc: "View documents, photos, and relics illustrating the area's growth." } + ], + Shopping: [ + { title: "Central Shopping District Walk", desc: "Browse high street shops, souvenirs, and craft stores." }, + { title: "Weekly Artisans Market", desc: "Buy handcrafted woodworks, textiles, and local preserves." } + ], + Nightlife: [ + { title: "High Street Live Music Cafe", desc: "Listen to regional bands playing acoustic sessions." }, + { title: "Local Lounge Pub Crawl", desc: "Explore the most popular bars and clubs downtown." } + ], + Photography: [ + { title: "Panoramic Viewpoint Shoot", desc: "Capture the golden hour lighting up the town grid." }, + { title: "Murals & Street Art Walk", desc: "Photograph massive wall paintings depicting community stories." } + ], + General: [ + { title: "Discovery Walking Tour", desc: "Get familiar with the town center, parks, and alleys." }, + { title: "Relax at Local Plaza", desc: "Grab a tea, sit by the fountain, and watch the town buzz." } + ] + } +}; + +// ========================================================================== +// 2. STATE MANAGEMENT & DOM REFERENCES +// ========================================================================== + +let appState = { + currentTrip: null, + savedTrips: [], + theme: "dark" +}; + +// DOM Elements +const selectDestination = document.getElementById("select-destination"); +const customDestinationGroup = document.getElementById("custom-destination-group"); +const inputCustomDestination = document.getElementById("input-custom-destination"); +const inputDuration = document.getElementById("input-duration"); +const inputStartDate = document.getElementById("input-start-date"); +const inputBudget = document.getElementById("input-budget"); +const selectStyle = document.getElementById("select-style"); +const selectTransport = document.getElementById("select-transport"); +const selectAccommodation = document.getElementById("select-accommodation"); +const itineraryForm = document.getElementById("itinerary-form"); +const validationAlert = document.getElementById("validation-alert"); +const btnGenerate = document.getElementById("btn-generate"); +const btnSaveTrip = document.getElementById("btn-save-trip"); +const btnClearAll = document.getElementById("btn-clear-all"); +const themeToggle = document.getElementById("theme-toggle"); + +// Results container +const resultsPlaceholder = document.getElementById("results-placeholder"); +const resultsContent = document.getElementById("results-content"); + +// Stats selectors +const statDestName = document.getElementById("stat-dest-name"); +const statTotalDays = document.getElementById("stat-total-days"); +const statTripBudget = document.getElementById("stat-trip-budget"); +const statEstimatedCost = document.getElementById("stat-estimated-cost"); +const statSavingsCost = document.getElementById("stat-savings-cost"); +const statSavingsLabel = document.getElementById("stat-savings-label"); +const statActivitiesCount = document.getElementById("stat-activities-count"); + +// Budget selectors +const budgetRingIndicator = document.getElementById("budget-ring-indicator"); +const budgetPctVal = document.getElementById("budget-pct-val"); +const budgetUtilizationStatus = document.getElementById("budget-utilization-status"); +const breakdownHotelCost = document.getElementById("breakdown-hotel-cost"); +const breakdownFoodCost = document.getElementById("breakdown-food-cost"); +const breakdownTransportCost = document.getElementById("breakdown-transport-cost"); +const breakdownActivitiesCost = document.getElementById("breakdown-activities-cost"); +const breakdownMiscCost = document.getElementById("breakdown-misc-cost"); +const breakdownHotelFill = document.getElementById("breakdown-hotel-fill"); +const breakdownFoodFill = document.getElementById("breakdown-food-fill"); +const breakdownTransportFill = document.getElementById("breakdown-transport-fill"); +const breakdownActivitiesFill = document.getElementById("breakdown-activities-fill"); +const breakdownMiscFill = document.getElementById("breakdown-misc-fill"); + +// Weather selectors +const weatherMainIcon = document.getElementById("weather-main-icon"); +const weatherTempVal = document.getElementById("weather-temp-val"); +const weatherDescription = document.getElementById("weather-description"); +const weatherSeasonVal = document.getElementById("weather-season-val"); +const weatherAdviceVal = document.getElementById("weather-advice-val"); + +// Attractions & Timeline +const attractionsListContainer = document.getElementById("attractions-list-container"); +const itineraryTimelineContainer = document.getElementById("itinerary-timeline-container"); +const btnToggleAllDays = document.getElementById("btn-toggle-all-days"); + +// Checklist & Saved List +const checklistProgressText = document.getElementById("checklist-progress-text"); +const checklistProgressFill = document.getElementById("checklist-progress-fill"); +const checklistContainer = document.getElementById("checklist-container"); +const savedTripsContainer = document.getElementById("saved-trips-container"); + +// Dashboard counter values +const qsTripsSaved = document.getElementById("qs-trips-saved"); + +// ========================================================================== +// 3. INITIALIZATION & THEME HANDLER +// ========================================================================== + +document.addEventListener("DOMContentLoaded", () => { + // Load saved theme + const savedTheme = localStorage.getItem("vagabond_theme") || "dark"; + setTheme(savedTheme); + + // Initialize dates + const today = new Date().toISOString().split("T")[0]; + inputStartDate.setAttribute("min", today); + inputStartDate.value = today; + + // Load Saved Trips + loadSavedTrips(); + + // Attach Listeners + selectDestination.addEventListener("change", handleDestinationChange); + itineraryForm.addEventListener("submit", handleFormSubmit); + btnSaveTrip.addEventListener("click", handleSaveTrip); + btnClearAll.addEventListener("click", handleClearAllTrips); + themeToggle.addEventListener("click", handleThemeToggle); + btnToggleAllDays.addEventListener("click", handleToggleAllDays); + + // Initialize icons + lucide.createIcons(); +}); + +function setTheme(theme) { + document.documentElement.setAttribute("data-theme", theme); + appState.theme = theme; + localStorage.setItem("vagabond_theme", theme); +} + +function handleThemeToggle() { + const newTheme = appState.theme === "dark" ? "light" : "dark"; + setTheme(newTheme); +} + +function handleDestinationChange() { + if (selectDestination.value === "custom") { + customDestinationGroup.style.display = "block"; + inputCustomDestination.setAttribute("required", "required"); + } else { + customDestinationGroup.style.display = "none"; + inputCustomDestination.removeAttribute("required"); + } +} + +// ========================================================================== +// 4. ITINERARY & BUDGET GENERATION LOGIC +// ========================================================================== + +function handleFormSubmit(e) { + e.preventDefault(); + validationAlert.style.display = "none"; + + // Basic Validation + if (!itineraryForm.checkValidity()) { + validationAlert.textContent = "Please fill in all required fields correctly."; + validationAlert.style.display = "block"; + itineraryForm.reportValidity(); + return; + } + + const destinationVal = selectDestination.value; + let destName = destinationVal; + let isCustom = false; + + if (destinationVal === "custom") { + destName = inputCustomDestination.value.trim(); + isCustom = true; + if (!destName) { + validationAlert.textContent = "Please enter custom destination name."; + validationAlert.style.display = "block"; + inputCustomDestination.focus(); + return; + } + } + + const duration = parseInt(inputDuration.value); + const budget = parseInt(inputBudget.value); + const startDate = inputStartDate.value; + const travelStyle = selectStyle.value; + const transport = selectTransport.value; + const accommodation = selectAccommodation.value; + + if (duration <= 0 || duration > 30) { + validationAlert.textContent = "Trip duration must be between 1 and 30 days."; + validationAlert.style.display = "block"; + inputDuration.focus(); + return; + } + + if (budget <= 0) { + validationAlert.textContent = "Overall budget must be a positive number."; + validationAlert.style.display = "block"; + inputBudget.focus(); + return; + } + + // Get interests + const checkedInterests = Array.from( + document.querySelectorAll('input[name="interests"]:checked') + ).map(cb => cb.value); + + // Generate Itinerary State + const trip = generateTripData({ + destName, + isCustom, + destinationVal, + duration, + budget, + startDate, + travelStyle, + transport, + accommodation, + interests: checkedInterests + }); + + appState.currentTrip = trip; + btnSaveTrip.removeAttribute("disabled"); + + // Render Layouts + renderTripDashboard(trip); + + // Scroll to Results + resultsContent.scrollIntoView({ behavior: "smooth" }); +} + +function generateTripData(inputs) { + const { + destName, + isCustom, + destinationVal, + duration, + budget, + startDate, + travelStyle, + transport, + accommodation, + interests + } = inputs; + + const db = isCustom ? FALLBACK_DESTINATION : DESTINATIONS_DATABASE[destinationVal]; + const mult = db.multipliers; + + // 1. Calculate Budget Breakdown + // Base daily costs in USD + let baseAcc = 120; + if (accommodation === "Hostel") baseAcc = 20; + else if (accommodation === "Budget") baseAcc = 50; + else if (accommodation === "Luxury") baseAcc = 350; + + let baseFood = 40; + if (accommodation === "Hostel") baseFood = 15; + else if (accommodation === "Budget") baseFood = 25; + else if (accommodation === "Luxury") baseFood = 120; + + let baseTransport = 10; + if (transport === "Flight") baseTransport = 50; // flat-rate daily average + else if (transport === "Train") baseTransport = 20; + else if (transport === "Car Rental") baseTransport = 45; + + let baseActivities = 30; + if (interests.length > 5) baseActivities = 50; + else if (interests.length < 2) baseActivities = 15; + if (accommodation === "Luxury") baseActivities += 30; + + // Multiplied Daily Estimates + const dailyAcc = baseAcc * mult.accommodation; + const dailyFood = baseFood * mult.food; + const dailyTransport = baseTransport * mult.transport; + const dailyActivities = baseActivities * mult.activities; + const dailyMisc = (dailyAcc + dailyFood + dailyTransport + dailyActivities) * 0.1 * mult.misc; + + // Total estimates + const estAcc = Math.round(dailyAcc * duration); + const estFood = Math.round(dailyFood * duration); + const estTransport = Math.round(dailyTransport * duration); + const estActivities = Math.round(dailyActivities * duration); + const estMisc = Math.round(dailyMisc * duration); + const estTotal = estAcc + estFood + estTransport + estActivities + estMisc; + + // 2. Determine Season based on Start Month + const startMonth = new Date(startDate).getMonth(); // 0-indexed + let season = "Spring"; + if (startMonth >= 5 && startMonth <= 7) season = "Summer"; + else if (startMonth >= 8 && startMonth <= 10) season = "Autumn"; + else if (startMonth === 11 || startMonth <= 1) season = "Winter"; + + // Fetch weather data + const weatherInfo = db.weather[season]; + + // 3. Generate Day-by-Day itinerary + const itinerary = []; + let actCount = 0; + + // Extract lists of activities by interest + let pooledActivities = []; + if (interests.length > 0) { + interests.forEach(interest => { + if (db.activities[interest]) { + pooledActivities.push(...db.activities[interest]); + } + }); + } + // Fill in with General activities if pool is sparse + if (pooledActivities.length < duration * 3) { + pooledActivities.push(...(db.activities.General || db.activities.Nature)); + } + + // Shuffle or cycle through pooled activities safely + for (let day = 1; day <= duration; day++) { + const morningIdx = (day * 3 - 3) % pooledActivities.length; + const afternoonIdx = (day * 3 - 2) % pooledActivities.length; + const eveningIdx = (day * 3 - 1) % pooledActivities.length; + + const morning = pooledActivities[morningIdx] || { title: "Explore Local Streets", desc: "Take a leisurely walk around and discover local cafes." }; + const afternoon = pooledActivities[afternoonIdx] || { title: "Visit Local Landmark", desc: "Sightsee the city's key points of interest." }; + const evening = pooledActivities[eveningIdx] || { title: "Relaxing Dinner & Walk", desc: "Enjoy regional delicacies and dynamic night atmosphere." }; + + actCount += 3; + + // Daily food spots + const restIdx = (day - 1) % db.restaurants.length; + const rest = db.restaurants[restIdx]; + + // Estimated daily cost breakdown + const dayCost = Math.round(dailyAcc + dailyFood + dailyTransport + dailyActivities + dailyMisc); + + // Dynamic travel tip + let dayTip = "Remember to stay hydrated and keep local currency handily available."; + if (travelStyle === "Solo") { + const soloTips = [ + "Great day to join a walking tour group and meet fellow travelers.", + "Keep your maps downloaded offline. Share your live location with family.", + "Avoid quiet alleys at night. Sit near cafe windows to soak local vibes." + ]; + dayTip = soloTips[day % soloTips.length]; + } else if (travelStyle === "Family") { + const familyTips = [ + "Ensure child strollers are easily foldable for transport transits.", + "Pack dry snacks. Plan bathroom stops near landmarks.", + "Look out for family discounts at museum gates." + ]; + dayTip = familyTips[day % familyTips.length]; + } else if (travelStyle === "Couple") { + const coupleTips = [ + "Take romantic photos at scenic viewpoints.", + "Consider booking a cozy table corner at the recommended restaurant.", + "Take a slow evening walk to catch beautiful city lights." + ]; + dayTip = coupleTips[day % coupleTips.length]; + } + + itinerary.push({ + dayNumber: day, + date: addDays(startDate, day - 1), + morning, + afternoon, + evening, + restaurant: rest, + tips: dayTip, + cost: dayCost + }); + } + + // 4. Generate Checklist items + const checklist = generateChecklistData(destName, season, travelStyle, interests); + + return { + id: Date.now(), + destName, + destinationVal, + isCustom, + duration, + budget, + startDate, + travelStyle, + transport, + accommodation, + interests, + season, + weather: weatherInfo, + breakdown: { accommodation: estAcc, food: estFood, transport: estTransport, activities: estActivities, misc: estMisc, total: estTotal }, + attractions: db.attractions, + itinerary, + checklist, + activitiesPlannedCount: actCount + }; +} + +function generateChecklistData(dest, season, style, interests) { + const list = { + Essentials: [ + { text: "Passport, Visa documents, & Photo IDs", checked: false }, + { text: "Travel insurance printouts & tickets", checked: false }, + { text: "Universal power outlets adapter", checked: false }, + { text: "Local currency (Cash) & backup credit cards", checked: false }, + { text: "First-aid essentials & personal prescriptions", checked: false } + ], + Clothing: [], + Gear: [], + Toiletries: [ + { text: "Toothbrush, paste, & dental floss", checked: false }, + { text: "Travel-size shampoo, wash, & conditioner", checked: false }, + { text: "Sunscreen lotion (SPF 30+)", checked: false }, + { text: "Deodorant & light perfume spray", checked: false } + ] + }; + + // Clothing based on season + if (season === "Summer") { + list.Clothing.push( + { text: "Light, breathable cotton t-shirts", checked: false }, + { text: "Shorts, skirts, & comfortable wear", checked: false }, + { text: "Swimwear & beach towel", checked: false }, + { text: "Sun hat or baseball cap", checked: false }, + { text: "Polarized sunglasses", checked: false } + ); + } else if (season === "Winter") { + list.Clothing.push( + { text: "Heavy wool winter coat / Puffer jacket", checked: false }, + { text: "Thermal base layers (tops & bottoms)", checked: false }, + { text: "Warm gloves, thick scarf, & beanie", checked: false }, + { text: "Thick woolen socks", checked: false }, + { text: "Water-resistant walking boots", checked: false } + ); + } else { + // Spring / Autumn + list.Clothing.push( + { text: "Light jacket, windbreaker, or cardigan", checked: false }, + { text: "Denim jeans & comfortable trousers", checked: false }, + { text: "Sneakers for long city walks", checked: false }, + { text: "Compact folding umbrella", checked: false } + ); + } + + // Gear based on interests + if (interests.includes("Adventure")) { + list.Gear.push( + { text: "Sturdy hiking shoes", checked: false }, + { text: "Refillable insulated water bottle", checked: false }, + { text: "Compact daypack / hiking bag", checked: false } + ); + } + if (interests.includes("Photography")) { + list.Gear.push( + { text: "Camera, lens system, & SD cards", checked: false }, + { text: "Portable battery pack charger", checked: false }, + { text: "Microfiber cleaning cloth & lens cap", checked: false } + ); + } + if (interests.includes("Beaches")) { + list.Gear.push( + { text: "Waterproof dry bag for phone/keys", checked: false }, + { text: "Flip-flops & beach sandals", checked: false } + ); + } + + // Gear based on Style + if (style === "Solo") { + list.Gear.push( + { text: "Offline maps downloaded on device", checked: false }, + { text: "Mini emergency whistle & door stopper", checked: false }, + { text: "E-reader / travel novel book", checked: false } + ); + } else if (style === "Family") { + list.Gear.push( + { text: "Child identification bands / cards", checked: false }, + { text: "Wet wipes & sanitizing sprays", checked: false }, + { text: "Kids travel toys & coloring papers", checked: false } + ); + } else if (style === "Business") { + list.Gear.push( + { text: "Laptop, charger, & mouse", checked: false }, + { text: "Formal blazer & ironed wear", checked: false }, + { text: "Notebook & fine pen", checked: false } + ); + } + + // Ensure clothing/gear is not empty + if (list.Clothing.length === 0) { + list.Clothing.push({ text: "Standard daily casual clothes", checked: false }); + } + if (list.Gear.length === 0) { + list.Gear.push({ text: "Portable power bank charger", checked: false }); + } + + return list; +} + +// Helper: adds days to date string +function addDays(dateStr, days) { + const date = new Date(dateStr); + date.setDate(date.getDate() + days); + return date.toLocaleDateString("en-US", { weekday: 'short', month: 'short', day: 'numeric' }); +} + +// ========================================================================== +// 5. DASHBOARD RENDERING & ANIMATED COUNTERS +// ========================================================================== + +function renderTripDashboard(trip) { + resultsPlaceholder.style.display = "none"; + resultsContent.style.display = "block"; + + // Static Details + statDestName.textContent = `${trip.destName}, ${trip.isCustom ? "Custom" : DESTINATIONS_DATABASE[trip.destinationVal].country}`; + + // Animated counters + animateCounter(statTotalDays, 0, trip.duration, 800); + animateCounter(statTripBudget, 0, trip.budget, 1000, "$"); + animateCounter(statEstimatedCost, 0, trip.breakdown.total, 1000, "$"); + + const savings = trip.budget - trip.breakdown.total; + const savingsAbs = Math.abs(savings); + if (savings >= 0) { + statSavingsLabel.textContent = "Savings Remaining"; + statSavingsCost.className = "stat-value"; // reset color + animateCounter(statSavingsCost, 0, savingsAbs, 1000, "$"); + } else { + statSavingsLabel.textContent = "Amount Over Budget"; + statSavingsCost.className = "stat-value text-danger"; // make it red + animateCounter(statSavingsCost, 0, savingsAbs, 1000, "-$"); + } + animateCounter(statActivitiesCount, 0, trip.activitiesPlannedCount, 800); + + // Budget ring utilization + const pct = Math.round((trip.breakdown.total / trip.budget) * 100); + budgetPctVal.textContent = `${pct}%`; + + if (pct > 100) { + budgetUtilizationStatus.textContent = "Over Budget"; + budgetUtilizationStatus.style.color = "var(--danger)"; + budgetRingIndicator.setAttribute("stroke", "var(--danger)"); + } else { + budgetUtilizationStatus.textContent = "Under Budget"; + budgetUtilizationStatus.style.color = "var(--accent-green)"; + budgetRingIndicator.setAttribute("stroke", "var(--primary-glow)"); + } + + // Update ring dashoffset + // dasharray is 439.8 + const offset = 439.8 - (439.8 * Math.min(pct, 100)) / 100; + budgetRingIndicator.style.strokeDashoffset = offset; + + // Cost Breakdown Progress Bars + updateProgressBar(breakdownHotelCost, breakdownHotelFill, trip.breakdown.accommodation, trip.budget); + updateProgressBar(breakdownFoodCost, breakdownFoodFill, trip.breakdown.food, trip.budget); + updateProgressBar(breakdownTransportCost, breakdownTransportFill, trip.breakdown.transport, trip.budget); + updateProgressBar(breakdownActivitiesCost, breakdownActivitiesFill, trip.breakdown.activities, trip.budget); + updateProgressBar(breakdownMiscCost, breakdownMiscFill, trip.breakdown.misc, trip.budget); + + // Weather Card + weatherSeasonVal.textContent = trip.season; + weatherTempVal.textContent = trip.weather.temp; + weatherDescription.textContent = trip.weather.desc; + weatherAdviceVal.textContent = trip.weather.advice; + + // Weather Icon + let weatherIconName = "sun"; + if (trip.weather.icon === "cloud-sun") weatherIconName = "cloud-sun"; + else if (trip.weather.icon === "cloud-drizzle") weatherIconName = "cloud-drizzle"; + else if (trip.weather.icon === "cloud-snow") weatherIconName = "cloud-snow"; + else if (trip.weather.icon === "snowflake") weatherIconName = "snowflake"; + else if (trip.weather.icon === "cloud-rain") weatherIconName = "cloud-rain"; + else if (trip.weather.icon === "thermometer") weatherIconName = "thermometer"; + + weatherMainIcon.innerHTML = ``; + + // Attractions + renderAttractions(trip.attractions); + + // Day-by-Day timeline + renderItineraryTimeline(trip.itinerary); + + // Packing list checklist + renderPackingChecklist(trip.checklist); + + // Re-draw icons + lucide.createIcons(); +} + +function updateProgressBar(costEl, fillEl, amount, budget) { + costEl.textContent = `$${amount}`; + const pct = Math.min((amount / budget) * 100, 100); + fillEl.style.width = `${pct}%`; +} + +function renderAttractions(attractions) { + attractionsListContainer.innerHTML = ""; + const grid = document.createElement("div"); + grid.className = "attractions-grid-items"; + + attractions.forEach(att => { + const item = document.createElement("div"); + item.className = "attraction-item"; + item.innerHTML = ` +
+ +
+
+ ${att.name} ($${att.cost}) + ${att.desc} + Tip: ${att.tip} +
+ `; + grid.appendChild(item); + }); + + attractionsListContainer.appendChild(grid); +} + +// Animated counter utility +function animateCounter(element, start, end, duration, prefix = "", suffix = "") { + let startTimestamp = null; + const step = (timestamp) => { + if (!startTimestamp) startTimestamp = timestamp; + const progress = Math.min((timestamp - startTimestamp) / duration, 1); + const value = Math.floor(progress * (end - start) + start); + element.textContent = `${prefix}${value.toLocaleString()}${suffix}`; + if (progress < 1) { + window.requestAnimationFrame(step); + } else { + element.textContent = `${prefix}${end.toLocaleString()}${suffix}`; + } + }; + window.requestAnimationFrame(step); +} + +// ========================================================================== +// 6. TIMELINE ACCORDION & ITINERARY RENDERING +// ========================================================================== + +function renderItineraryTimeline(itinerary) { + itineraryTimelineContainer.innerHTML = ""; + + itinerary.forEach((day, index) => { + const card = document.createElement("div"); + card.className = `timeline-day-card ${index === 0 ? "active" : ""}`; + card.id = `day-card-${day.dayNumber}`; + + card.innerHTML = ` +
+ + +
+
+
+ +
+
+ + Morning +
+
+ ${day.morning.title} + ${day.morning.desc} +
+
+ +
+
+ + Midday +
+
+ ${day.afternoon.title} + ${day.afternoon.desc} +
+
+ +
+
+ + Night +
+
+ ${day.evening.title} + ${day.evening.desc} +
+
+ +
+ +
+
+ +
+ Dining Recommendation + ${day.restaurant.name} (${day.restaurant.style}) - ${day.restaurant.desc} Est: $${day.restaurant.avgCost} +
+
+ +
+ +
+ Day Safety / Local Tip + ${day.tips} +
+
+
+
+
+ `; + + // Accordion Toggle Handlers + const headerBtn = card.querySelector(".day-header-btn"); + headerBtn.addEventListener("click", () => { + const isCurrentlyActive = card.classList.contains("active"); + + // Close this or toggle + if (isCurrentlyActive) { + card.classList.remove("active"); + headerBtn.setAttribute("aria-expanded", "false"); + } else { + card.classList.add("active"); + headerBtn.setAttribute("aria-expanded", "true"); + } + }); + + itineraryTimelineContainer.appendChild(card); + }); +} + +function handleToggleAllDays() { + const cards = Array.from(itineraryTimelineContainer.querySelectorAll(".timeline-day-card")); + const anyActive = cards.some(c => c.classList.contains("active")); + + if (anyActive) { + cards.forEach(c => { + c.classList.remove("active"); + c.querySelector(".day-header-btn").setAttribute("aria-expanded", "false"); + }); + btnToggleAllDays.textContent = "Expand All"; + } else { + cards.forEach(c => { + c.classList.add("active"); + c.querySelector(".day-header-btn").setAttribute("aria-expanded", "true"); + }); + btnToggleAllDays.textContent = "Collapse All"; + } +} + +// ========================================================================== +// 7. CHECKLIST PERSISTENCE & RENDERING +// ========================================================================== + +function renderPackingChecklist(checklist) { + checklistContainer.innerHTML = ""; + + Object.keys(checklist).forEach(category => { + const items = checklist[category]; + if (items.length === 0) return; + + const catPanel = document.createElement("div"); + catPanel.className = "checklist-cat-panel"; + + let iconName = "briefcase"; + if (category === "Essentials") iconName = "key-round"; + else if (category === "Clothing") iconName = "shirt"; + else if (category === "Gear") iconName = "mountain"; + else if (category === "Toiletries") iconName = "sparkles"; + + catPanel.innerHTML = ` +
+ + ${category} +
+
+ `; + + const listContainer = catPanel.querySelector(".checklist-items-list"); + + items.forEach((item, index) => { + const itemLabel = document.createElement("label"); + itemLabel.className = "check-item-label"; + itemLabel.innerHTML = ` + + ${item.text} + `; + + const checkbox = itemLabel.querySelector("input"); + checkbox.addEventListener("change", () => { + item.checked = checkbox.checked; + updateChecklistProgress(); + // Save state immediately if saving trips is active + saveTripsToLocalStorage(); + }); + + listContainer.appendChild(itemLabel); + }); + + checklistContainer.appendChild(catPanel); + }); + + updateChecklistProgress(); +} + +function updateChecklistProgress() { + if (!appState.currentTrip) return; + + const list = appState.currentTrip.checklist; + let total = 0; + let checked = 0; + + Object.keys(list).forEach(cat => { + list[cat].forEach(item => { + total++; + if (item.checked) checked++; + }); + }); + + checklistProgressText.textContent = `${checked} / ${total} Completed`; + const pct = total > 0 ? (checked / total) * 100 : 0; + checklistProgressFill.style.width = `${pct}%`; +} + +// ========================================================================== +// 8. SAVED TRIPS LOCAL STORAGE CONTROLLER +// ========================================================================== + +function handleSaveTrip() { + if (!appState.currentTrip) return; + + // Check if trip is already saved (update/overwrite) + const existingIdx = appState.savedTrips.findIndex(t => t.id === appState.currentTrip.id); + + if (existingIdx >= 0) { + appState.savedTrips[existingIdx] = appState.currentTrip; + } else { + // New Save + appState.savedTrips.push(appState.currentTrip); + } + + saveTripsToLocalStorage(); + renderSavedTripsList(); + showSaveConfirmationBadge(); +} + +function showSaveConfirmationBadge() { + btnSaveTrip.innerHTML = ` Saved!`; + btnSaveTrip.className = "btn btn-primary"; + lucide.createIcons(); + + setTimeout(() => { + btnSaveTrip.innerHTML = ` Save Itinerary`; + btnSaveTrip.className = "btn btn-secondary"; + lucide.createIcons(); + }, 2000); +} + +function saveTripsToLocalStorage() { + try { + localStorage.setItem("vagabond_saved_trips", JSON.stringify(appState.savedTrips)); + qsTripsSaved.textContent = appState.savedTrips.length; + } catch (err) { + console.error("Local storage error:", err); + validationAlert.textContent = "Notice: LocalStorage is full. Trip details could not be permanently saved."; + validationAlert.style.display = "block"; + } +} + +function loadSavedTrips() { + try { + const raw = localStorage.getItem("vagabond_saved_trips"); + if (raw) { + appState.savedTrips = JSON.parse(raw); + } else { + appState.savedTrips = []; + } + } catch (err) { + console.error("Failed to load saved trips", err); + appState.savedTrips = []; + } + + qsTripsSaved.textContent = appState.savedTrips.length; + renderSavedTripsList(); +} + +function renderSavedTripsList() { + savedTripsContainer.innerHTML = ""; + + if (appState.savedTrips.length === 0) { + savedTripsContainer.innerHTML = ` +

No saved itineraries found. Generate and click "Save Itinerary" to store your vacation details.

+ `; + return; + } + + appState.savedTrips.forEach(trip => { + const card = document.createElement("article"); + card.className = "saved-trip-card"; + + // Format date nicely + const dateFormatted = new Date(trip.startDate).toLocaleDateString("en-US", { + month: "short", day: "numeric", year: "numeric" + }); + + card.innerHTML = ` +
+
+

${trip.destName}

+ ${trip.travelStyle} +
+ ${dateFormatted} + +
+
+ Duration + ${trip.duration} Days +
+
+ Est. Cost + $${trip.breakdown.total.toLocaleString()} +
+
+
+ +
+ + +
+ `; + + // Listeners for load/delete + card.querySelector(".btn-reload").addEventListener("click", () => reloadTrip(trip.id)); + card.querySelector(".btn-delete").addEventListener("click", () => deleteTrip(trip.id)); + + savedTripsContainer.appendChild(card); + }); + + lucide.createIcons(); +} + +function reloadTrip(tripId) { + const trip = appState.savedTrips.find(t => t.id === tripId); + if (!trip) return; + + // Restore inputs + selectDestination.value = trip.destinationVal; + handleDestinationChange(); + + if (trip.isCustom) { + inputCustomDestination.value = trip.destName; + } else { + inputCustomDestination.value = ""; + } + + inputDuration.value = trip.duration; + inputStartDate.value = trip.startDate; + inputBudget.value = trip.budget; + selectStyle.value = trip.travelStyle; + selectTransport.value = trip.transport; + selectAccommodation.value = trip.accommodation; + + // Restore checkboxes + const checkboxes = document.querySelectorAll('input[name="interests"]'); + checkboxes.forEach(cb => { + cb.checked = trip.interests.includes(cb.value); + }); + + // Set active trip state and render + appState.currentTrip = trip; + btnSaveTrip.removeAttribute("disabled"); + renderTripDashboard(trip); + + // Scroll + resultsContent.scrollIntoView({ behavior: "smooth" }); +} + +function deleteTrip(tripId) { + appState.savedTrips = appState.savedTrips.filter(t => t.id !== tripId); + + // If the deleted trip is currently active, clear active state + if (appState.currentTrip && appState.currentTrip.id === tripId) { + appState.currentTrip = null; + btnSaveTrip.setAttribute("disabled", "disabled"); + } + + saveTripsToLocalStorage(); + renderSavedTripsList(); +} + +function handleClearAllTrips() { + if (confirm("Are you sure you want to delete all saved itineraries? This action cannot be undone.")) { + appState.savedTrips = []; + appState.currentTrip = null; + btnSaveTrip.setAttribute("disabled", "disabled"); + saveTripsToLocalStorage(); + renderSavedTripsList(); + } +} diff --git a/projects/AI Travel Itinerary Planner/style.css b/projects/AI Travel Itinerary Planner/style.css new file mode 100644 index 0000000..976220b --- /dev/null +++ b/projects/AI Travel Itinerary Planner/style.css @@ -0,0 +1,1810 @@ +/* ========================================================================== + 1. VARIABLES & THEMING + ========================================================================== */ + +:root { + /* Font Stacks */ + --font-display: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Global Transitions */ + --transition-fast: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 0.35s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.6s cubic-bezier(0.4, 0, 0.2, 1); + + /* Radius System */ + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-circle: 50%; +} + +/* Dark Theme (Default) */ +html[data-theme="dark"] { + --bg-gradient-start: #080b11; + --bg-gradient-end: #0f131c; + --text-main: #f8fafc; + --text-muted: #94a3b8; + --text-inverse: #0f172a; + + /* Glassmorphism Configuration */ + --glass-bg: rgba(22, 28, 41, 0.55); + --glass-border: rgba(255, 255, 255, 0.07); + --glass-glow: rgba(99, 102, 241, 0.05); + --card-shadow: 0 10px 40px 0 rgba(0, 0, 0, 0.5); + + /* Curated Color Palette */ + --primary: #6366f1; /* Indigo */ + --primary-hover: #818cf8; + --primary-glow: #6366f1; + + --secondary: rgba(255, 255, 255, 0.06); + --secondary-hover: rgba(255, 255, 255, 0.1); + --secondary-text: #e2e8f0; + + --danger: #f43f5e; /* Rose */ + --danger-hover: #fb7185; + + /* Accent Colors */ + --accent-blue: #38bdf8; + --accent-orange: #fb923c; + --accent-green: #34d399; + --accent-purple: #c084fc; + --accent-pink: #f472b6; + --accent-teal: #2dd4bf; + + /* Form Elements */ + --input-bg: rgba(13, 17, 26, 0.7); + --input-border: rgba(255, 255, 255, 0.1); + --input-focus-border: #6366f1; + --input-text: #f8fafc; + + /* Static Widgets */ + --header-bg: rgba(8, 11, 17, 0.8); + --border-divider: rgba(255, 255, 255, 0.06); + --card-hover-bg: rgba(255, 255, 255, 0.02); +} + +/* Light Theme */ +html[data-theme="light"] { + --bg-gradient-start: #f8fafc; + --bg-gradient-end: #e2e8f0; + --text-main: #0f172a; + --text-muted: #64748b; + --text-inverse: #ffffff; + + /* Glassmorphism Configuration */ + --glass-bg: rgba(255, 255, 255, 0.75); + --glass-border: rgba(15, 23, 42, 0.07); + --glass-glow: rgba(99, 102, 241, 0.04); + --card-shadow: 0 10px 40px 0 rgba(15, 23, 42, 0.06); + + /* Curated Color Palette */ + --primary: #4f46e5; /* Indigo */ + --primary-hover: #4338ca; + --primary-glow: #4f46e5; + + --secondary: rgba(15, 23, 42, 0.04); + --secondary-hover: rgba(15, 23, 42, 0.08); + --secondary-text: #334155; + + --danger: #e11d48; /* Rose */ + --danger-hover: #be123c; + + /* Accent Colors */ + --accent-blue: #0284c7; + --accent-orange: #ea580c; + --accent-green: #059669; + --accent-purple: #7c3aed; + --accent-pink: #db2777; + --accent-teal: #0d9488; + + /* Form Elements */ + --input-bg: rgba(255, 255, 255, 0.9); + --input-border: rgba(15, 23, 42, 0.12); + --input-focus-border: #4f46e5; + --input-text: #0f172a; + + /* Static Widgets */ + --header-bg: rgba(248, 250, 252, 0.85); + --border-divider: rgba(15, 23, 42, 0.06); + --card-hover-bg: rgba(15, 23, 42, 0.01); +} + +/* ========================================================================== + 2. RESET & BASE + ========================================================================== */ + +*, *::before, *::after { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-body); + background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); + background-attachment: fixed; + color: var(--text-main); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 100vh; +} + +/* Accessibility: Skip Links */ +.skip-link { + position: absolute; + top: -100px; + left: 0; + background: var(--primary); + color: var(--text-inverse); + padding: 10px 20px; + font-weight: 600; + border-radius: 0 0 var(--radius-sm) 0; + z-index: 10000; + transition: top var(--transition-fast); + text-decoration: none; +} + +.skip-link:focus { + top: 0; + outline: 3px solid var(--text-main); +} + +:focus-visible { + outline: 3px solid var(--primary); + outline-offset: 4px; +} + +input, button, select, textarea { + font-family: inherit; +} + +/* Scrollbar Customization */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--input-border); + border-radius: 8px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* ========================================================================== + 3. LAYOUT & HEADER + ========================================================================== */ + +.app-header { + position: sticky; + top: 0; + z-index: 1000; + background: var(--header-bg); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--glass-border); + padding: 14px 20px; + transition: background var(--transition-normal), border var(--transition-normal); +} + +.header-container { + max-width: 1400px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; +} + +.logo-area { + display: flex; + align-items: center; + gap: 12px; +} + +.logo-icon { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + color: var(--text-inverse); + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: var(--radius-md); + box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3); +} + +.logo-icon i { + width: 22px; + height: 22px; +} + +.logo-area h1 { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 800; + margin: 0; + line-height: 1.1; + letter-spacing: -0.5px; +} + +.logo-area .subtitle { + font-size: 0.78rem; + color: var(--text-muted); + margin: 2px 0 0 0; + font-weight: 500; + letter-spacing: 0.2px; +} + +.header-controls { + display: flex; + align-items: center; + gap: 16px; +} + +.slogan-badge { + font-size: 0.85rem; + color: var(--primary); + font-weight: 600; + background: var(--glass-glow); + padding: 6px 14px; + border-radius: 50px; + border: 1px solid var(--glass-border); + text-align: center; + max-width: 320px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.theme-toggle-btn { + background: var(--secondary); + border: 1px solid var(--glass-border); + color: var(--text-main); + width: 40px; + height: 40px; + border-radius: var(--radius-md); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background var(--transition-fast), transform var(--transition-fast); +} + +.theme-toggle-btn:hover { + background: var(--secondary-hover); + transform: translateY(-2px); +} + +.theme-toggle-btn i { + width: 18px; + height: 18px; +} + +html[data-theme="dark"] .sun-icon { display: block; } +html[data-theme="dark"] .moon-icon { display: none; } +html[data-theme="light"] .sun-icon { display: none; } +html[data-theme="light"] .moon-icon { display: block; } + +/* Main App Container */ +.app-container { + max-width: 1400px; + margin: 0 auto; + padding: 24px 20px 60px 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* ========================================================================== + 4. GLASS PANELS & HERO + ========================================================================== */ + +.glass-panel { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + box-shadow: var(--card-shadow); + padding: 24px; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + position: relative; + overflow: hidden; + transition: transform var(--transition-normal), box-shadow var(--transition-normal); +} + +.glass-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, transparent, var(--glass-glow), transparent); + pointer-events: none; +} + +.hero-section { + display: flex; + justify-content: space-between; + align-items: center; + gap: 40px; +} + +.hero-content h2 { + font-family: var(--font-display); + font-size: 2.1rem; + font-weight: 800; + margin: 0 0 10px 0; + letter-spacing: -0.5px; + background: linear-gradient(90deg, var(--text-main) 0%, var(--primary-hover) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.hero-content p { + color: var(--text-muted); + margin: 0; + max-width: 800px; + font-size: 0.98rem; +} + +.hero-quick-stats { + display: flex; + gap: 20px; + flex-shrink: 0; +} + +.quick-stat-item { + display: flex; + flex-direction: column; + align-items: flex-end; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--glass-border); + padding: 12px 20px; + border-radius: var(--radius-md); + text-align: right; + min-width: 140px; +} + +.qs-val { + font-family: var(--font-display); + font-size: 1.8rem; + font-weight: 800; + color: var(--primary); + line-height: 1.2; +} + +.qs-label { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 600; + text-transform: uppercase; + margin-top: 4px; +} + +/* ========================================================================== + 5. DASHBOARD GRID & FORMS + ========================================================================== */ + +.dashboard-grid { + display: grid; + grid-template-columns: 420px 1fr; + gap: 24px; + align-items: start; +} + +.planner-panel { + position: sticky; + top: 90px; +} + +.results-dashboard-column { + display: flex; + flex-direction: column; + gap: 24px; + min-width: 0; /* Prevents flex items from breaking layout overflow */ +} + +.section-title { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-display); + font-size: 1.3rem; + font-weight: 700; + margin: 0 0 20px 0; +} + +.section-title i { + color: var(--primary); + width: 20px; + height: 20px; +} + +.card-subtitle-small { + font-family: var(--font-display); + font-size: 0.95rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + letter-spacing: 0.5px; + margin: 0 0 16px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.card-subtitle-small i { + width: 16px; + height: 16px; + color: var(--primary); +} + +/* Form Styles */ +.form-group { + margin-bottom: 18px; + display: flex; + flex-direction: column; +} + +.form-group-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-group label { + font-size: 0.88rem; + font-weight: 600; + margin-bottom: 6px; + color: var(--text-main); +} + +.select-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.select-wrapper select { + width: 100%; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius-sm); + padding: 12px 40px 12px 16px; + color: var(--input-text); + font-size: 0.95rem; + font-weight: 500; + appearance: none; + cursor: pointer; + transition: border var(--transition-fast), box-shadow var(--transition-fast); +} + +.select-wrapper select:focus { + outline: none; + border-color: var(--input-focus-border); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); +} + +.select-arrow { + position: absolute; + right: 14px; + pointer-events: none; + width: 16px; + height: 16px; + color: var(--text-muted); +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.input-prefix { + position: absolute; + left: 16px; + font-size: 1rem; + font-weight: 600; + color: var(--text-muted); + pointer-events: none; +} + +.input-wrapper input { + padding-left: 32px; +} + +input[type="number"], +input[type="text"], +input[type="date"] { + width: 100%; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius-sm); + padding: 12px 16px; + color: var(--input-text); + font-size: 0.95rem; + font-weight: 500; + transition: border var(--transition-fast), box-shadow var(--transition-fast); +} + +input[type="number"]:focus, +input[type="text"]:focus, +input[type="date"]:focus { + outline: none; + border-color: var(--input-focus-border); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); +} + +.input-help { + font-size: 0.76rem; + color: var(--text-muted); + margin-top: 4px; +} + +/* Interests Fieldset and Checkboxes */ +.interests-fieldset { + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + padding: 16px; + margin: 4px 0 0 0; + background: rgba(255, 255, 255, 0.01); +} + +.interests-fieldset legend { + font-size: 0.82rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + padding: 0 8px; +} + +.interests-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; + user-select: none; +} + +.checkbox-label input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.checkbox-label span { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius-sm); + color: var(--text-muted); + font-size: 0.85rem; + font-weight: 500; + width: 100%; + transition: all var(--transition-fast); +} + +.checkbox-icon { + width: 14px; + height: 14px; + color: var(--text-muted); + transition: color var(--transition-fast); +} + +.checkbox-label:hover span { + color: var(--text-main); + border-color: var(--text-muted); +} + +.checkbox-label input:checked + span { + background: var(--glass-glow); + border-color: var(--primary); + color: var(--primary-hover); + font-weight: 600; +} + +.checkbox-label input:checked + span .checkbox-icon { + color: var(--primary-hover); +} + +/* Error Banner styling */ +.validation-banner { + background: rgba(244, 63, 94, 0.12); + border: 1px solid rgba(244, 63, 94, 0.25); + color: var(--danger); + padding: 12px 16px; + border-radius: var(--radius-sm); + font-size: 0.85rem; + font-weight: 600; + margin-top: 14px; + text-align: center; +} + +/* Form Action Buttons */ +.form-actions { + display: flex; + gap: 12px; + margin-top: 24px; + border-top: 1px solid var(--border-divider); + padding-top: 20px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 20px; + border-radius: var(--radius-sm); + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast); + border: 1px solid transparent; + white-space: nowrap; + flex: 1; +} + +.btn:active { + transform: scale(0.97); +} + +.btn-primary { + background: var(--primary); + color: var(--text-inverse); +} + +.btn-primary:hover { + background: var(--primary-hover); + box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35); +} + +.btn-secondary { + background: var(--secondary); + color: var(--secondary-text); + border-color: var(--glass-border); +} + +.btn-secondary:hover { + background: var(--secondary-hover); +} + +.btn-secondary:disabled { + opacity: 0.35; + cursor: not-allowed; + transform: none; +} + +.btn-danger { + background: var(--danger); + color: var(--text-inverse); +} + +.btn-danger:hover { + background: var(--danger-hover); + box-shadow: 0 4px 14px rgba(244, 63, 94, 0.25); +} + +.btn-sm { + padding: 6px 12px; + font-size: 0.78rem; + border-radius: var(--radius-sm); +} + +/* ========================================================================== + 6. PLACEHOLDER & STATISTICS CARDS + ========================================================================== */ + +.empty-state-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 80px 40px; + color: var(--text-muted); +} + +.empty-state-icon { + width: 80px; + height: 80px; + border-radius: 20px; + background: var(--secondary); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; +} + +.empty-state-icon i { + width: 40px; + height: 40px; + color: var(--primary); +} + +.empty-state-panel h3 { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 700; + color: var(--text-main); + margin: 0 0 10px 0; +} + +.empty-state-panel p { + max-width: 460px; + margin: 0; + font-size: 0.95rem; +} + +/* Statistics Grid */ +.dashboard-stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.stat-card { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + overflow: hidden; +} + +.stat-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: var(--radius-md); + flex-shrink: 0; +} + +.stat-icon-wrapper i { + width: 22px; + height: 22px; +} + +/* Stats Colors */ +.stat-icon-wrapper.blue { background: rgba(56, 189, 248, 0.12); color: var(--accent-blue); } +.stat-icon-wrapper.orange { background: rgba(251, 146, 60, 0.12); color: var(--accent-orange); } +.stat-icon-wrapper.green { background: rgba(52, 211, 153, 0.12); color: var(--accent-green); } +.stat-icon-wrapper.purple { background: rgba(192, 132, 252, 0.12); color: var(--accent-purple); } +.stat-icon-wrapper.teal { background: rgba(45, 212, 191, 0.12); color: var(--accent-teal); } +.stat-icon-wrapper.pink { background: rgba(244, 114, 182, 0.12); color: var(--accent-pink); } + +.stat-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.stat-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + letter-spacing: 0.5px; +} + +.stat-value { + font-family: var(--font-display); + font-size: 1.3rem; + font-weight: 800; + margin-top: 2px; + line-height: 1.2; +} + +.text-truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ========================================================================== + 7. BUDGET DASHBOARD & PROGRESS + ========================================================================== */ + +.budget-split-layout { + display: grid; + grid-template-columns: 180px 1fr; + gap: 40px; + align-items: center; +} + +/* Progress Ring Component */ +.gauge-card { + display: flex; + justify-content: center; +} + +.progress-ring-container { + position: relative; + width: 160px; + height: 160px; +} + +.progress-ring-fill { + transition: stroke-dashoffset var(--transition-slow); +} + +.progress-text-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + text-align: center; +} + +.score-number { + font-family: var(--font-display); + font-size: 2.2rem; + font-weight: 800; + line-height: 1.1; +} + +.score-grade { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + margin-top: 4px; + letter-spacing: 0.5px; + color: var(--text-muted); +} + +/* Progress bar list details */ +.budget-bars-list { + display: flex; + flex-direction: column; + gap: 14px; +} + +.budget-bar-item { + display: flex; + flex-direction: column; +} + +.bar-info { + display: flex; + justify-content: space-between; + font-size: 0.85rem; + font-weight: 550; + margin-bottom: 6px; +} + +.bar-label { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-muted); +} + +.bar-label i { + width: 14px; + height: 14px; +} + +.bar-cost { + color: var(--text-main); + font-weight: 700; +} + +.bar-progress-bg { + height: 6px; + background: var(--secondary); + border-radius: 3px; + overflow: hidden; +} + +.bar-progress-fill { + height: 100%; + border-radius: 3px; + width: 0%; + transition: width var(--transition-slow); +} + +.bar-progress-fill.purple { background: var(--accent-purple); } +.bar-progress-fill.orange { background: var(--accent-orange); } +.bar-progress-fill.blue { background: var(--accent-blue); } +.bar-progress-fill.pink { background: var(--accent-pink); } +.bar-progress-fill.teal { background: var(--accent-teal); } + +/* ========================================================================== + 8. WEATHER CARD & ATTRACTIONS + ========================================================================== */ + +.weather-attractions-row { + display: grid; + grid-template-columns: 1fr 1.3fr; + gap: 24px; +} + +.weather-body { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + min-height: 130px; +} + +.weather-main { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 12px; +} + +.weather-icon-lg { + font-size: 2.5rem; + color: var(--accent-orange); + animation: float 4s ease-in-out infinite; +} + +.weather-icon-lg i { + width: 44px; + height: 44px; +} + +.weather-temp { + display: block; + font-family: var(--font-display); + font-size: 2.2rem; + font-weight: 800; + line-height: 1.1; +} + +.weather-desc { + font-size: 0.88rem; + color: var(--text-muted); + font-weight: 500; +} + +.weather-details { + display: flex; + gap: 24px; + border-top: 1px solid var(--border-divider); + padding-top: 12px; +} + +.weather-detail-item { + display: flex; + flex-direction: column; +} + +.w-label { + font-size: 0.72rem; + text-transform: uppercase; + color: var(--text-muted); + font-weight: 600; + letter-spacing: 0.5px; +} + +.w-val { + font-size: 0.85rem; + font-weight: 700; + color: var(--text-main); + margin-top: 2px; +} + +/* Attractions List Component */ +.attractions-grid-items { + display: flex; + flex-direction: column; + gap: 12px; +} + +.attraction-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px; + border-radius: var(--radius-md); + border: 1px solid transparent; + transition: all var(--transition-fast); +} + +.attraction-item:hover { + background: var(--card-hover-bg); + border-color: var(--glass-border); +} + +.attraction-badge-icon { + width: 32px; + height: 32px; + border-radius: var(--radius-sm); + background: rgba(99, 102, 241, 0.08); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.attraction-badge-icon i { + width: 16px; + height: 16px; +} + +.attraction-details { + display: flex; + flex-direction: column; +} + +.attraction-name { + font-size: 0.9rem; + font-weight: 700; + color: var(--text-main); +} + +.attraction-desc { + font-size: 0.78rem; + color: var(--text-muted); + margin-top: 2px; +} + +.attraction-tip { + font-size: 0.74rem; + color: var(--accent-green); + font-weight: 600; + margin-top: 3px; + display: flex; + align-items: center; + gap: 4px; +} + +.attraction-tip i { + width: 12px; + height: 12px; +} + +/* ========================================================================== + 9. TIMELINE ITINERARY + ========================================================================== */ + +.itinerary-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.itinerary-header .section-title { + margin: 0; +} + +.timeline-container { + position: relative; + padding-left: 28px; + margin-left: 10px; +} + +/* Timeline Vertical Line */ +.timeline-container::before { + content: ''; + position: absolute; + left: 0; + top: 10px; + bottom: 10px; + width: 2px; + background: var(--border-divider); +} + +/* Timeline Day Node */ +.timeline-day-card { + position: relative; + margin-bottom: 24px; +} + +.timeline-day-card:last-child { + margin-bottom: 0; +} + +/* Timeline Node Circle Dot */ +.timeline-node-dot { + position: absolute; + left: -38px; + top: 14px; + width: 20px; + height: 20px; + border-radius: var(--radius-circle); + background: var(--bg-gradient-start); + border: 4px solid var(--primary); + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15); + z-index: 2; + transition: transform var(--transition-fast), border-color var(--transition-fast); +} + +.timeline-day-card:hover .timeline-node-dot { + transform: scale(1.1); + border-color: var(--accent-teal); +} + +/* Accordion Day Header Trigger */ +.day-header-btn { + width: 100%; + background: rgba(255, 255, 255, 0.01); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + padding: 16px 20px; + color: var(--text-main); + text-align: left; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all var(--transition-fast); +} + +.day-header-btn:hover { + background: var(--secondary); + border-color: var(--text-muted); +} + +.day-header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.day-number-badge { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 800; + color: var(--primary-hover); + background: var(--glass-glow); + padding: 4px 10px; + border-radius: var(--radius-sm); + border: 1px solid var(--glass-border); +} + +.day-title-text { + font-weight: 700; + font-size: 0.98rem; +} + +.day-header-right { + display: flex; + align-items: center; + gap: 14px; + color: var(--text-muted); + font-size: 0.8rem; +} + +.day-cost-tag { + font-weight: 700; + color: var(--accent-green); + font-size: 0.88rem; +} + +.day-chevron { + width: 16px; + height: 16px; + transition: transform var(--transition-normal); +} + +/* Collapsible Content Area */ +.day-content-collapsible { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows var(--transition-normal); + overflow: hidden; +} + +.day-content-inner { + min-height: 0; + border-left: 1px solid var(--glass-border); + border-right: 1px solid var(--glass-border); + border-bottom: 1px solid var(--glass-border); + border-bottom-left-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.015); + padding: 0 20px; /* Transitioning padding via inner content margin/padding */ +} + +/* Active State (Expanded Accordion) */ +.timeline-day-card.active .day-header-btn { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background: var(--glass-glow); + border-color: var(--primary); +} + +.timeline-day-card.active .day-chevron { + transform: rotate(180deg); +} + +.timeline-day-card.active .day-content-collapsible { + grid-template-rows: 1fr; +} + +.timeline-day-card.active .day-content-inner { + padding: 20px; +} + +/* Slots for Activities */ +.day-slots-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.slot-item { + display: flex; + gap: 16px; + padding-bottom: 16px; + border-bottom: 1px dashed var(--border-divider); +} + +.slot-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.slot-time-badge { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + border-radius: var(--radius-md); + background: var(--input-bg); + border: 1px solid var(--glass-border); + flex-shrink: 0; + color: var(--primary-hover); +} + +.slot-time-badge i { + width: 18px; + height: 18px; +} + +.slot-time-label { + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + margin-top: 2px; + letter-spacing: 0.3px; + color: var(--text-muted); +} + +.slot-details { + display: flex; + flex-direction: column; +} + +.slot-title { + font-size: 0.92rem; + font-weight: 700; + color: var(--text-main); +} + +.slot-desc { + font-size: 0.82rem; + color: var(--text-muted); + margin-top: 2px; +} + +/* Day Food/Tip Footer bar */ +.day-extra-row { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 16px; + margin-top: 20px; + border-top: 1px solid var(--border-divider); + padding-top: 16px; +} + +.day-extra-box { + background: var(--input-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + padding: 10px 14px; + display: flex; + align-items: flex-start; + gap: 10px; +} + +.day-extra-box i { + width: 16px; + height: 16px; + margin-top: 2px; + flex-shrink: 0; +} + +.day-extra-box.food i { color: var(--accent-orange); } +.day-extra-box.tips i { color: var(--accent-blue); } + +.day-extra-info { + display: flex; + flex-direction: column; +} + +.day-extra-label { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + color: var(--text-muted); + letter-spacing: 0.5px; +} + +.day-extra-val { + font-size: 0.82rem; + color: var(--text-main); + margin-top: 2px; +} + +/* ========================================================================== + 10. CHECKLIST SECTION + ========================================================================== */ + +.checklist-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.checklist-header .section-title { + margin: 0; +} + +.checklist-count { + font-size: 0.82rem; + font-weight: 700; + color: var(--primary-hover); + background: var(--glass-glow); + padding: 4px 10px; + border-radius: 20px; + border: 1px solid var(--glass-border); +} + +.checklist-progress-bar-bg { + height: 4px; + background: var(--secondary); + border-radius: 2px; + overflow: hidden; + margin: 16px 0 20px 0; +} + +.checklist-progress-bar-fill { + height: 100%; + background: var(--primary); + width: 0%; + transition: width var(--transition-normal); +} + +.checklist-categories { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +.checklist-cat-panel { + background: rgba(255, 255, 255, 0.01); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + padding: 16px; +} + +.checklist-cat-title { + font-family: var(--font-display); + font-size: 0.88rem; + font-weight: 700; + text-transform: uppercase; + color: var(--text-muted); + letter-spacing: 0.5px; + margin-bottom: 12px; + border-bottom: 1px solid var(--border-divider); + padding-bottom: 6px; + display: flex; + align-items: center; + gap: 8px; +} + +.checklist-cat-title i { + width: 14px; + height: 14px; + color: var(--primary); +} + +.checklist-items-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.check-item-label { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + user-select: none; + font-size: 0.85rem; + color: var(--text-main); + font-weight: 500; +} + +.check-item-label input[type="checkbox"] { + width: 18px; + height: 18px; + appearance: none; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 4px; + cursor: pointer; + position: relative; + transition: background var(--transition-fast), border-color var(--transition-fast); +} + +.check-item-label input[type="checkbox"]:focus { + outline: none; + border-color: var(--primary); +} + +.check-item-label input[type="checkbox"]:checked { + background: var(--primary); + border-color: var(--primary); +} + +.check-item-label input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 4px; + height: 8px; + border: solid var(--text-inverse); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.check-item-label span { + transition: color var(--transition-fast), text-decoration var(--transition-fast); +} + +.check-item-label input:checked + span { + color: var(--text-muted); + text-decoration: line-through; +} + +/* ========================================================================== + 11. SAVED TRIPS DIRECTORY + ========================================================================== */ + +.saved-trips-section { + display: flex; + flex-direction: column; +} + +.saved-trips-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.saved-trips-header .section-title { + margin: 0; +} + +.saved-trips-grid-wrapper { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 20px; +} + +.empty-saved-note { + grid-column: 1 / -1; + text-align: center; + color: var(--text-muted); + padding: 40px; + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.01); + border: 1px dashed var(--glass-border); + border-radius: var(--radius-md); + margin: 0; +} + +.saved-trip-card { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 20px; + border-radius: var(--radius-md); + border: 1px solid var(--glass-border); + background: rgba(255, 255, 255, 0.015); + transition: all var(--transition-fast); +} + +.saved-trip-card:hover { + background: var(--card-hover-bg); + border-color: var(--text-muted); + transform: translateY(-4px); +} + +.saved-trip-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 12px; +} + +.saved-trip-dest { + font-family: var(--font-display); + font-size: 1.15rem; + font-weight: 800; + color: var(--text-main); + margin: 0; +} + +.saved-trip-date { + font-size: 0.74rem; + color: var(--text-muted); + margin-top: 2px; + display: flex; + align-items: center; + gap: 4px; +} + +.saved-trip-date i { + width: 12px; + height: 12px; +} + +.saved-trip-badge { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + color: var(--primary-hover); + background: var(--glass-glow); + padding: 3px 8px; + border-radius: var(--radius-sm); + border: 1px solid var(--glass-border); +} + +.saved-trip-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 18px; + border-top: 1px solid var(--border-divider); + padding-top: 12px; +} + +.meta-field { + display: flex; + flex-direction: column; +} + +.meta-label { + font-size: 0.68rem; + text-transform: uppercase; + color: var(--text-muted); + font-weight: 600; +} + +.meta-val { + font-size: 0.85rem; + font-weight: 700; + color: var(--text-main); + margin-top: 1px; +} + +.saved-trip-actions { + display: flex; + gap: 10px; + border-top: 1px solid var(--border-divider); + padding-top: 14px; +} + +.saved-trip-actions .btn { + padding: 8px 12px; + font-size: 0.8rem; +} + +/* ========================================================================== + 12. FOOTER & ANIMATIONS + ========================================================================== */ + +.app-footer { + border-top: 1px solid var(--glass-border); + background: var(--header-bg); + backdrop-filter: blur(12px); + padding: 30px 20px; + text-align: center; + margin-top: 40px; +} + +.footer-container { + max-width: 1400px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.app-footer p { + margin: 0; + font-size: 0.88rem; + color: var(--text-muted); +} + +.app-footer a { + color: var(--primary-hover); + text-decoration: none; + font-weight: 600; +} + +.app-footer a:hover { + text-decoration: underline; +} + +.footer-note { + font-size: 0.74rem !important; + opacity: 0.8; +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes float { + 0% { transform: translateY(0px); } + 50% { transform: translateY(-5px); } + 100% { transform: translateY(0px); } +} + +.results-content-wrapper { + animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; +} + +/* ========================================================================== + 13. RESPONSIVE MEDIA QUERIES + ========================================================================== */ + +/* 1440px and higher (Aesthetic tweaks) */ +@media (min-width: 1440px) { + .app-container, .header-container, .footer-container { + max-width: 1360px; + } +} + +/* 1024px (Laptops) */ +@media (max-width: 1024px) { + .dashboard-grid { + grid-template-columns: 360px 1fr; + gap: 20px; + } + + .planner-panel { + top: 80px; + } + + .dashboard-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +/* 768px (Tablets) */ +@media (max-width: 768px) { + .dashboard-grid { + grid-template-columns: 1fr; + } + + .planner-panel { + position: static; + } + + .hero-section { + flex-direction: column; + align-items: flex-start; + gap: 20px; + } + + .hero-quick-stats { + width: 100%; + } + + .quick-stat-item { + flex: 1; + align-items: center; + text-align: center; + } + + .budget-split-layout { + grid-template-columns: 1fr; + gap: 24px; + } + + .checklist-categories { + grid-template-columns: 1fr; + } +} + +/* 375px & 320px (Mobile Phones) */ +@media (max-width: 480px) { + .header-container { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .header-controls { + width: 100%; + justify-content: space-between; + } + + .slogan-badge { + max-width: 220px; + } + + .hero-content h2 { + font-size: 1.7rem; + } + + .dashboard-stats-grid { + grid-template-columns: 1fr; + } + + .form-group-row { + grid-template-columns: 1fr; + gap: 0; + } + + .interests-grid { + grid-template-columns: 1fr; + } + + .weather-attractions-row { + grid-template-columns: 1fr; + } + + .day-extra-row { + grid-template-columns: 1fr; + } + + .day-header-btn { + padding: 12px 14px; + } + + .day-header-left { + gap: 8px; + } + + .day-title-text { + font-size: 0.88rem; + } + + .day-chevron { + width: 14px; + height: 14px; + } +} diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/README.md b/projects/Carbon Footprint Calculator & Eco Dashboard/README.md new file mode 100644 index 0000000..e5c68c3 --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/README.md @@ -0,0 +1,126 @@ +# Carbon Footprint Calculator & Eco Dashboard + +A high-performance, premium, and fully accessible single-page web dashboard to calculate, log, and visualize personal carbon footprints using realistic emission coefficients. Built with pure vanilla frontend technologies, featuring glassmorphism design layouts, light/dark themes, and real-time interactive analytics. + +--- + +## Features + +- **Multi-Category Carbon Calculator:** Custom step-by-step forms with validations covering: + - **Transportation:** Mileage logs for private cars, motorcycles, bus transits, trains/metro systems, and hours spent on flights. + - **Energy Consumption:** Household electricity (kWh), cooking LPG cylinders (kg), air conditioner run-times (hours), and daily water utility (liters). + - **Dietary Footprint:** Weekly meals counts (Vegetarian, Non-Vegetarian) and portion logs for dairy/egg products. + - **Waste & Recycling:** Weight of plastics/paper trashed alongside offsetting inputs for sorted recycling and composted organic matter. +- **Dynamic Eco Score Widget:** A circular dashboard progress ring displaying an overall score from `0` to `100` (Excellent, Good, Average, Needs Improvement) using real-time animated transitions. +- **Interactive Visual Analytics:** Powered by `Chart.js` via CDN: + - **Category Breakdown (Doughnut):** Real-time proportional split of carbon impact. + - **Detailed Sources (Bar):** Visual comparison of individual contributors, featuring positive emission bars and negative green offset indicators (recycling/composting). + - **Trend Analysis (Line):** Chronological monthly progression of total emissions and Eco Scores. +- **Tailored Recommendations:** Auto-generated action lists targeting highest-impact emission categories (e.g. suggesting meatless days, solar utility, public transport, or bin sorting). +- **Monthly History Ledger:** Persistent storage for logging data. Includes options to export complete history logs as JSON and perform master database resets. +- **Dark/Light Theme Toggle:** Fluid theme-shifting system using modern CSS design tokens. +- **Accessibility Compliant:** Built using semantic HTML5 elements, correct keyboard focus handling, `aria-live` alert regions, and high color-contrast ratio. + +--- + +## Folder Structure + +```text +projects/Carbon Footprint Calculator & Eco Dashboard/ +├── index.html # HTML structure, layouts, form markup & CDN links +├── style.css # Global theme variables, layouts, animations & typography +├── script.js # Core engine, calculations, localstorage sync & chart managers +├── README.md # Extensive project documentations (this file) +├── project.json # BuildVerse project registration metadata +└── assets/ + ├── icons/ # Visual interface icons & markers + ├── images/ # Static illustrative graphics + └── screenshots/ # Project demo screenshots placeholders +``` + +--- + +## Technologies Used + +- **HTML5:** Semantic architecture for layout, form groups, and accessibility. +- **CSS3:** Modern design styling utilizing variables, glassmorphic filters, responsive flex/grid layouts, and micro-animations. +- **Vanilla JavaScript (ES6):** State binding, calculations, debouncing, and localstorage operations. +- **Chart.js (CDN):** Fast canvas-based data visualizations. +- **Google Fonts:** `Outfit` (display) and `Inter` (body text) fonts. + +--- + +## Installation & Running + +This project is completely serverless and runs directly in browser environments without needing build tools or package managers. + +1. Clone or download the BuildVerse repository. +2. Locate the project folder: + `projects/Carbon Footprint Calculator & Eco Dashboard/` +3. Double-click `index.html` or open it with any modern browser (Chrome, Edge, Firefox, Safari) to launch the app. +4. (Optional) Run a local server for testing via standard IDE extensions like Live Server, or run `npm run dev` at the root workspace directory. + +--- + +## Carbon Calculation Methodology + +Emissions are calculated in kilograms of Carbon Dioxide equivalents (kg CO₂e) per month using standard public metrics: + +### 1. Transportation +- **Private Car:** `Distance (km) * 0.18 kg CO₂e` +- **Motorcycle/Bike:** `Distance (km) * 0.02 kg CO₂e` +- **Bus Transit:** `Distance (km) * 0.08 kg CO₂e` +- **Train/Metro:** `Distance (km) * 0.04 kg CO₂e` +- **Flight Time:** `Hours * 150.00 kg CO₂e` + +### 2. Energy +- **Grid Electricity:** `kWh * 0.45 kg CO₂e` +- **LPG Cooking Gas:** `kg * 2.98 kg CO₂e` +- **Air Conditioning:** `Hours * 0.80 kg CO₂e` +- **Water Consumption:** `Liters/day * 30 days * 0.0003 kg CO₂e` + +### 3. Food (Weekly portions scaled to Monthly using factor 4.33) +- **Vegetarian Meals:** `Meals/week * 1.50 kg CO₂e * 4.33` +- **Non-Vegetarian Meals:** `Meals/week * 6.00 kg CO₂e * 4.33` +- **Dairy Portions:** `Portions/week * 0.40 kg CO₂e * 4.33` + +### 4. Waste & Recycling Offsets +- **Plastic Waste:** `kg * 2.00 kg CO₂e` +- **Paper Waste:** `kg * 0.50 kg CO₂e` +- **Recycling Offset:** `- kg sorted * 0.50 kg CO₂e` (deducted credit) +- **Composting Offset:** `- kg organic composted * 0.20 kg CO₂e` (deducted credit) + +*Note: Total category waste footprint is capped at a minimum of `0` to prevent excessive offsets from creating impossible negative footprints.* + +### 5. Eco Score Formula +The score scales footprints from `0` to `100` relative to monthly emission limits: +$$\text{Eco Score} = \max\left(0, \min\left(100, 100 - \text{round}\left(\frac{\text{Total Monthly Emissions}}{8.0}\right)\right)\right)$$ + +- **Excellent (90-100):** Extremely low carbon footprint. +- **Good (70-89):** Balanced footprint with minor opportunities. +- **Average (50-69):** Moderate carbon footprint. +- **Needs Improvement (0-49):** High carbon footprint. + +--- + +## Accessibility Compliance + +- **Aria Roles:** Defined tab lists (`role="tablist"`), panel views (`role="tabpanel"`), toggles, state values (`aria-selected`), and descriptive labels (`aria-describedby`). +- **Skip Navigation:** Implemented a visible-on-focus skip link allowing screen readers to bypass headers. +- **Keyboard Navigation:** Tab panels and visual chart displays are fully navigable using arrow keys, Home, End, Tab, and Enter keys. +- **Contrast and Readability:** Contrast ratios between background panels and text elements meet WCAG AA specifications in both Light and Dark mode variations. + +--- + +## Future Improvements + +1. **Geo-located Emission Grid Factors:** Query dynamic APIs to fetch localized regional grid coefficients for electricity consumption. +2. **Detailed Food Splits:** Expand diet category forms to cover beef, dairy, pork, and local grains individually. +3. **Interactive Mini-Games:** Include gamified ecological quizzes and carbon-neutral challenges. +4. **Historical CSV Export:** Add spreadsheets export (CSV) option alongside JSON. + +--- + +## License + +This project is open-source under the MIT License - see the LICENSE file in the BuildVerse root for details. diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/assets/icons/.gitkeep b/projects/Carbon Footprint Calculator & Eco Dashboard/assets/icons/.gitkeep new file mode 100644 index 0000000..4ad0019 --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/assets/icons/.gitkeep @@ -0,0 +1 @@ +# Gitkeep for icons folder diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/assets/images/.gitkeep b/projects/Carbon Footprint Calculator & Eco Dashboard/assets/images/.gitkeep new file mode 100644 index 0000000..acb23a8 --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/assets/images/.gitkeep @@ -0,0 +1 @@ +# Gitkeep for images folder diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/assets/screenshots/.gitkeep b/projects/Carbon Footprint Calculator & Eco Dashboard/assets/screenshots/.gitkeep new file mode 100644 index 0000000..f77d098 --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/assets/screenshots/.gitkeep @@ -0,0 +1 @@ +# Gitkeep for screenshots folder diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/index.html b/projects/Carbon Footprint Calculator & Eco Dashboard/index.html new file mode 100644 index 0000000..9cacb77 --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/index.html @@ -0,0 +1,484 @@ + + + + + + EcoCalc — Carbon Footprint Calculator & Eco Dashboard + + + + + + + + + + + + + + + + + + +
+
+
+ +
+

EcoCalc

+

Carbon Footprint Calculator & Eco Dashboard

+
+
+ +
+
+ "Small changes lead to a sustainable future." +
+ +
+
+
+ +
+ + +
+
+

Track. Visualize. Reduce.

+

Understanding your carbon footprint is the first step towards combatting climate change. Log your daily choices across transport, energy, food, and waste, visualize your environmental impact in real-time, and get actionable recommendations to live a greener life.

+
+
+
+ 0 + kg CO₂e / month +
+
+ - + Eco Grade +
+
+
+ + +
+ + +
+

+ + + + + + + + Carbon Footprint Calculator +

+ + +
+ + + + +
+ +
+ + +
+

Transportation Inputs (Monthly)

+

Estimate your distance traveled or flight hours to calculate travel-related carbon emissions.

+ +
+ +
+ + km +
+ Average mileage driven in a month. +
+ +
+ +
+ + km +
+ Distance covered by fuel-powered two-wheelers. +
+ +
+ +
+ + km +
+ Monthly distance using public buses. +
+ +
+ +
+ + km +
+ Monthly train or subways/metro commuting. +
+ +
+ +
+ + hrs +
+ Average flight hours per month (yearly hours divided by 12). +
+
+ + + + + + + + + + + + + +
+ + + +
+
+
+ + +
+ + +
+ + +
+

Eco Score

+
+ +
+ 100 + Excellent +
+
+

Your ecological impact is very low!

+
+ + +
+

Total Monthly Emissions

+
+ 0.00 + kg CO₂e +
+
+ + Calculating footprint... +
+
+
+ + +
+
+
🚗
+
+

Transport

+

0.00 kg

+
+
0%
+
+ +
+
+
+

Energy

+

0.00 kg

+
+
0%
+
+ +
+
🥗
+
+

Food

+

0.00 kg

+
+
0%
+
+ +
+
🗑️
+
+

Waste

+

0.00 kg

+
+
0%
+
+
+ + +
+
+

Visual Analytics

+
+ + + +
+
+ +
+ +
+ +
+ + + + +
+
+ + +
+

+ + + + + Eco Recommendations +

+
+
Please input carbon data to view customized sustainability tips.
+
+
+ +
+
+ + +
+
+

+ + + + + Monthly History +

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + +
Date LoggedEco ScoreEmissions (kg CO₂e)Category BreakdownAction
No historical logs found. Save a calculation to track your trends over time.
+
+
+ +
+ + + + + + + diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/project.json b/projects/Carbon Footprint Calculator & Eco Dashboard/project.json new file mode 100644 index 0000000..8095de8 --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/project.json @@ -0,0 +1,25 @@ +{ + "title": "Carbon Footprint Calculator & Eco Dashboard", + "description": "A modern carbon footprint calculator and eco dashboard to track, visualize, and improve your carbon footprint using realistic calculations and interactive charts.", + "author": { + "name": "Kola Sailaja", + "github": "KolaSailaja" + }, + "githubUsername": "KolaSailaja", + "tags": [ + "Sustainability", + "Eco", + "Calculator", + "Dashboard", + "Green" + ], + "technologies": [ + "HTML5", + "CSS3", + "JavaScript", + "Chart.js" + ], + "category": "Education / Utility", + "responsive": true, + "version": "1.0.0" +} diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/script.js b/projects/Carbon Footprint Calculator & Eco Dashboard/script.js new file mode 100644 index 0000000..95ebf6d --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/script.js @@ -0,0 +1,1218 @@ +/** + * EcoCalc — Carbon Footprint Calculator & Eco Dashboard + * Author: Kola Sailaja + * Technology: HTML5, CSS3, Vanilla JS, Chart.js + */ + +// ========================================================================== +// 1. CONFIGURATIONS & EMISSION FACTORS (kg CO2e per unit) +// ========================================================================== +const EMISSION_FACTORS = { + transport: { + car: 0.18, // per km + bike: 0.02, // per km + bus: 0.08, // per km + train: 0.04, // per km + flight: 150.0 // per hour of flight + }, + energy: { + electricity: 0.45, // per kWh + lpg: 2.98, // per kg + ac: 0.80, // per hour of AC use + water: 0.0003 // per liter (daily inputs scaled to monthly) + }, + food: { + veg: 1.5, // per vegetarian meal + nonveg: 6.0, // per non-veg meal + dairy: 0.4 // per portion of dairy + }, + waste: { + plastic: 2.0, // per kg + paper: 0.5, // per kg + recycled: -0.5, // offset credit per kg + composted: -0.2 // offset credit per kg + } +}; + +const MOTIVATIONAL_SLOGANS = [ + "Small changes lead to a sustainable future.", + "Your ecological choices shape tomorrow.", + "Ditch the car, grab a bike, save the planet!", + "Switch off a light, switch on your future.", + "One less plastic bottle is a victory for the ocean.", + "Every eco-friendly choice builds a greener tomorrow.", + "Sustainable living is not a trend, it is our survival." +]; + +// Target average monthly footprint of a sustainable global citizen: ~150 kg CO2e +const TARGET_EMISSIONS_GOAL = 150.0; +// Maximum reference monthly footprint for comparison bar (e.g. high footprint limit): 800 kg CO2e +const MAX_EMISSIONS_LIMIT = 800.0; + +// ========================================================================== +// 2. STATE MANAGEMENT +// ========================================================================== +let state = { + theme: 'dark', + inputs: { + car: '', + bike: '', + bus: '', + train: '', + flight: '', + electricity: '', + lpg: '', + ac: '', + water: '', + veg: '', + nonveg: '', + dairy: '', + plastic: '', + paper: '', + recycled: '', + composted: '' + }, + history: [] +}; + +// Global Chart variables for cleanup +let doughnutChartInstance = null; +let barChartInstance = null; +let lineChartInstance = null; + +// Debounce helper for live calculations +function debounce(func, delay = 250) { + let timer; + return function (...args) { + clearTimeout(timer); + timer = setTimeout(() => func.apply(this, args), delay); + }; +} + +// ========================================================================== +// 3. INITIALIZATION & DOMELEMENTS +// ========================================================================== +document.addEventListener('DOMContentLoaded', () => { + initApp(); +}); + +function initApp() { + cacheDOM(); + loadLocalStorage(); + bindEvents(); + updateThemeUI(); + updateTabsUI(); + + // Set random slogan + setRandomSlogan(); + + // First render + runCalculations(); +} + +let DOM = {}; +function cacheDOM() { + DOM.html = document.documentElement; + DOM.themeToggle = document.getElementById('theme-toggle'); + DOM.sloganBadge = document.getElementById('slogan-badge'); + DOM.mainContent = document.getElementById('main-content'); + + // Calculator Tab Elements + DOM.tabButtons = document.querySelectorAll('.tabs-list .tab-btn'); + DOM.tabPanels = document.querySelectorAll('.tab-panel'); + DOM.footprintForm = document.getElementById('footprint-form'); + DOM.btnPrevTab = document.getElementById('btn-prev-tab'); + DOM.btnNextTab = document.getElementById('btn-next-tab'); + DOM.validationAlert = document.getElementById('validation-alert'); + + // Inputs + DOM.inputs = { + car: document.getElementById('input-transport-car'), + bike: document.getElementById('input-transport-bike'), + bus: document.getElementById('input-transport-bus'), + train: document.getElementById('input-transport-train'), + flight: document.getElementById('input-transport-flight'), + electricity: document.getElementById('input-energy-electricity'), + lpg: document.getElementById('input-energy-lpg'), + ac: document.getElementById('input-energy-ac'), + water: document.getElementById('input-energy-water'), + veg: document.getElementById('input-food-veg'), + nonveg: document.getElementById('input-food-nonveg'), + dairy: document.getElementById('input-food-dairy'), + plastic: document.getElementById('input-waste-plastic'), + paper: document.getElementById('input-waste-paper'), + recycled: document.getElementById('input-waste-recycled'), + composted: document.getElementById('input-waste-composted') + }; + + // Outputs / Widgets + DOM.qsTotalFootprint = document.getElementById('qs-total-footprint'); + DOM.qsEcoGrade = document.getElementById('qs-eco-grade'); + DOM.scoreValue = document.getElementById('score-value'); + DOM.scoreGrade = document.getElementById('score-grade'); + DOM.scoreVerdict = document.getElementById('score-verdict'); + DOM.scoreRing = document.getElementById('score-ring-indicator'); + + DOM.totalEmissionsVal = document.getElementById('total-emissions-val'); + DOM.comparisonIndicator = document.getElementById('comparison-indicator'); + DOM.comparisonVerdict = document.getElementById('comparison-verdict'); + + // Category values + DOM.statTransportVal = document.getElementById('stat-transport-val'); + DOM.pctTransport = document.getElementById('pct-transport'); + DOM.statEnergyVal = document.getElementById('stat-energy-val'); + DOM.pctEnergy = document.getElementById('pct-energy'); + DOM.statFoodVal = document.getElementById('stat-food-val'); + DOM.pctFood = document.getElementById('pct-food'); + DOM.statWasteVal = document.getElementById('stat-waste-val'); + DOM.pctWaste = document.getElementById('pct-waste'); + + // Recommendations + DOM.recsContainer = document.getElementById('recommendations-container'); + + // History & actions + DOM.historyTableBody = document.getElementById('history-table-body'); + DOM.btnExportHistory = document.getElementById('btn-export-history'); + DOM.btnResetAll = document.getElementById('btn-reset-all'); + + // Chart Toggle Buttons + DOM.btnChartDoughnut = document.getElementById('btn-chart-doughnut'); + DOM.btnChartBar = document.getElementById('btn-chart-bar'); + DOM.btnChartLine = document.getElementById('btn-chart-line'); + DOM.chartViews = document.querySelectorAll('.chart-wrapper'); + DOM.lineChartPlaceholderInfo = document.getElementById('line-chart-placeholder-info'); +} + +// ========================================================================== +// 4. STORAGE & STATE PERSISTENCE +// ========================================================================== +function loadLocalStorage() { + try { + // Theme + const storedTheme = localStorage.getItem('ecoTheme'); + if (storedTheme) { + state.theme = storedTheme; + } else { + // System default preference + const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; + state.theme = prefersLight ? 'light' : 'dark'; + } + + // Inputs + const storedInputs = localStorage.getItem('carbonInputs'); + if (storedInputs) { + state.inputs = { ...state.inputs, ...JSON.parse(storedInputs) }; + // Distribute to DOM elements + Object.keys(state.inputs).forEach(key => { + if (DOM.inputs[key]) { + DOM.inputs[key].value = state.inputs[key]; + } + }); + } + + // History + const storedHistory = localStorage.getItem('ecoHistory'); + if (storedHistory) { + state.history = JSON.parse(storedHistory); + } + } catch (error) { + console.error('LocalStorage not available or corrupted:', error); + } +} + +function saveInputsToStorage() { + try { + Object.keys(DOM.inputs).forEach(key => { + state.inputs[key] = DOM.inputs[key].value; + }); + localStorage.setItem('carbonInputs', JSON.stringify(state.inputs)); + } catch (error) { + console.error('Error saving inputs to LocalStorage:', error); + } +} + +function saveHistoryToStorage() { + try { + localStorage.setItem('ecoHistory', JSON.stringify(state.history)); + } catch (error) { + console.error('Error saving history to LocalStorage:', error); + } +} + +// ========================================================================== +// 5. EVENT BINDING & HANDLERS +// ========================================================================== +function bindEvents() { + // Theme Toggle + DOM.themeToggle.addEventListener('click', toggleTheme); + + // Live Input calculation (debounced) + const debouncedCalculate = debounce(() => { + saveInputsToStorage(); + runCalculations(); + }, 300); + + Object.values(DOM.inputs).forEach(input => { + input.addEventListener('input', () => { + // Quick validation before calculation + if (parseFloat(input.value) < 0) { + DOM.validationAlert.removeAttribute('hidden'); + DOM.validationAlert.textContent = `Error in "${input.previousElementSibling ? input.previousElementSibling.textContent : 'Field'}": Value cannot be negative.`; + } else { + DOM.validationAlert.setAttribute('hidden', ''); + debouncedCalculate(); + } + }); + }); + + // Tab buttons click + DOM.tabButtons.forEach(btn => { + btn.addEventListener('click', (e) => { + switchTab(e.currentTarget.id); + }); + // Keyboard navigation in tabs + btn.addEventListener('keydown', handleTabKeyboard); + }); + + // Prev/Next tab buttons + DOM.btnPrevTab.addEventListener('click', () => navigateTab(-1)); + DOM.btnNextTab.addEventListener('click', () => navigateTab(1)); + + // Form Submission (Save current calculation to history) + DOM.footprintForm.addEventListener('submit', handleFormSubmit); + + // Chart view switches + DOM.btnChartDoughnut.addEventListener('click', () => switchChartView('doughnut')); + DOM.btnChartBar.addEventListener('click', () => switchChartView('bar')); + DOM.btnChartLine.addEventListener('click', () => switchChartView('line')); + + // Export & Reset buttons + DOM.btnExportHistory.addEventListener('click', exportHistoryJSON); + DOM.btnResetAll.addEventListener('click', handleResetAll); +} + +// ========================================================================== +// 6. LAYOUT NAVIGATION & THEME LOGIC +// ========================================================================== +function toggleTheme() { + state.theme = state.theme === 'dark' ? 'light' : 'dark'; + updateThemeUI(); + + // Re-render charts to update grid/axis colors for light/dark mode + renderCharts(getCategoryFootprints()); +} + +function updateThemeUI() { + DOM.html.setAttribute('data-theme', state.theme); + try { + localStorage.setItem('ecoTheme', state.theme); + } catch (error) { + console.error('Error saving theme to storage:', error); + } +} + +function setRandomSlogan() { + const randIdx = Math.floor(Math.random() * MOTIVATIONAL_SLOGANS.length); + DOM.sloganBadge.textContent = `"${MOTIVATIONAL_SLOGANS[randIdx]}"`; +} + +// Tab Switches +function switchTab(tabId) { + DOM.tabButtons.forEach(btn => { + if (btn.id === tabId) { + btn.classList.add('active'); + btn.setAttribute('aria-selected', 'true'); + btn.setAttribute('tabindex', '0'); + } else { + btn.classList.remove('active'); + btn.setAttribute('aria-selected', 'false'); + btn.setAttribute('tabindex', '-1'); + } + }); + + const activeControl = tabId.replace('tab-', 'panel-'); + DOM.tabPanels.forEach(panel => { + if (panel.id === activeControl) { + panel.classList.add('active'); + panel.removeAttribute('hidden'); + } else { + panel.classList.remove('active'); + panel.setAttribute('hidden', ''); + } + }); + + updateTabsUI(); +} + +function handleTabKeyboard(e) { + const tabs = Array.from(DOM.tabButtons); + const curIdx = tabs.indexOf(e.currentTarget); + let nextIdx; + + if (e.key === 'ArrowRight') { + nextIdx = (curIdx + 1) % tabs.length; + } else if (e.key === 'ArrowLeft') { + nextIdx = (curIdx - 1 + tabs.length) % tabs.length; + } else if (e.key === 'Home') { + nextIdx = 0; + } else if (e.key === 'End') { + nextIdx = tabs.length - 1; + } else { + return; + } + + e.preventDefault(); + tabs[nextIdx].focus(); + switchTab(tabs[nextIdx].id); +} + +function navigateTab(direction) { + const tabs = Array.from(DOM.tabButtons); + const activeTabIdx = tabs.findIndex(btn => btn.classList.contains('active')); + const newIdx = activeTabIdx + direction; + + if (newIdx >= 0 && newIdx < tabs.length) { + switchTab(tabs[newIdx].id); + tabs[newIdx].focus(); + } +} + +function updateTabsUI() { + const tabs = Array.from(DOM.tabButtons); + const activeTabIdx = tabs.findIndex(btn => btn.classList.contains('active')); + + DOM.btnPrevTab.disabled = activeTabIdx === 0; + + if (activeTabIdx === tabs.length - 1) { + DOM.btnNextTab.textContent = "Review Results"; + DOM.btnNextTab.disabled = true; + } else { + DOM.btnNextTab.textContent = "Next Tab \u2192"; + DOM.btnNextTab.disabled = false; + } +} + +// Chart view switches +function switchChartView(viewName) { + const viewBtns = [DOM.btnChartDoughnut, DOM.btnChartBar, DOM.btnChartLine]; + const activeBtnId = `btn-chart-${viewName}`; + + viewBtns.forEach(btn => { + if (btn.id === activeTabButtonId(viewName)) { + btn.classList.add('active'); + btn.setAttribute('aria-selected', 'true'); + btn.setAttribute('tabindex', '0'); + } else { + btn.classList.remove('active'); + btn.setAttribute('aria-selected', 'false'); + btn.setAttribute('tabindex', '-1'); + } + }); + + const activeContainerId = `container-${viewName}`; + DOM.chartViews.forEach(wrapper => { + if (wrapper.id === activeContainerId) { + wrapper.classList.add('active'); + wrapper.removeAttribute('hidden'); + } else { + wrapper.classList.remove('active'); + wrapper.setAttribute('hidden', ''); + } + }); +} + +function activeTabButtonId(viewName) { + return `btn-chart-${viewName}`; +} + +// ========================================================================== +// 7. CALCULATION ENGINE +// ========================================================================== +function safeFloat(val, fallback = 0.0) { + const parsed = parseFloat(val); + return isNaN(parsed) || parsed < 0 ? fallback : parsed; +} + +function runCalculations() { + // Input Validation checks + let hasNegatives = false; + Object.keys(DOM.inputs).forEach(key => { + const val = parseFloat(DOM.inputs[key].value); + if (!isNaN(val) && val < 0) { + hasNegatives = true; + } + }); + + if (hasNegatives) { + DOM.validationAlert.removeAttribute('hidden'); + return; + } else { + DOM.validationAlert.setAttribute('hidden', ''); + } + + const categoryEmissions = getCategoryFootprints(); + const totalEmissions = categoryEmissions.transport + categoryEmissions.energy + categoryEmissions.food + categoryEmissions.waste; + + // Calculate Eco Score + // 100 is best, score goes down as footprint goes up. + // 800 kg CO2e is typically a very high score limit where Eco Score becomes 0. + const score = Math.max(0, Math.min(100, Math.round(100 - (totalEmissions / 8.0)))); + + updateWidgets(totalEmissions, score, categoryEmissions); + generateRecommendations(categoryEmissions); + renderCharts(categoryEmissions); + renderHistoryTable(); +} + +function getCategoryFootprints() { + // 1. Transportation + const carD = safeFloat(DOM.inputs.car.value); + const bikeD = safeFloat(DOM.inputs.bike.value); + const busD = safeFloat(DOM.inputs.bus.value); + const trainD = safeFloat(DOM.inputs.train.value); + const flightH = safeFloat(DOM.inputs.flight.value); + + const transportTotal = (carD * EMISSION_FACTORS.transport.car) + + (bikeD * EMISSION_FACTORS.transport.bike) + + (busD * EMISSION_FACTORS.transport.bus) + + (trainD * EMISSION_FACTORS.transport.train) + + (flightH * EMISSION_FACTORS.transport.flight); + + // 2. Energy + const electricity = safeFloat(DOM.inputs.electricity.value); + const lpg = safeFloat(DOM.inputs.lpg.value); + const ac = safeFloat(DOM.inputs.ac.value); + const water = safeFloat(DOM.inputs.water.value); // daily + + const energyTotal = (electricity * EMISSION_FACTORS.energy.electricity) + + (lpg * EMISSION_FACTORS.energy.lpg) + + (ac * EMISSION_FACTORS.energy.ac) + + (water * 30 * EMISSION_FACTORS.energy.water); // scaled to monthly + + // 3. Food (weekly to monthly: * 4.33 weeks per month) + const veg = safeFloat(DOM.inputs.veg.value); + const nonveg = safeFloat(DOM.inputs.nonveg.value); + const dairy = safeFloat(DOM.inputs.dairy.value); + + const foodTotal = ((veg * EMISSION_FACTORS.food.veg) + + (nonveg * EMISSION_FACTORS.food.nonveg) + + (dairy * EMISSION_FACTORS.food.dairy)) * 4.33; + + // 4. Waste (with subtraction/offsets for recycling and composting) + const plastic = safeFloat(DOM.inputs.plastic.value); + const paper = safeFloat(DOM.inputs.paper.value); + const recycled = safeFloat(DOM.inputs.recycled.value); + const composted = safeFloat(DOM.inputs.composted.value); + + const rawWaste = (plastic * EMISSION_FACTORS.waste.plastic) + + (paper * EMISSION_FACTORS.waste.paper); + const offsets = (recycled * Math.abs(EMISSION_FACTORS.waste.recycled)) + + (composted * Math.abs(EMISSION_FACTORS.waste.composted)); + + // Net Waste footprint (must not go below zero) + const wasteTotal = Math.max(0, rawWaste - offsets); + + return { + transport: transportTotal, + energy: energyTotal, + food: foodTotal, + waste: wasteTotal, + detailed: { + car: carD * EMISSION_FACTORS.transport.car, + bike: bikeD * EMISSION_FACTORS.transport.bike, + bus: busD * EMISSION_FACTORS.transport.bus, + train: trainD * EMISSION_FACTORS.transport.train, + flight: flightH * EMISSION_FACTORS.transport.flight, + electricity: electricity * EMISSION_FACTORS.energy.electricity, + lpg: lpg * EMISSION_FACTORS.energy.lpg, + ac: ac * EMISSION_FACTORS.energy.ac, + water: water * 30 * EMISSION_FACTORS.energy.water, + veg: veg * EMISSION_FACTORS.food.veg * 4.33, + nonveg: nonveg * EMISSION_FACTORS.food.nonveg * 4.33, + dairy: dairy * EMISSION_FACTORS.food.dairy * 4.33, + plastic: plastic * EMISSION_FACTORS.waste.plastic, + paper: paper * EMISSION_FACTORS.waste.paper, + recycledOffset: -recycled * Math.abs(EMISSION_FACTORS.waste.recycled), + compostOffset: -composted * Math.abs(EMISSION_FACTORS.waste.composted) + } + }; +} + +// ========================================================================== +// 8. UPDATE UI COMPONENTS & ANIMATIONS +// ========================================================================== +function updateWidgets(totalEmissions, score, breakdown) { + // Format total emissions + const formattedEmissions = totalEmissions.toFixed(2); + DOM.qsTotalFootprint.textContent = Math.round(totalEmissions); + DOM.totalEmissionsVal.textContent = formattedEmissions; + + // Grade evaluation + let grade = "Excellent"; + let gradeClass = "grade-excellent"; + let verdictText = "Your ecological footprint is small and sustainable. Fantastic job!"; + + if (score < 50) { + grade = "Needs Improvement"; + gradeClass = "grade-poor"; + verdictText = "Your carbon footprint is high. Try using public transport or checking electricity leaks."; + } else if (score < 70) { + grade = "Average"; + gradeClass = "grade-average"; + verdictText = "You have an average carbon footprint. There are simple steps you can take to improve."; + } else if (score < 90) { + grade = "Good"; + gradeClass = "grade-good"; + verdictText = "Great effort! Your footprint is lower than average, but more optimizations are possible."; + } + + DOM.qsEcoGrade.className = `qs-val ${gradeClass}`; + DOM.qsEcoGrade.textContent = score >= 90 ? "A" : score >= 70 ? "B" : score >= 50 ? "C" : "D"; + + // Animate numeric score text + animateCounter(DOM.scoreValue, parseInt(DOM.scoreValue.textContent) || 0, score, 800); + DOM.scoreGrade.className = `score-grade ${gradeClass}`; + DOM.scoreGrade.textContent = grade; + DOM.scoreVerdict.textContent = verdictText; + + // Animate SVG circular stroke + // Circumference is 439.8 + const strokeOffset = 439.8 - (score / 100) * 439.8; + DOM.scoreRing.style.strokeDashoffset = strokeOffset; + + // Set stroke color matching the score classification + let strokeColor = "var(--color-excellent)"; + if (score < 50) strokeColor = "var(--color-poor)"; + else if (score < 70) strokeColor = "var(--color-average)"; + else if (score < 90) strokeColor = "var(--color-good)"; + DOM.scoreRing.style.stroke = strokeColor; + + // Comparison progress bar + const pct = Math.min(100, (totalEmissions / MAX_EMISSIONS_LIMIT) * 100); + DOM.comparisonIndicator.style.width = `${pct}%`; + + // Update indicator color based on emissions level + let barColor = "var(--color-excellent)"; + if (totalEmissions > 450) barColor = "var(--color-poor)"; + else if (totalEmissions > 250) barColor = "var(--color-average)"; + else if (totalEmissions > 120) barColor = "var(--color-good)"; + DOM.comparisonIndicator.style.backgroundColor = barColor; + + // Comparison text + if (totalEmissions === 0) { + DOM.comparisonVerdict.textContent = "Start filling details to benchmark your impact."; + } else if (totalEmissions <= TARGET_EMISSIONS_GOAL) { + DOM.comparisonVerdict.textContent = `Under target (${Math.round((TARGET_EMISSIONS_GOAL - totalEmissions))} kg below 150kg target!)`; + } else { + const times = (totalEmissions / TARGET_EMISSIONS_GOAL).toFixed(1); + DOM.comparisonVerdict.textContent = `${times}x higher than sustainable target (150 kg/mo).`; + } + + // Category values and percentage calculations + const total = totalEmissions || 1; // prevent divide-by-zero + + DOM.statTransportVal.textContent = breakdown.transport.toFixed(1); + DOM.pctTransport.textContent = `${Math.round((breakdown.transport / total) * 100)}%`; + + DOM.statEnergyVal.textContent = breakdown.energy.toFixed(1); + DOM.pctEnergy.textContent = `${Math.round((breakdown.energy / total) * 100)}%`; + + DOM.statFoodVal.textContent = breakdown.food.toFixed(1); + DOM.pctFood.textContent = `${Math.round((breakdown.food / total) * 100)}%`; + + DOM.statWasteVal.textContent = breakdown.waste.toFixed(1); + DOM.pctWaste.textContent = `${Math.round((breakdown.waste / total) * 100)}%`; +} + +function animateCounter(element, start, end, duration) { + let startTime = null; + const step = (timestamp) => { + if (!startTime) startTime = timestamp; + const progress = Math.min((timestamp - startTime) / duration, 1); + const value = Math.round(start + progress * (end - start)); + element.textContent = value; + if (progress < 1) { + window.requestAnimationFrame(step); + } + }; + window.requestAnimationFrame(step); +} + +// ========================================================================== +// 9. DYNAMIC ACTION RECOMMENDATIONS ENGINE +// ========================================================================== +function generateRecommendations(breakdown) { + DOM.recsContainer.innerHTML = ''; + + const recList = []; + + // Transport details + const carEmissions = breakdown.detailed.car; + const flightEmissions = breakdown.detailed.flight; + if (carEmissions > 80) { + recList.push({ + category: 'transport', + icon: '🚗', + title: 'High Private Car Footprint', + text: 'Car travel generates substantial emissions. Consider carpooling, combining trips, or switching to public transit (metro/bus) for commutes.', + impact: carEmissions + }); + } + if (flightEmissions > 100) { + recList.push({ + category: 'transport', + icon: '✈️', + title: 'High Air Travel Impact', + text: 'Long flights produce severe high-altitude carbon. Opt for train journeys for domestic travel, or join conferences virtually to offset flight hours.', + impact: flightEmissions + }); + } + + // Energy details + const electricityEmissions = breakdown.detailed.electricity; + const acEmissions = breakdown.detailed.ac; + if (electricityEmissions > 90) { + recList.push({ + category: 'energy', + icon: '💡', + title: 'High Grid Electricity Use', + text: 'Your household electricity footprint is high. Switch to LED lighting, pull plugs of standby electronics, and seek energy-certified appliances.', + impact: electricityEmissions + }); + } + if (acEmissions > 40) { + recList.push({ + category: 'energy', + icon: '❄️', + title: 'Heavy AC Power Consumption', + text: 'Air conditioning draws heavy power. Try setting thermostats to 24-25°C (75-77°F), clean AC filters regularly, or utilize cross-ventilation/fans.', + impact: acEmissions + }); + } + + // Food details + const nonvegEmissions = breakdown.detailed.nonveg; + if (nonvegEmissions > 60) { + recList.push({ + category: 'food', + icon: '🥩', + title: 'Meat-Intensive Dietary Impact', + text: 'Meat production (especially beef and pork) is highly carbon-intensive. Incorporating a few meat-free days per week cuts diet emissions by up to 50%.', + impact: nonvegEmissions + }); + } + + // Waste details + const plasticEmissions = breakdown.detailed.plastic; + const recyclingCredit = breakdown.detailed.recycledOffset; + if (plasticEmissions > 20) { + recList.push({ + category: 'waste', + icon: '🥤', + title: 'Excessive Plastic Packaging', + text: 'Single-use plastics have huge raw extraction costs. Switch to reusable steel bottles, cloth shopping bags, and buy foods in bulk.', + impact: plasticEmissions + }); + } + if (recyclingCredit === 0 && (plasticEmissions > 0 || breakdown.detailed.paper > 0)) { + recList.push({ + category: 'waste', + icon: '♻️', + title: 'No Active Waste Recycling Logged', + text: 'You are throwing plastics/papers into normal trash. Setting up a dual-bin recycling system saves materials from landfills and offsets your footprint.', + impact: 15.0 + }); + } + + // Sort recommendations by footprint impact descending + recList.sort((a, b) => b.impact - a.impact); + + if (recList.length === 0) { + const emptyDiv = document.createElement('div'); + emptyDiv.className = 'empty-recs'; + emptyDiv.textContent = 'Keep up the fantastic work! Your footprint is excellent, and you have no major red-flags.'; + DOM.recsContainer.appendChild(emptyDiv); + } else { + // Show top 3 recommendations to keep UI clean and actionable + recList.slice(0, 3).forEach(rec => { + const recCard = document.createElement('div'); + recCard.className = `rec-card`; + recCard.innerHTML = ` + +
+

${rec.title}

+

${rec.text}

+
+ `; + DOM.recsContainer.appendChild(recCard); + }); + } +} + +// ========================================================================== +// 10. CHART RENDERING VIA CHART.JS (LIVE RELOAD-SAFE) +// ========================================================================== +function getChartFontColors() { + return state.theme === 'dark' ? '#94a3b8' : '#475569'; +} + +function getChartGridColors() { + return state.theme === 'dark' ? 'rgba(255, 255, 255, 0.06)' : 'rgba(15, 23, 42, 0.06)'; +} + +function renderCharts(breakdown) { + const fontColor = getChartFontColors(); + const gridColor = getChartGridColors(); + + // 1. DOUGHNUT CHART (Category Breakdown) + if (doughnutChartInstance) { + doughnutChartInstance.destroy(); + } + + const ctxDoughnut = document.getElementById('chart-doughnut').getContext('2d'); + + const hasEmissionsData = (breakdown.transport + breakdown.energy + breakdown.food + breakdown.waste) > 0; + const doughnutData = hasEmissionsData + ? [breakdown.transport, breakdown.energy, breakdown.food, breakdown.waste] + : [1, 1, 1, 1]; // placeholder equal split if empty inputs + + doughnutChartInstance = new Chart(ctxDoughnut, { + type: 'doughnut', + data: { + labels: ['Transport', 'Energy', 'Food', 'Waste'], + datasets: [{ + data: doughnutData, + backgroundColor: [ + 'rgba(59, 130, 246, 0.75)', // Blue + 'rgba(245, 158, 11, 0.75)', // Amber/Yellow + 'rgba(16, 185, 129, 0.75)', // Emerald + 'rgba(239, 68, 68, 0.75)' // Red + ], + borderColor: [ + 'rgba(59, 130, 246, 1)', + 'rgba(245, 158, 11, 1)', + 'rgba(16, 185, 129, 1)', + 'rgba(239, 68, 68, 1)' + ], + borderWidth: 1.5 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + color: fontColor, + font: { family: 'Inter', size: 12, weight: '500' } + } + }, + tooltip: { + callbacks: { + label: function(context) { + if (!hasEmissionsData) return 'No data logged'; + const val = context.raw; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = Math.round((val / total) * 100); + return `${context.label}: ${val.toFixed(1)} kg CO₂e (${percentage}%)`; + } + } + } + } + } + }); + + // 2. BAR CHART (Detailed Sources) + if (barChartInstance) { + barChartInstance.destroy(); + } + + const ctxBar = document.getElementById('chart-bar').getContext('2d'); + const d = breakdown.detailed; + + // Filter sources that are non-zero to keep the bar chart tidy and relevant + const detailedLabels = [ + 'Car', 'Bike', 'Bus', 'Train', 'Flight', + 'Electricity', 'LPG', 'AC', 'Water', + 'Veg Food', 'Non-Veg Food', 'Dairy Food', + 'Plastic Waste', 'Paper Waste', 'Recycling Offset', 'Compost Offset' + ]; + + const detailedValues = [ + d.car, d.bike, d.bus, d.train, d.flight, + d.electricity, d.lpg, d.ac, d.water, + d.veg, d.nonveg, d.dairy, + d.plastic, d.paper, d.recycledOffset, d.compostOffset + ]; + + // Colors: Red offsets are negative bars, others are gradients + const barColors = detailedValues.map(val => { + if (val < 0) return 'rgba(16, 185, 129, 0.8)'; // Green offset savings + if (val === 0) return 'rgba(255, 255, 255, 0.05)'; + return 'rgba(59, 130, 246, 0.75)'; // Standard blue + }); + + const barBorders = detailedValues.map(val => { + if (val < 0) return 'rgba(16, 185, 129, 1)'; + if (val === 0) return 'rgba(255, 255, 255, 0.1)'; + return 'rgba(59, 130, 246, 1)'; + }); + + barChartInstance = new Chart(ctxBar, { + type: 'bar', + data: { + labels: detailedLabels, + datasets: [{ + label: 'Emissions Contribution (kg CO₂e)', + data: detailedValues, + backgroundColor: barColors, + borderColor: barBorders, + borderWidth: 1.5, + borderRadius: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + callbacks: { + label: function(context) { + const val = context.raw; + if (val < 0) { + return `${context.label}: Saves ${Math.abs(val).toFixed(1)} kg CO₂e`; + } + return `${context.label}: ${val.toFixed(1)} kg CO₂e`; + } + } + } + }, + scales: { + x: { + grid: { display: false }, + ticks: { + color: fontColor, + font: { family: 'Inter', size: 9 }, + maxRotation: 45, + minRotation: 45 + } + }, + y: { + grid: { color: gridColor }, + ticks: { + color: fontColor, + font: { family: 'Inter', size: 10 } + } + } + } + } + }); + + // 3. LINE CHART (Monthly Trend) + if (lineChartInstance) { + lineChartInstance.destroy(); + } + + const ctxLine = document.getElementById('chart-line').getContext('2d'); + + if (state.history.length < 2) { + DOM.lineChartPlaceholderInfo.removeAttribute('hidden'); + DOM.lineChartPlaceholderInfo.style.display = 'block'; + + // Create empty chart container for rendering + lineChartInstance = new Chart(ctxLine, { + type: 'line', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { grid: { display: false }, ticks: { display: false } }, + y: { grid: { display: false }, ticks: { display: false } } + } + } + }); + } else { + DOM.lineChartPlaceholderInfo.setAttribute('hidden', ''); + DOM.lineChartPlaceholderInfo.style.display = 'none'; + + // Sort history chronologically by timestamp/date representation + // History contains items: { date, total, score, timestamp } + const sortedHistory = [...state.history].sort((a, b) => a.timestamp - b.timestamp); + const lineLabels = sortedHistory.map(h => h.date); + const lineEmissions = sortedHistory.map(h => h.total); + const lineScores = sortedHistory.map(h => h.score); + + lineChartInstance = new Chart(ctxLine, { + type: 'line', + data: { + labels: lineLabels, + datasets: [ + { + label: 'Emissions (kg CO₂e)', + data: lineEmissions, + borderColor: 'rgba(59, 130, 246, 1)', + backgroundColor: 'rgba(59, 130, 246, 0.12)', + fill: true, + tension: 0.3, + borderWidth: 3, + pointBackgroundColor: 'rgba(59, 130, 246, 1)', + yAxisID: 'y' + }, + { + label: 'Eco Score', + data: lineScores, + borderColor: 'rgba(16, 185, 129, 1)', + backgroundColor: 'transparent', + borderWidth: 3, + borderDash: [5, 5], + pointBackgroundColor: 'rgba(16, 185, 129, 1)', + yAxisID: 'y1' + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + labels: { + color: fontColor, + font: { family: 'Inter', size: 11, weight: '500' } + } + } + }, + scales: { + x: { + grid: { display: false }, + ticks: { + color: fontColor, + font: { family: 'Inter', size: 10 } + } + }, + y: { + position: 'left', + grid: { color: gridColor }, + ticks: { + color: fontColor, + font: { family: 'Inter', size: 10 } + }, + title: { + display: true, + text: 'kg CO₂e', + color: fontColor, + font: { family: 'Inter', size: 10, weight: '600' } + } + }, + y1: { + position: 'right', + grid: { drawOnChartArea: false }, // only draw grid lines for the emissions scale + min: 0, + max: 100, + ticks: { + color: fontColor, + font: { family: 'Inter', size: 10 } + }, + title: { + display: true, + text: 'Eco Score', + color: fontColor, + font: { family: 'Inter', size: 10, weight: '600' } + } + } + } + } + }); + } +} + +// ========================================================================== +// 11. HISTORY AND SAVING MECHANISMS +// ========================================================================== +function handleFormSubmit(e) { + e.preventDefault(); + + // Validate inputs + let isValid = true; + Object.keys(DOM.inputs).forEach(key => { + const val = parseFloat(DOM.inputs[key].value); + if (!isNaN(val) && val < 0) { + isValid = false; + } + }); + + if (!isValid) { + DOM.validationAlert.removeAttribute('hidden'); + DOM.validationAlert.scrollIntoView({ behavior: 'smooth' }); + return; + } + + const categoryEmissions = getCategoryFootprints(); + const totalEmissions = categoryEmissions.transport + categoryEmissions.energy + categoryEmissions.food + categoryEmissions.waste; + const score = Math.max(0, Math.min(100, Math.round(100 - (totalEmissions / 8.0)))); + + const now = new Date(); + const dateStr = now.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); + const timestamp = now.getTime(); + + // Create entry + const entry = { + id: 'eco-' + timestamp, + date: dateStr, + timestamp: timestamp, + total: totalEmissions, + score: score, + breakdown: { + transport: categoryEmissions.transport, + energy: categoryEmissions.energy, + food: categoryEmissions.food, + waste: categoryEmissions.waste + } + }; + + // Prevent duplicate entries for the same month - replace it if exists or just append + // Here we just append to show monthly trend logs (multiple logs allowed in same month) + state.history.push(entry); + + saveHistoryToStorage(); + runCalculations(); + + // Show visual notification/feedback of success + const btn = DOM.footprintForm.querySelector('button[type="submit"]'); + const originalHtml = btn.innerHTML; + btn.innerHTML = 'Saved Successfully!'; + btn.style.backgroundColor = 'var(--color-excellent)'; + btn.style.color = '#ffffff'; + btn.disabled = true; + + setTimeout(() => { + btn.innerHTML = originalHtml; + btn.style.backgroundColor = ''; + btn.style.color = ''; + btn.disabled = false; + }, 2000); +} + +function renderHistoryTable() { + DOM.historyTableBody.innerHTML = ''; + + if (state.history.length === 0) { + DOM.historyTableBody.innerHTML = ` + + No historical logs found. Save a calculation to track your trends over time. + + `; + return; + } + + // Display logs from newest to oldest + const reversedHistory = [...state.history].sort((a, b) => b.timestamp - a.timestamp); + + reversedHistory.forEach(entry => { + const tr = document.createElement('tr'); + + // Eco Grade styling for table badge + let gradeLabel = 'D'; + let badgeClass = 'bg-grade-poor'; + if (entry.score >= 90) { + gradeLabel = 'A'; + badgeClass = 'bg-grade-excellent'; + } else if (entry.score >= 70) { + gradeLabel = 'B'; + badgeClass = 'bg-grade-good'; + } else if (entry.score >= 50) { + gradeLabel = 'C'; + badgeClass = 'bg-grade-average'; + } + + tr.innerHTML = ` + ${entry.date} + + + Grade ${gradeLabel} (${entry.score}) + + + ${entry.total.toFixed(1)} kg CO₂e + +
+ 🚗 ${Math.round(entry.breakdown.transport)}kg + ⚡ ${Math.round(entry.breakdown.energy)}kg + 🥗 ${Math.round(entry.breakdown.food)}kg + 🗑️ ${Math.round(entry.breakdown.waste)}kg +
+ + + + + `; + + // Bind delete row + tr.querySelector('.btn-delete-row').addEventListener('click', (e) => { + const id = e.currentTarget.getAttribute('data-id'); + deleteHistoryRow(id); + }); + + DOM.historyTableBody.appendChild(tr); + }); +} + +function deleteHistoryRow(id) { + state.history = state.history.filter(h => h.id !== id); + saveHistoryToStorage(); + runCalculations(); +} + +function exportHistoryJSON() { + if (state.history.length === 0) { + alert('No history logs to export.'); + return; + } + + const jsonStr = JSON.stringify(state.history, null, 2); + const blob = new Blob([jsonStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `eco-calc-history-${new Date().toISOString().slice(0,10)}.json`; + document.body.appendChild(a); + a.click(); + + // Cleanup + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function handleResetAll() { + if (!confirm('Are you sure you want to reset all calculations? This will clear current input fields and erase your entire monthly history. This action cannot be undone.')) { + return; + } + + // Clear inputs + Object.keys(DOM.inputs).forEach(key => { + DOM.inputs[key].value = ''; + }); + + state.inputs = {}; + state.history = []; + + try { + localStorage.removeItem('carbonInputs'); + localStorage.removeItem('ecoHistory'); + } catch (error) { + console.error('Error clearing LocalStorage:', error); + } + + // Reset tab selection + switchTab(DOM.tabButtons[0].id); + + // Recalculate + runCalculations(); +} diff --git a/projects/Carbon Footprint Calculator & Eco Dashboard/style.css b/projects/Carbon Footprint Calculator & Eco Dashboard/style.css new file mode 100644 index 0000000..117f03b --- /dev/null +++ b/projects/Carbon Footprint Calculator & Eco Dashboard/style.css @@ -0,0 +1,1232 @@ +/* ========================================================================== + 1. VARIABLES & THEMING + ========================================================================== */ + +:root { + /* Common Font Families */ + --font-display: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Global Animations */ + --transition-fast: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 0.35s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.6s cubic-bezier(0.4, 0, 0.2, 1); + + /* Border Radii */ + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-circle: 50%; +} + +/* Dark Theme (Default) */ +html[data-theme="dark"] { + --bg-gradient-start: #0a0d14; + --bg-gradient-end: #121824; + --text-main: #f8fafc; + --text-muted: #94a3b8; + --text-inverse: #0f172a; + + /* Glassmorphism Colors */ + --glass-bg: rgba(25, 32, 48, 0.45); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-glow: rgba(16, 185, 129, 0.06); + --card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + + /* Colors */ + --primary: #10b981; + --primary-hover: #34d399; + --primary-glow: #10b981; + --secondary: rgba(255, 255, 255, 0.07); + --secondary-hover: rgba(255, 255, 255, 0.12); + --secondary-text: #e2e8f0; + --danger: #ef4444; + --danger-hover: #f87171; + + /* Rating Colors */ + --color-excellent: #10b981; + --color-good: #3b82f6; + --color-average: #f59e0b; + --color-poor: #ef4444; + + /* Inputs */ + --input-bg: rgba(15, 20, 31, 0.6); + --input-border: rgba(255, 255, 255, 0.12); + --input-focus-border: #10b981; + --input-text: #f8fafc; + + /* Misc */ + --header-bg: rgba(10, 13, 20, 0.75); + --table-border: rgba(255, 255, 255, 0.06); + --table-stripe: rgba(255, 255, 255, 0.02); +} + +/* Light Theme Toggle */ +html[data-theme="light"] { + --bg-gradient-start: #f1f5f9; + --bg-gradient-end: #e2e8f0; + --text-main: #0f172a; + --text-muted: #64748b; + --text-inverse: #ffffff; + + /* Glassmorphism Colors */ + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(15, 23, 42, 0.08); + --glass-glow: rgba(16, 185, 129, 0.04); + --card-shadow: 0 8px 32px 0 rgba(15, 23, 42, 0.08); + + /* Colors */ + --primary: #059669; + --primary-hover: #10b981; + --primary-glow: #059669; + --secondary: rgba(15, 23, 42, 0.05); + --secondary-hover: rgba(15, 23, 42, 0.08); + --secondary-text: #334155; + --danger: #dc2626; + --danger-hover: #ef4444; + + /* Rating Colors */ + --color-excellent: #059669; + --color-good: #2563eb; + --color-average: #d97706; + --color-poor: #dc2626; + + /* Inputs */ + --input-bg: rgba(255, 255, 255, 0.9); + --input-border: rgba(15, 23, 42, 0.15); + --input-focus-border: #059669; + --input-text: #0f172a; + + /* Misc */ + --header-bg: rgba(241, 245, 249, 0.85); + --table-border: rgba(15, 23, 42, 0.06); + --table-stripe: rgba(15, 23, 42, 0.02); +} + +/* ========================================================================== + 2. RESET & BASE + ========================================================================== */ + +*, *::before, *::after { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-body); + background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); + background-attachment: fixed; + color: var(--text-main); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 100vh; +} + +/* Accessibility: Skip Links & Focus outline styling */ +.skip-link { + position: absolute; + top: -100px; + left: 0; + background: var(--primary); + color: var(--text-inverse); + padding: 10px 20px; + font-weight: 600; + border-radius: 0 0 var(--radius-sm) 0; + z-index: 10000; + transition: top var(--transition-fast); +} + +.skip-link:focus { + top: 0; + outline: 3px solid var(--text-main); +} + +:focus-visible { + outline: 3px solid var(--primary); + outline-offset: 4px; +} + +input, button, select, textarea { + font-family: inherit; +} + +/* Custom Scrollbar for modern touch */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--input-border); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* ========================================================================== + 3. LAYOUT & STRUCTURE + ========================================================================== */ + +.app-header { + position: sticky; + top: 0; + z-index: 1000; + background: var(--header-bg); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--glass-border); + padding: 14px 20px; + transition: background var(--transition-normal), border var(--transition-normal); +} + +.header-container { + max-width: 1400px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; +} + +.logo-area { + display: flex; + align-items: center; + gap: 12px; +} + +.logo-icon { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + color: var(--text-inverse); + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: var(--radius-md); + box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3); +} + +.logo-area h1 { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 800; + margin: 0; + line-height: 1.1; + letter-spacing: -0.5px; +} + +.logo-area .subtitle { + font-size: 0.78rem; + color: var(--text-muted); + margin: 2px 0 0 0; + font-weight: 500; + letter-spacing: 0.2px; +} + +.header-controls { + display: flex; + align-items: center; + gap: 16px; +} + +.slogan-badge { + font-size: 0.85rem; + color: var(--primary); + font-weight: 600; + background: var(--glass-glow); + padding: 6px 14px; + border-radius: 50px; + border: 1px solid var(--glass-border); + text-align: center; + max-width: 320px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.theme-toggle-btn { + background: var(--secondary); + border: 1px solid var(--glass-border); + color: var(--text-main); + width: 40px; + height: 40px; + border-radius: var(--radius-md); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background var(--transition-fast), transform var(--transition-fast); +} + +.theme-toggle-btn:hover { + background: var(--secondary-hover); + transform: translateY(-2px); +} + +html[data-theme="dark"] .sun-icon { display: block; } +html[data-theme="dark"] .moon-icon { display: none; } +html[data-theme="light"] .sun-icon { display: none; } +html[data-theme="light"] .moon-icon { display: block; } + +/* Main App Container */ +.app-container { + max-width: 1400px; + margin: 0 auto; + padding: 24px 20px 60px 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* ========================================================================== + 4. COMPONENTS + ========================================================================== */ + +/* Glass Panels */ +.glass-panel { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + box-shadow: var(--card-shadow); + padding: 24px; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + position: relative; + overflow: hidden; + transition: transform var(--transition-normal), box-shadow var(--transition-normal); +} + +.glass-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, transparent, var(--glass-glow), transparent); + pointer-events: none; +} + +/* Hero Section Styles */ +.hero-section { + display: flex; + justify-content: space-between; + align-items: center; + gap: 40px; +} + +.hero-content h2 { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 700; + margin: 0 0 10px 0; + letter-spacing: -0.5px; +} + +.hero-content p { + color: var(--text-muted); + margin: 0; + max-width: 800px; +} + +.hero-quick-stats { + display: flex; + gap: 20px; + flex-shrink: 0; +} + +.quick-stat-item { + display: flex; + flex-direction: column; + align-items: flex-end; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--glass-border); + padding: 12px 20px; + border-radius: var(--radius-md); + text-align: right; + min-width: 140px; +} + +.qs-val { + font-family: var(--font-display); + font-size: 1.8rem; + font-weight: 800; + color: var(--primary); + line-height: 1.2; +} + +.qs-label { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 600; + text-transform: uppercase; + margin-top: 4px; +} + +/* Dashboard Grid System */ +.dashboard-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + align-items: start; +} + +.results-dashboard-column { + display: flex; + flex-direction: column; + gap: 24px; +} + +.section-title { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-display); + font-size: 1.35rem; + font-weight: 700; + margin: 0 0 20px 0; +} + +.section-title svg { + color: var(--primary); +} + +.section-subtitle { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-display); + font-size: 1.2rem; + font-weight: 700; + margin: 0; +} + +.card-subtitle-small { + font-family: var(--font-display); + font-size: 0.95rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + letter-spacing: 0.5px; + margin: 0 0 16px 0; +} + +/* Navigation Tabs */ +.tabs-list { + display: flex; + gap: 8px; + background: var(--input-bg); + border: 1px solid var(--glass-border); + padding: 6px; + border-radius: var(--radius-md); + margin-bottom: 24px; + overflow-x: auto; +} + +.tab-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + background: transparent; + border: none; + color: var(--text-muted); + padding: 10px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.tab-btn:hover { + color: var(--text-main); + background: rgba(255, 255, 255, 0.03); +} + +.tab-btn.active { + background: var(--primary); + color: var(--text-inverse); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2); +} + +/* Forms & Inputs */ +.tab-panel { + display: none; + animation: fadeIn 0.4s ease-in-out forwards; +} + +.tab-panel.active { + display: block; +} + +.panel-header { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 6px 0; +} + +.panel-desc { + font-size: 0.85rem; + color: var(--text-muted); + margin: 0 0 20px 0; +} + +.form-group { + margin-bottom: 18px; + display: flex; + flex-direction: column; +} + +.form-group-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-group label { + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 6px; + color: var(--text-main); +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.input-wrapper input { + width: 100%; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius-sm); + padding: 12px 64px 12px 16px; + color: var(--input-text); + font-size: 1rem; + font-weight: 500; + transition: border var(--transition-fast), box-shadow var(--transition-fast); +} + +.input-wrapper input:focus { + outline: none; + border-color: var(--input-focus-border); + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15); +} + +.input-wrapper .unit { + position: absolute; + right: 16px; + font-size: 0.85rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + pointer-events: none; +} + +.input-help { + font-size: 0.78rem; + color: var(--text-muted); + margin-top: 4px; +} + +/* Form Action Buttons */ +.form-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 28px; + border-top: 1px solid var(--glass-border); + padding-top: 20px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 20px; + border-radius: var(--radius-sm); + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast); + border: 1px solid transparent; + white-space: nowrap; +} + +.btn:active { + transform: scale(0.98); +} + +.btn-primary { + background: var(--primary); + color: var(--text-inverse); +} + +.btn-primary:hover { + background: var(--primary-hover); + box-shadow: 0 4px 14px rgba(16, 185, 129, 0.35); +} + +.btn-secondary { + background: var(--secondary); + color: var(--secondary-text); + border-color: var(--glass-border); +} + +.btn-secondary:hover { + background: var(--secondary-hover); +} + +.btn-secondary:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.btn-danger { + background: var(--danger); + color: var(--text-inverse); +} + +.btn-danger:hover { + background: var(--danger-hover); + box-shadow: 0 4px 14px rgba(239, 68, 68, 0.25); +} + +.btn-sm { + padding: 8px 14px; + font-size: 0.8rem; +} + +/* Error banner styling */ +.validation-banner { + background: rgba(239, 68, 68, 0.12); + border: 1px solid rgba(239, 68, 68, 0.25); + color: var(--danger); + padding: 12px 16px; + border-radius: var(--radius-sm); + font-size: 0.85rem; + font-weight: 600; + margin-top: 14px; + text-align: center; +} + +/* Score display and metric styling */ +.dashboard-scores-row { + display: grid; + grid-template-columns: 1fr 1.2fr; + gap: 20px; +} + +/* Score Circle Styling */ +.score-card { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + justify-content: space-between; +} + +.progress-ring-container { + position: relative; + width: 160px; + height: 160px; +} + +.progress-ring-fill { + transition: stroke-dashoffset var(--transition-slow); +} + +.progress-text-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; +} + +.score-number { + font-family: var(--font-display); + font-size: 2.6rem; + font-weight: 800; + line-height: 1; +} + +.score-grade { + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; + margin-top: 4px; + letter-spacing: 0.5px; +} + +.score-caption { + font-size: 0.85rem; + color: var(--text-muted); + margin: 12px 0 0 0; + font-weight: 500; +} + +/* Emissions Card Details */ +.emissions-total-card { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.emissions-number-wrapper { + margin: 16px 0; +} + +.emissions-val { + font-family: var(--font-display); + font-size: 3.2rem; + font-weight: 800; + color: var(--primary); + line-height: 1.1; + letter-spacing: -1px; +} + +.emissions-unit { + font-size: 1rem; + font-weight: 600; + color: var(--text-muted); + margin-left: 6px; +} + +.impact-comparison { + margin-top: auto; +} + +.comparison-bar-bg { + height: 6px; + background: var(--secondary); + border-radius: 3px; + overflow: hidden; + margin-bottom: 8px; +} + +.comparison-bar-fill { + height: 100%; + background: var(--primary); + border-radius: 3px; + width: 0%; + transition: width var(--transition-slow), background var(--transition-normal); +} + +.comparison-text { + font-size: 0.82rem; + color: var(--text-muted); + font-weight: 500; +} + +/* Category grid dashboard cards */ +.categories-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.flex-card { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 20px; +} + +.card-icon-tag { + font-size: 1.8rem; + line-height: 1; +} + +.card-info h4 { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + margin: 0; + letter-spacing: 0.3px; +} + +.card-info .stat-number { + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: 700; + margin: 2px 0 0 0; + line-height: 1.2; +} + +.card-info .stat-number small { + font-size: 0.75rem; + font-weight: 500; + color: var(--text-muted); +} + +.percentage-pill { + margin-left: auto; + font-size: 0.78rem; + font-weight: 700; + background: var(--secondary); + border: 1px solid var(--glass-border); + padding: 4px 8px; + border-radius: 20px; + color: var(--text-main); +} + +/* Visual analytics / Charts wrapper styling */ +.charts-section { + display: flex; + flex-direction: column; +} + +.charts-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + gap: 16px; +} + +.chart-toggles { + display: flex; + background: var(--secondary); + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + padding: 3px; +} + +.chart-toggle-btn { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 0.8rem; + font-weight: 600; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + transition: color var(--transition-fast), background var(--transition-fast); +} + +.chart-toggle-btn:hover { + color: var(--text-main); +} + +.chart-toggle-btn.active { + background: var(--primary); + color: var(--text-inverse); +} + +.chart-views-container { + height: 250px; + position: relative; +} + +.chart-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-normal); + display: flex; + align-items: center; + justify-content: center; +} + +.chart-wrapper.active { + opacity: 1; + pointer-events: auto; +} + +.chart-wrapper canvas { + max-width: 100% !important; + max-height: 100% !important; +} + +.chart-fallback { + font-size: 0.88rem; + color: var(--text-muted); + font-weight: 500; + text-align: center; + padding: 20px; +} + +/* Eco Recommendations styling */ +.recommendations-section { + display: flex; + flex-direction: column; +} + +.recs-container { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 14px; +} + +.rec-card { + display: flex; + gap: 14px; + background: rgba(255, 255, 255, 0.015); + border: 1px solid var(--glass-border); + padding: 14px 16px; + border-radius: var(--radius-md); + animation: slideUp 0.3s ease-out forwards; +} + +.rec-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.rec-text h4 { + font-size: 0.92rem; + font-weight: 600; + margin: 0 0 3px 0; +} + +.rec-text p { + font-size: 0.82rem; + color: var(--text-muted); + margin: 0; +} + +.empty-recs { + font-size: 0.88rem; + color: var(--text-muted); + text-align: center; + padding: 20px 0; + font-weight: 500; +} + +/* History lists & actions */ +.history-section { + display: flex; + flex-direction: column; +} + +.history-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 18px; + gap: 16px; +} + +.history-actions { + display: flex; + gap: 10px; +} + +.history-table-wrapper { + overflow-x: auto; +} + +.history-table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: 0.88rem; +} + +.history-table th { + font-weight: 600; + color: var(--text-muted); + border-bottom: 2px solid var(--table-border); + padding: 12px 16px; +} + +.history-table td { + padding: 14px 16px; + border-bottom: 1px solid var(--table-border); + vertical-align: middle; +} + +.history-table tbody tr:nth-child(even) { + background: var(--table-stripe); +} + +.history-table tbody tr:hover { + background: rgba(255, 255, 255, 0.01); +} + +.table-empty { + text-align: center; + color: var(--text-muted); + padding: 40px !important; + font-weight: 500; +} + +.history-grade-badge { + display: inline-block; + font-size: 0.78rem; + font-weight: 700; + padding: 3px 10px; + border-radius: 12px; + text-transform: uppercase; +} + +.history-breakdown-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.breakdown-badge { + font-size: 0.75rem; + font-weight: 500; + background: var(--secondary); + border: 1px solid var(--glass-border); + padding: 2px 6px; + border-radius: 4px; + color: var(--text-muted); +} + +.btn-delete-row { + background: transparent; + border: none; + color: var(--danger); + font-size: 1.1rem; + cursor: pointer; + padding: 4px 8px; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + transition: background var(--transition-fast); +} + +.btn-delete-row:hover { + background: rgba(239, 68, 68, 0.1); +} + +/* App Footer info */ +.app-footer { + border-top: 1px solid var(--glass-border); + padding: 30px 20px; + background: var(--header-bg); + text-align: center; + margin-top: auto; +} + +.footer-container { + max-width: 1400px; + margin: 0 auto; +} + +.app-footer p { + margin: 0; + font-size: 0.85rem; + color: var(--text-muted); +} + +.app-footer p + p { + margin-top: 6px; +} + +.app-footer a { + color: var(--primary); + text-decoration: none; + font-weight: 600; + transition: color var(--transition-fast); +} + +.app-footer a:hover { + color: var(--primary-hover); + text-decoration: underline; +} + +.footer-note { + font-size: 0.75rem !important; + opacity: 0.8; +} + +/* ========================================================================== + 5. UTILITIES, CLASSES & ANIMATIONS + ========================================================================== */ + +/* Color grades */ +.grade-excellent { color: var(--color-excellent) !important; } +.grade-good { color: var(--color-good) !important; } +.grade-average { color: var(--color-average) !important; } +.grade-poor { color: var(--color-poor) !important; } + +.bg-grade-excellent { background: rgba(16, 185, 129, 0.12); color: var(--color-excellent); } +.bg-grade-good { background: rgba(59, 130, 246, 0.12); color: var(--color-good); } +.bg-grade-average { background: rgba(245, 158, 11, 0.12); color: var(--color-average); } +.bg-grade-poor { background: rgba(239, 68, 68, 0.12); color: var(--color-poor); } + +/* Animation Keyframes */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ========================================================================== + 6. RESPONSIVE BREAKPOINTS (320px, 375px, 768px, 1024px, 1440px) + ========================================================================== */ + +/* 1440px+ (Widescreen enhancements) */ +@media (min-width: 1440px) { + .app-container { + padding-left: 0; + padding-right: 0; + } +} + +/* 1024px to 1439px */ +@media (max-width: 1439px) { + .app-container { + max-width: 100%; + } +} + +/* 768px to 1023px (Tablets) */ +@media (max-width: 1023px) { + .dashboard-grid { + grid-template-columns: 1fr; + } + + .hero-section { + flex-direction: column; + align-items: stretch; + text-align: center; + gap: 20px; + } + + .hero-quick-stats { + justify-content: center; + } +} + +/* 375px to 767px (Large Mobiles) */ +@media (max-width: 767px) { + .dashboard-scores-row { + grid-template-columns: 1fr; + } + + .form-group-row { + grid-template-columns: 1fr; + gap: 0; + } + + .categories-grid { + grid-template-columns: 1fr; + } + + .charts-header { + flex-direction: column; + align-items: flex-start; + } + + .chart-toggles { + width: 100%; + } + + .chart-toggle-btn { + flex: 1; + text-align: center; + } + + .header-container { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .header-controls { + justify-content: space-between; + } + + .slogan-badge { + max-width: 80%; + } + + .history-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .history-actions { + width: 100%; + } + + .history-actions button { + flex: 1; + } +} + +/* 320px to 374px (Small Mobiles) */ +@media (max-width: 374px) { + .hero-quick-stats { + flex-direction: column; + gap: 10px; + } + + .quick-stat-item { + align-items: center; + text-align: center; + } + + .tabs-list { + gap: 4px; + padding: 4px; + } + + .tab-btn { + padding: 8px 10px; + font-size: 0.8rem; + } + + .app-header { + padding: 10px; + } + + .logo-icon { + width: 36px; + height: 36px; + } + + .logo-area h1 { + font-size: 1.25rem; + } + + .slogan-badge { + display: none; /* Hide slogan badge on extremely tiny screen to conserve vital spaces */ + } +} diff --git a/src/app/live/[...slug]/route.js b/src/app/live/[...slug]/route.js index 57e4461..43240d3 100644 --- a/src/app/live/[...slug]/route.js +++ b/src/app/live/[...slug]/route.js @@ -12,8 +12,8 @@ export async function GET(request, { params }) { const resolvedParams = await params; const slugArray = resolvedParams.slug; - // Validate slug contains only safe characters (alphanumeric, hyphens, underscores, dots) - const safeSlug = slugArray.map(s => s.replace(/[^a-zA-Z0-9\-_.]/g, '')); + // Validate slug contains only safe characters (alphanumeric, hyphens, underscores, dots, spaces, ampersands, parentheses) + const safeSlug = slugArray.map(s => s.replace(/[^a-zA-Z0-9\-_.\s&()]/g, '')); // Prevent any empty segments or path traversal attempts if (slugArray.some((s, i) => s !== safeSlug[i] || s === '..')) {