diff --git a/.gitignore b/.gitignore index 8ac9df1a..f6e1e6ba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ vite.config.ts.* test-results playwright-report attached_assets +.env diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index 8ae7fdb6..7a07f441 100644 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -1,4 +1,14 @@ -import { Search, Moon, Sun, Menu, X } from "lucide-react"; +import { + Search, + Moon, + Sun, + X, + Play, + Pause, + PanelLeft, + ChevronLeft, + ChevronRight, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -8,18 +18,33 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useTheme } from "@/providers/theme-provider"; -import { Link } from "wouter"; import { useSearch } from "@/hooks/use-search"; import { SearchResults } from "@/components/ui/search-results"; import { getToolsCount } from "@/data/tools"; import { useState, useRef, useEffect } from "react"; +import { useDemo } from "@/hooks/use-demo-hook"; interface HeaderProps { onMenuClick: () => void; } export function Header({ onMenuClick }: HeaderProps) { + const searchInputRef = useRef(null); + const { theme, setTheme } = useTheme(); + const { + isDemoRunning, + isDemoPaused, + startDemo, + stopDemo, + pauseDemo, + resumeDemo, + skipToNext, + skipToPrevious, + demoSpeed, + setDemoSpeed, + } = useDemo(); + const { searchQuery, setSearchQuery, @@ -29,28 +54,18 @@ export function Header({ onMenuClick }: HeaderProps) { selectResult, resetSelection, } = useSearch(); + const [showResults, setShowResults] = useState(false); - const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false); const searchRef = useRef(null); const toggleTheme = () => { - if (theme === "dark") { - setTheme("light"); - } else { - setTheme("dark"); - } + setTheme(theme === "dark" ? "light" : "dark"); }; const handleSearchChange = (value: string) => { setSearchQuery(value); setShowResults(value.trim().length > 0); - resetSelection(); // Reset selection when search changes - }; - - const handleResultClick = () => { - setShowResults(false); - setSearchQuery(""); - setIsMobileSearchOpen(false); + resetSelection(); }; const clearSearch = () => { @@ -59,32 +74,6 @@ export function Header({ onMenuClick }: HeaderProps) { resetSelection(); }; - const focusSearch = () => { - const searchInput = document.querySelector( - '[data-testid="search-input"]' - ) as HTMLInputElement; - if (searchInput) { - searchInput.focus(); - searchInput.select(); - } - }; - - // Close search results when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - searchRef.current && - !searchRef.current.contains(event.target as Node) - ) { - setShowResults(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - // Handle keyboard navigation in search const handleSearchKeyDown = ( event: React.KeyboardEvent ) => { @@ -103,9 +92,9 @@ export function Header({ onMenuClick }: HeaderProps) { event.preventDefault(); const selected = selectResult(); if (selected) { - // Navigate to the selected result window.location.href = selected.path; - handleResultClick(); + setShowResults(false); + setSearchQuery(""); } break; } @@ -113,234 +102,291 @@ export function Header({ onMenuClick }: HeaderProps) { event.preventDefault(); setShowResults(false); resetSelection(); - (event.target as HTMLInputElement).blur(); break; - default: { - // Handle default case - } + + default: + break; } }; - // Add Ctrl+S keyboard shortcut for search useEffect(() => { - const handleKeydown = (event: KeyboardEvent) => { - if (event.ctrlKey && event.key === "s") { - event.preventDefault(); - focusSearch(); + const handler = () => { + searchInputRef.current?.focus(); + }; + + window.addEventListener("focus-search", handler); + return () => window.removeEventListener("focus-search", handler); + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + searchRef.current && + !searchRef.current.contains(event.target as Node) + ) { + setShowResults(false); } }; - document.addEventListener("keydown", handleKeydown); - return () => document.removeEventListener("keydown", handleKeydown); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); }, []); return ( -
-
-
- {/* Logo and Title - Always leftmost */} -
-
- {/* Blue FD Logo - toggles menu */} -
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onMenuClick(); - } - }} +
+ {/* LEFT: Menu + Search */} +
+ {/* Menu Button */} + {!isDemoRunning && ( + + +
- - {/* Text Logo - links to homepage */} -
- -

- FreeDevTool.App -

- -

- - Secure - {" "} - Developer Tools -

-
-
-
- - {/* Search and Actions */} -
- {/* Desktop Search */} -
- - handleSearchChange(e.target.value)} - onKeyDown={handleSearchKeyDown} - className="pl-10 pr-8 w-64 bg-slate-50 dark:bg-slate-700 border-slate-300 dark:border-slate-600" - data-testid="search-input" - onFocus={() => setShowResults(searchQuery.trim().length > 0)} - /> - {searchQuery ? ( - - - - - -

Clear Search

-
-
- ) : null} - {showResults ? ( - - ) : null} -
+ + + + + Toggle Menu (Ctrl+M) + + + )} - {/* Mobile Search Toggle */} - - - - - -

Open Search (Ctrl+S)

-
-
- - {/* Theme Toggle */} - - - - - -

Switch to {theme === "dark" ? "Light" : "Dark"} Mode

-
-
- - {/* Hamburger Menu - Always visible */} - - - - - -

Toggle Menu (Ctrl+M)

-
-
-
-
- - {/* Mobile Search Bar */} - {isMobileSearchOpen ? ( + {/* Search */} + {!isDemoRunning && (
-
- - handleSearchChange(e.target.value)} - onKeyDown={handleSearchKeyDown} - className="pl-10 pr-8 w-full bg-slate-50 dark:bg-slate-700 border-slate-300 dark:border-slate-600" - data-testid="mobile-search-input" - autoFocus - /> - {searchQuery ? ( - - - - - -

Clear Search

-
-
- ) : null} -
+ + handleSearchChange(e.target.value)} + onKeyDown={handleSearchKeyDown} + onFocus={() => setShowResults(searchQuery.trim().length > 0)} + className=" + pl-10 pr-8 rounded-lg + bg-slate-100 text-slate-900 placeholder-slate-500 + border border-slate-300 + dark:bg-slate-800 dark:text-slate-100 + dark:placeholder-slate-400 dark:border-slate-700 + " + /> + {searchQuery ? ( + + ) : null} + {showResults ? ( { + setShowResults(false); + setSearchQuery(""); + }} /> ) : null}
+ )} +
+ + {/* RIGHT: Demo + Theme */} +
+ {/* Demo Tour */} + {!isDemoRunning && ( + + )} + + {/* DEMO CONTROLS */} + {isDemoRunning ? ( +
+ + Demo Mode Active + + {/* PREVIOUS */} + + + {/* PLAY / PAUSE */} + {!isDemoPaused ? ( + + ) : ( + + )} + + {/* NEXT */} + + + {/* DEMO SPEED */} +
+ +
+ + {/* STOP */} + +
) : null} + + {/* Theme Toggle */} + + + + + +

Toggle Theme (Ctrl+D)

+
+
diff --git a/client/src/components/layout/Layout.tsx b/client/src/components/layout/Layout.tsx index dde2ee42..2b9a0132 100644 --- a/client/src/components/layout/Layout.tsx +++ b/client/src/components/layout/Layout.tsx @@ -1,7 +1,12 @@ -import { useState, useEffect } from "react"; -import { useLocation } from "wouter"; +import { useState, useEffect, useMemo } from "react"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; +import { ChevronUp, ChevronDown } from "lucide-react"; +import { useDemo } from "@/hooks/use-demo-hook"; +import { useTheme } from "@/providers/theme-provider"; +import { useLocation } from "wouter"; +import { toolsData } from "@/data/tools"; + import { Sheet, SheetContent, @@ -9,513 +14,193 @@ import { SheetTitle, SheetDescription, } from "@/components/ui/sheet"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, - TooltipTrigger, } from "@/components/ui/tooltip"; -import { - ChevronUp, - Square, - SkipForward, - SkipBack, - Timer, - Play, - Pause, -} from "lucide-react"; -import { toolsData, getToolByPath } from "@/data/tools"; -import { useDemo } from "@/hooks/use-demo-hook"; -import { useTheme } from "@/providers/theme-provider"; -import { HOMEPAGE_TITLE, getToolPageTitle } from "@shared/page-title"; - -interface LayoutProps { - children: React.ReactNode; -} +import { TooltipTrigger } from "@radix-ui/react-tooltip"; -export function Layout({ children }: LayoutProps) { - const [location, setLocation] = useLocation(); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const [headerCollapsed, setHeaderCollapsed] = useState(false); - const [desktopSidebarVisible, setDesktopSidebarVisible] = useState(true); +export function Layout({ children }: { children: React.ReactNode }) { + const { isDemoRunning } = useDemo(); + const { theme, setTheme } = useTheme(); + const [location] = useLocation(); - // Determine if sidebar should be shown by default (only on homepage and desktop) - const isHomepage = location === "/"; - const isToolPage = location.startsWith("/tools/"); - const shouldShowSidebarByDefault = isHomepage && !isToolPage; - const { - isDemoRunning, - isDemoPaused, - currentDemoTool, - demoProgress, - demoSpeed, - stopDemo, - skipToNext, - skipToPrevious, - setDemoSpeed, - pauseDemo, - resumeDemo, - } = useDemo(); + const allTools = useMemo( + () => Object.values(toolsData).flatMap(category => category.tools), + [] + ); - const { theme, setTheme } = useTheme(); + const [isSidebarOpen] = useState(true); - // Mobile fix: Blur CodeMirror when touching anywhere outside the editor - // This prevents the focus lock issue on mobile devices - useEffect(() => { - const blurCodeMirror = () => { - // Blur any focused CodeMirror editors - const focusedEditors = document.querySelectorAll(".cm-editor.cm-focused"); - focusedEditors.forEach(editor => { - const contentArea = editor.querySelector(".cm-content") as HTMLElement; - if (contentArea) { - contentArea.blur(); - } - }); - // Also blur any active element that might be inside CodeMirror - if (document.activeElement instanceof HTMLElement) { - const isInEditor = document.activeElement.closest(".cm-editor"); - if (isInEditor) { - document.activeElement.blur(); - } - } - }; + const [headerCollapsed, setHeaderCollapsed] = useState(false); - const handlePointerDown = (event: PointerEvent) => { - const target = event.target as HTMLElement; + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - // If touch/click is NOT inside a CodeMirror editor, blur any focused editors - const isInsideEditor = target.closest(".cm-editor"); - if (!isInsideEditor) { - blurCodeMirror(); - } - }; + const [isDesktop, setIsDesktop] = useState(() => { + if (typeof window === "undefined") return true; + return window.innerWidth >= 1024; + }); - // Use pointerdown for better cross-device support (touch and mouse) - document.addEventListener("pointerdown", handlePointerDown, { - passive: true, - }); - return () => document.removeEventListener("pointerdown", handlePointerDown); - }, []); + useEffect(() => { + // Desktop pe sirf homepage par sidebar expanded rahe + if (isDesktop) { + setSidebarCollapsed(location !== "/"); + } + }, [location, isDesktop]); - // Update document title based on current location useEffect(() => { - if (location === "/") { - document.title = HOMEPAGE_TITLE; - } else if (location.startsWith("/tools/")) { - const tool = getToolByPath(location); - if (tool) { - document.title = getToolPageTitle(tool); - } + if (isDesktop && mobileMenuOpen) { + setMobileMenuOpen(false); } - }, [location]); + }, [isDesktop, mobileMenuOpen]); - // Global keyboard shortcut handler useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // Only handle if no input field is focused - if (event.target && (event.target as HTMLElement).tagName === "INPUT") - return; + const handler = (event: KeyboardEvent) => { + if (isDemoRunning) return; - // Handle Escape key to close menu - if (event.key === "Escape" && mobileMenuOpen) { - event.preventDefault(); - setMobileMenuOpen(false); - return; - } + if (!event.ctrlKey) return; - if (event.ctrlKey) { - // Handle theme toggle with Ctrl+D (case-sensitive check) - if (event.key === "d" || event.key === "D") { + switch (event.key.toLowerCase()) { + case "d": event.preventDefault(); - event.stopPropagation(); setTheme(theme === "dark" ? "light" : "dark"); - return; - } + break; - // Handle menu toggle with Ctrl+M (case-sensitive check) - if (event.key === "m" || event.key === "M") { + case "m": event.preventDefault(); - event.stopPropagation(); + setSidebarCollapsed(prev => !prev); + break; - // Toggle appropriate menu based on current context - if (shouldShowSidebarByDefault) { - // On homepage, toggle desktop sidebar on large screens, mobile menu on small screens - const isLargeScreen = window.innerWidth >= 1024; // lg breakpoint - if (isLargeScreen) { - setDesktopSidebarVisible(!desktopSidebarVisible); - } else { - toggleStateOfMobileMenu(mobileMenuOpen, setMobileMenuOpen); - } - return; - } - - // On tool pages, always toggle mobile menu - toggleStateOfMobileMenu(mobileMenuOpen, setMobileMenuOpen); - return; - } + case "s": + event.preventDefault(); + window.dispatchEvent(new CustomEvent("focus-search")); + break; - // Find matching tool by shortcut - Object.values(toolsData).forEach(section => { - section.tools.forEach(tool => { - const shortcutParts = tool.shortcut.split("+"); - let matches = true; + default: + break; + } + }; - if (shortcutParts.includes("Ctrl") && !event.ctrlKey) - matches = false; - if (shortcutParts.includes("Shift") && !event.shiftKey) - matches = false; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [theme, setTheme, isDemoRunning]); - const key = shortcutParts[shortcutParts.length - 1].toLowerCase(); - if (event.key.toLowerCase() !== key) matches = false; + useEffect(() => { + const handleResize = () => { + const desktop = window.innerWidth >= 1024; + setIsDesktop(desktop); - if (matches) { - event.preventDefault(); - setLocation(tool.path); // tool.path already includes /tools prefix - } - }); - }); - } + // ✅ IMPORTANT: when going to desktop, force-close the mobile sheet + if (desktop) setMobileMenuOpen(false); }; - document.addEventListener("keydown", handleKeyDown, true); - return () => document.removeEventListener("keydown", handleKeyDown, true); - }, [ - setLocation, - mobileMenuOpen, - shouldShowSidebarByDefault, - desktopSidebarVisible, - theme, - setTheme, - ]); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + useEffect(() => { + const tool = allTools.find(t => t.path === location); + + if (tool?.metadata.title) { + document.title = `${tool.metadata.title} | FreeDevTool.App`; + } else { + document.title = "FreeDevTool.App | Free Developer Tools"; + } + }, [location, allTools]); return ( -
- {/* Collapsible Header */} -
-
{ - // Toggle appropriate menu based on current context - if (shouldShowSidebarByDefault) { - // On homepage, toggle desktop sidebar on large screens, mobile menu on small screens - const isLargeScreen = window.innerWidth >= 1024; // lg breakpoint - if (isLargeScreen) { - setDesktopSidebarVisible(!desktopSidebarVisible); - } else { - setMobileMenuOpen(!mobileMenuOpen); - } - } else { - // On tool pages, always toggle mobile menu - setMobileMenuOpen(!mobileMenuOpen); - } - }} +
+ {/* DESKTOP SIDEBAR */} + {isDesktop && !sidebarCollapsed && !isDemoRunning && isSidebarOpen ? ( + setSidebarCollapsed(false)} /> -
- - {/* Demo Status Bar */} - {isDemoRunning ? ( -
-
-
- - Demo Mode Active - - - {currentDemoTool} - -
-
-
- - {Math.round(demoProgress)}% - - - {/* Speed Control */} -
- - -
-
-
- - - - - -

Previous Tool in Demo

-
-
- - - - - -

- {isDemoPaused ? "Resume Demo Tour" : "Pause Demo Tour"} -

-
-
- - - - - -

Next Tool in Demo

-
-
- - - - - -

Stop Demo Tour

-
-
-
-
-
) : null} - {/* Header Collapse Toggle */} -
- - - - - -

{headerCollapsed ? "Expand Header" : "Minimize Header"}

-
-
-
- -
- {/* Sidebar - Show by default on homepage desktop, hamburger menu on mobile and tool pages */} - {shouldShowSidebarByDefault ? ( - <> - {/* Default sidebar on homepage - hidden on mobile (lg:block) */} - {desktopSidebarVisible ? ( - - ) : null} - - {/* Hamburger menu for mobile on homepage */} - - + {/* HEADER */} +
+ {/* HEADER */} + {!headerCollapsed && ( +
+
{ + if (isDesktop) { + setSidebarCollapsed(v => !v); + } else { + setMobileMenuOpen(v => !v); + } }} + /> +
+ )} + + {/* HEADER TOGGLE BUTTON */} + + + + + Toggle Header + +
- {/* Main content with sidebar visible on desktop, full width on mobile */} -
- {children} -
- - ) : ( - <> - {/* Hamburger menu for tool pages */} - - - - Navigation Menu - - Navigation menu with all available developer tools - organized by category - - -
- setMobileMenuOpen(false)} - /> -
-
-
- {/* Main content full width */} -
- {children} -
- - )} + {/* MAIN CONTENT */} +
+ {children} +
- {/* Footer */} -
-
-
- Ready - - All operations client-side -
-
- - Ctrl+M - - Menu - - - Ctrl+S - - Search - - - Ctrl+D - - Theme -
-
-
+ {/* MOBILE SIDEBAR */} + {!isDesktop && ( + { + if (!isDemoRunning) setMobileMenuOpen(open); + }} + > + + + Navigation + Sidebar navigation + + + setMobileMenuOpen(false)} + /> + + + )}
); } - -function toggleStateOfMobileMenu( - mobileMenuOpen: boolean, - setMobileMenuOpen: React.Dispatch> -) { - const wasOpen = mobileMenuOpen; - setMobileMenuOpen(!mobileMenuOpen); - - // If opening the menu, focus the sidebar after a brief delay - if (!wasOpen) { - setTimeout(() => { - const sidebar = document.querySelector( - '[role="navigation"]' - ) as HTMLElement; - if (sidebar) { - sidebar.focus(); - } - }, 100); - } -} diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx index 11adec71..e45cb01d 100644 --- a/client/src/components/layout/Sidebar.tsx +++ b/client/src/components/layout/Sidebar.tsx @@ -1,621 +1,215 @@ -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { Link, useLocation } from "wouter"; -import { useState, useEffect, useRef, useCallback } from "react"; -import { toolsData } from "@/data/tools"; +import { useEffect, useState } from "react"; + import { + type LucideIcon, Home, - Calendar, - ArrowRightLeft, + ChevronRight, + ChevronDown, + Repeat, Code, - FileText, - Paintbrush, - FileCode, - Hash, - Link as LinkIcon, - Key, Shield, - GitCompare, - Search, - ArrowUpDown, - FileBarChart, - Clock, - Timer, - Globe, - Calculator, - CreditCard, - Square, - BarChart3, Type, + Clock, + DollarSign, Palette, - Video, - Volume2, - Command, - ChevronDown, - ChevronRight, - FileSpreadsheet, - ChevronsDown, - ChevronsUp, + Settings, + Lock, + HelpCircle, } from "lucide-react"; +import { toolsData } from "@/data/tools"; +import { cn } from "@/lib/utils"; interface SidebarProps { - className?: string; - collapsed?: boolean; - onToolClick?: () => void; // Callback for when a tool is clicked (to close mobile menu) + collapsed: boolean; + onToolClick?: () => void; + onExpandRequest?: (category: string) => void; } -// Icon mapping function based on category and tool name -function getToolIcon(category: string, toolName: string) { - const iconMap: Record> = { - Conversions: { - "Date Converter": , - "JSON ↔ YAML": , - "Timezone Converter": , - "Unit Converter": , - "URL to JSON": , - "CSV to JSON": , - "Number Base Converter": , - }, - Formatters: { - "JSON Formatter": , - "HTML Formatter": , - "YAML Formatter": , - "Markdown Formatter": , - "CSS Formatter": , - "LESS Formatter": , - "Time Formatter": , - }, - Encoders: { - "Base64 Encoder": , - "URL Encoder": , - "JWT Decoder": , - "TLS Decoder": , - "MD5 Hash": , - "BCrypt Hash": , - }, - "Text Tools": { - "Text Diff Viewer": , - "Regex Tester": , - "Text Sort": , - "Word Counter": , - "QR Code Generator": , - "Barcode Generator": , - "Lorem Ipsum Generator": , - "Unicode Character Map": , - "Password Generator": , - "UUID Generator": , - "Search & Replace": , - "Text Split": , - }, - "Time Tools": { - "World Clock": , - Timer: , - Stopwatch: , - Countdown: , - "Date/Time Diff": , - Metronome: , - }, - "Financial Tools": { - "Compound Interest": , - "Debt Repayment": , - }, - "Color Tools": { - "Color Palette Generator": , - }, - Hardware: { - "Camera Test":