import { LitElement, html, svg } from "lit"; import { property, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { styles, HOT_DESIGN_SYSTEM_URL } from "./tool-menu.styles.js"; import { translations } from "./translations.js"; // Import tools icons. These icons will be added in the near future, and are not being showed as default. import droneIcon from "../assets/icon-drone.svg"; import oamIcon from "../assets/icon-oam.svg"; import tmIcon from "../assets/icon-tm.svg"; import fairIcon from "../assets/icon-fair.svg"; import fieldIcon from "../assets/icon-field.svg"; import chatmapIcon from "../assets/icon-chatmap.svg"; import exportIcon from "../assets/icon-export.svg"; import umapIcon from "../assets/icon-umap.svg"; interface Tool { id: string; title: string; href: string; icon: string; section: "imagery" | "mapping" | "field" | "data"; } interface Section { id: "imagery" | "mapping" | "field" | "data"; } // Section IDs (titles will be translated) const SECTIONS: Section[] = [ { id: "imagery" }, { id: "mapping" }, { id: "field" }, { id: "data" }, ]; const TOOLS_DATA: Tool[] = [ { id: "drone", title: "Drone Tasking Manager", href: "https://dronetm.org/", icon: droneIcon, section: "imagery", }, { id: "oam", title: "OpenAerialMap", href: "https://openaerialmap.org/", icon: oamIcon, section: "imagery", }, { id: "tasking-manager", title: "Tasking Manager", href: "https://tasks.hotosm.org/", icon: tmIcon, section: "mapping", }, { id: "fair", title: "fAIr", href: "https://fair.hotosm.org/", icon: fairIcon, section: "mapping", }, { id: "field", title: "Field Tasking Manager", href: "https://fmtm.hotosm.org/", icon: fieldIcon, section: "field", }, { id: "chat-map", title: "ChatMap", href: "https://chatmap.hotosm.org", icon: chatmapIcon, section: "field", }, { id: "export-tool", title: "Export Tool", href: "https://export.hotosm.org/", icon: exportIcon, section: "data", }, { id: "umap", title: "uMap", href: "https://umap.hotosm.org/", icon: umapIcon, section: "data", }, ]; export class HotToolMenu extends LitElement { static styles = styles; @property({ type: Boolean, attribute: "show-logos", reflect: false }) showLogos = false; // Language code (en, es, fr, pt, etc.) @property({ type: String }) lang = "en"; @state() private isOpen = false; private tools: Tool[] = TOOLS_DATA; /** * Get translated string for the current language * Falls back to English if translation not found */ private t(key: keyof typeof translations.en): string { const langTranslations = translations[this.lang] || translations.en; return langTranslations[key] || translations.en[key] || key; } private getToolHref(tool: Tool): string { const hostname = window.location.hostname; // Detect environment: local (.test), dev (dev.portal), or production const isLocal = hostname.endsWith('.test'); const isDev = hostname.includes('dev.portal'); if (!isLocal && !isDev) { return tool.href; // Production URLs } // Local environment URLs (.hotosm.test) const localUrls: Record = { 'drone': 'https://dronetm.hotosm.test', 'oam': 'https://openaerialmap.hotosm.test', 'tasking-manager': 'https://tasks.hotosm.test', 'fair': 'https://fair.hotosm.test', 'field': 'https://fmtm.hotosm.test', 'chat-map': 'https://chatmap.hotosm.test', 'export-tool': 'https://export-tool.hotosm.test', 'umap': 'https://umap.hotosm.test', }; // Dev environment URLs (testlogin.*) const devUrls: Record = { 'drone': 'https://dronetm.testlogin.hotosm.org', 'oam': 'https://openaerialmap.org', 'tasking-manager': 'https://tasks.hotosm.org', 'fair': 'https://fair.testlogin.hotosm.org', 'field': 'https://fmtm.hotosm.org', 'chat-map': 'https://chatmap-dev.hotosm.org', 'export-tool': 'https://export.testlogin.hotosm.org', 'umap': 'https://umap.testlogin.hotosm.org', }; const urls = isLocal ? localUrls : devUrls; return urls[tool.id] || tool.href; } private getToolsBySection(sectionId: string): Tool[] { return this.tools.filter((t) => t.section === sectionId); } private toggleDropdown() { this.isOpen = !this.isOpen; if (this.isOpen) { // Defer adding the listener so the current click event finishes bubbling // before we start listening for outside clicks setTimeout(() => { document.addEventListener("click", this.handleOutsideClick); }, 0); } else { document.removeEventListener("click", this.handleOutsideClick); } } private closeDropdown() { this.isOpen = false; document.removeEventListener("click", this.handleOutsideClick); } private handleToolClick(tool: Tool) { // Dispatch custom event for external handling if needed this.dispatchEvent( new CustomEvent("tool-selected", { detail: { tool }, bubbles: true, composed: true, }) ); // Open tool page in new tab with environment-specific URL const href = this.getToolHref(tool); window.open(href, "_blank", "noopener,noreferrer"); // Close dropdown after selection this.closeDropdown(); } private handleOutsideClick = (event: MouseEvent) => { if (!this.contains(event.target as Node)) { this.closeDropdown(); } }; override createRenderRoot() { const root = super.createRenderRoot() as ShadowRoot; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = HOT_DESIGN_SYSTEM_URL; root.appendChild(link); return root; } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener('click', this.handleOutsideClick); } render() { return html` ${SECTIONS.map((section, sectionIndex) => { const tools = this.getToolsBySection(section.id); if (tools.length === 0) return ""; return html` ${this.t(section.id)} ${tools.map( (tool) => html` this.handleToolClick(tool)} > ${this.showLogos ? html`` : ""} ${tool.title} ` )} `; })} `; } } export default HotToolMenu;