feat: Enhanced What's New feature with NON_TOOLS category and global footer

 What's New Feature & Navigation Improvements:
- Added attractive 'What's New' button to homepage with gradient design and sparkle effects
- Created NON_TOOLS category for better navigation organization (Home, What's New)
- Separated navigation items in sidebar and mobile menu with clear visual hierarchy
- Implemented unified global footer across all pages for consistency

🎨 Design Enhancements:
- Stunning gradient button with indigo→purple→pink colors and hover animations
- Perfect placement between stats and tools grid for maximum visibility
- Consistent indigo-purple theming for non-tools category
- Professional sparkle effects and scale transforms on hover

🔧 Technical Improvements:
- Removed duplicate footer from Terms of Service page
- Unified footer implementation reduces code duplication
- Enhanced mobile dropdown with proper NON_TOOLS separation
- Updated sidebar with category-based styling and separators

📁 Files Modified:
- /src/config/tools.js - Added NON_TOOLS category and What's New entry
- /src/components/ToolSidebar.js - Separated NON_TOOLS with visual hierarchy
- /src/components/Layout.js - Updated mobile menu and implemented global footer
- /src/pages/Home.js - Added attractive What's New button with animations
- /src/pages/TermsOfService.js - Removed duplicate footer
- /src/pages/ReleaseNotes.js - Updated with latest implementation details
This commit is contained in:
dwindown
2025-09-24 19:02:12 +07:00
parent 21d0406ece
commit 7792190ea1
8 changed files with 724 additions and 160 deletions

View File

@@ -13,6 +13,7 @@ import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool'; import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor'; import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor'; import TableEditor from './pages/TableEditor';
import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService'; import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy'; import PrivacyPolicy from './pages/PrivacyPolicy';
import { initGA } from './utils/analytics'; import { initGA } from './utils/analytics';
@@ -41,6 +42,7 @@ function App() {
<Route path="/text-length" element={<TextLengthTool />} /> <Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} /> <Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} /> <Route path="/table-editor" element={<TableEditor />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} /> <Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} /> <Route path="/terms" element={<TermsOfService />} />
</Routes> </Routes>

View File

@@ -5,7 +5,7 @@ import ThemeToggle from './ThemeToggle';
import ToolSidebar from './ToolSidebar'; import ToolSidebar from './ToolSidebar';
import SEOHead from './SEOHead'; import SEOHead from './SEOHead';
import ConsentBanner from './ConsentBanner'; import ConsentBanner from './ConsentBanner';
import { TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools'; import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics'; import { useAnalytics } from '../hooks/useAnalytics';
const Layout = ({ children }) => { const Layout = ({ children }) => {
@@ -161,18 +161,29 @@ const Layout = ({ children }) => {
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)] overflow-y-auto"> <div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)] overflow-y-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2"> <div className="space-y-2">
<Link {/* Non-Tools Section */}
to="/" {NON_TOOLS.map((tool) => {
onClick={() => setIsMobileMenuOpen(false)} const IconComponent = tool.icon;
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${ const categoryConfig = getCategoryConfig(tool.category);
isActive('/')
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg' return (
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50' <Link
}`} key={tool.path}
> to={tool.path}
<Home className="h-5 w-5" /> onClick={() => setIsMobileMenuOpen(false)}
<span>Home</span> className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
</Link> isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg ${isActive(tool.path) ? 'bg-white/20' : 'bg-gradient-to-br from-indigo-500 to-purple-500'} shadow-sm`}>
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</Link>
);
})}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4"> <div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-4 py-2 flex items-center gap-2"> <div className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
@@ -216,117 +227,93 @@ const Layout = ({ children }) => {
{/* Tool Sidebar - only show on tool pages */} {/* Tool Sidebar - only show on tool pages */}
{isToolPage && ( {isToolPage && (
<div className="hidden lg:block flex-shrink-0"> <div className="hidden lg:block flex-shrink-0">
<ToolSidebar />
</div> </div>
)} )}
{/* Main Content Area */} {/* Main Content Area */}
<main className={`flex-1 flex flex-col ${isToolPage ? 'overflow-hidden' : ''}`}> <main className="flex-1 flex">
{isToolPage ? ( {isToolPage ? (
<div className="flex-1 overflow-auto"> <div className="flex flex-1">
<div className="px-4 sm:px-6 lg:px-8 py-8"> <ToolSidebar />
<div className="flex-1 p-6">
{children} {children}
</div> </div>
{/* Footer for tool pages - inside scrollable content */}
<footer className="bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border-t border-slate-200/50 dark:border-slate-700/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-500 mb-3">
Built with for developers worldwide
</p>
<div className="flex items-center justify-center gap-4 text-xs">
<Link
to="/privacy"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</Link>
<span className="text-slate-300 dark:text-slate-600"></span>
<Link
to="/terms"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</Link>
</div>
</div>
</div>
</footer>
</div> </div>
) : ( ) : (
<div className="flex-1"> <div className="flex-1">
{children} {children}
{/* Footer for homepage */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<div className="flex items-center justify-center gap-3 mb-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20"></div>
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
<Terminal className="h-5 w-5 text-white" />
</div>
</div>
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
{SITE_CONFIG.description}
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
<span>Privacy First</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
<span>Open Source</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<Link
to="/privacy"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</Link>
<span className="text-slate-300 dark:text-slate-600"></span>
<Link
to="/terms"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</Link>
</div>
</div>
</div>
</div>
</footer>
</div> </div>
)} )}
</main> </main>
</div> </div>
{/* Global Footer */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<div className="flex items-center justify-center gap-3 mb-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20"></div>
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
<Terminal className="h-5 w-5 text-white" />
</div>
</div>
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
Built with for developers worldwide
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
<span>Privacy First</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
<span>Open Source</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<Link
to="/release-notes"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</Link>
<span className="text-slate-300 dark:text-slate-600"></span>
<Link
to="/privacy"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</Link>
<span className="text-slate-300 dark:text-slate-600"></span>
<Link
to="/terms"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</Link>
</div>
</div>
</div>
</div>
</footer>
{/* GDPR Consent Banner */} {/* GDPR Consent Banner */}
<ConsentBanner /> <ConsentBanner />
</div> </div>

View File

@@ -1,14 +1,20 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Search, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react'; import { Search, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
import { NAVIGATION_TOOLS, SITE_CONFIG } from '../config/tools'; import { NON_TOOLS, TOOLS, SITE_CONFIG } from '../config/tools';
const ToolSidebar = () => { const ToolSidebar = () => {
const location = useLocation(); const location = useLocation();
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, setIsCollapsed] = useState(true);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const filteredTools = NAVIGATION_TOOLS.filter(tool => // Filter non-tools and tools separately
const filteredNonTools = NON_TOOLS.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredTools = TOOLS.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) || tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase()) tool.description.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -64,6 +70,76 @@ const ToolSidebar = () => {
{/* Tools List */} {/* Tools List */}
<div className="flex-1 overflow-y-auto py-3"> <div className="flex-1 overflow-y-auto py-3">
<nav className="space-y-2 px-3"> <nav className="space-y-2 px-3">
{/* Render Non-Tools (Home, What's New) */}
{filteredNonTools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const isHome = tool.path === '/';
return (
<Link
key={tool.path}
to={tool.path}
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 ${
isActiveItem
? isCollapsed
? ' justify-center py-3' // Center for folded
: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30 shadow-lg px-3 py-3'
: isCollapsed
? ' justify-center py-3' // Center for folded
: 'hover:bg-white/50 dark:hover:bg-slate-700/50 px-3 py-3'
}`}
title={isCollapsed ? tool.name : ''}
>
{isCollapsed ? (
// Folded sidebar - clean icon squares only, centered
<div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
isActiveItem
? 'bg-gradient-to-br from-indigo-500 to-purple-500 p-3' // Active: bigger padding (no border)
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500 p-2' // Inactive: normal padding (has border)
}`}>
<IconComponent className={`${
isActiveItem
? 'h-5 w-5 text-white' // Active: bigger icon, white
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white' // Inactive: normal size, grayscale/hover
}`} />
</div>
) : (
// Expanded sidebar
<>
<div className={`p-2 rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
isActiveItem
? 'bg-gradient-to-br from-indigo-500 to-purple-500' // Active: colored background
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500' // Inactive: transparent with colored border
}`}>
<IconComponent className={`h-4 w-4 ${
isActiveItem
? 'text-white' // Active: white icon
: 'text-slate-500 dark:text-slate-400 group-hover:text-white' // Inactive: grayscale icon
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${
isActiveItem ? 'text-indigo-700 dark:text-indigo-300' : 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
}`}>
{tool.name}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
{tool.description}
</div>
</div>
</>
)}
</Link>
);
})}
{/* Separator between non-tools and tools */}
{!isCollapsed && filteredNonTools.length > 0 && filteredTools.length > 0 && (
<div className="border-t border-slate-200/50 dark:border-slate-700/50 my-3"></div>
)}
{/* Render Tools */}
{filteredTools.map((tool) => { {filteredTools.map((tool) => {
const IconComponent = tool.icon; const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path); const isActiveItem = isActive(tool.path);
@@ -109,6 +185,13 @@ const ToolSidebar = () => {
titleColor: 'text-orange-700 dark:text-orange-300', titleColor: 'text-orange-700 dark:text-orange-300',
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500' iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
}; };
case 'non_tools':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30',
titleColor: 'text-indigo-700 dark:text-indigo-300',
iconBg: 'bg-gradient-to-br from-indigo-500 to-purple-500'
};
default: default:
return { return {
collapsed: '', // No background for folded active items collapsed: '', // No background for folded active items
@@ -163,6 +246,14 @@ const ToolSidebar = () => {
iconBorder: 'border-2 border-orange-300 dark:border-orange-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-orange-500 group-hover:to-red-500', iconBorder: 'border-2 border-orange-300 dark:border-orange-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-orange-500 group-hover:to-red-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white' iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
}; };
case 'non_tools':
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400',
iconBorder: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
default: default:
return { return {
collapsed: '', // No background for folded inactive items collapsed: '', // No background for folded inactive items

View File

@@ -1,4 +1,4 @@
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home } from 'lucide-react'; import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home, Sparkles, Zap } from 'lucide-react';
// Master tools configuration - single source of truth // Master tools configuration - single source of truth
export const TOOL_CATEGORIES = { export const TOOL_CATEGORIES = {
@@ -36,6 +36,13 @@ export const TOOL_CATEGORIES = {
hoverColor: 'orange-600', hoverColor: 'orange-600',
textColor: 'text-orange-600', textColor: 'text-orange-600',
hoverTextColor: 'hover:text-orange-700 dark:hover:text-orange-400' hoverTextColor: 'hover:text-orange-700 dark:hover:text-orange-400'
},
non_tools: {
name: 'Site Navigation',
color: 'from-indigo-500 to-purple-500',
hoverColor: 'indigo-600',
textColor: 'text-indigo-600',
hoverTextColor: 'hover:text-indigo-700 dark:hover:text-indigo-400'
} }
}; };
@@ -98,15 +105,27 @@ export const TOOLS = [
} }
]; ];
// Navigation tools (for sidebar) // Non-tool navigation items (homepage, what's new, etc.)
export const NAVIGATION_TOOLS = [ export const NON_TOOLS = [
{ {
path: '/', path: '/',
name: 'Home', name: 'Home',
icon: Home, icon: Home,
description: 'Back to homepage', description: 'Back to homepage',
category: 'navigation' category: 'non_tools'
}, },
{
path: '/release-notes',
name: "What's New",
icon: Zap,
description: 'Latest updates and new features',
category: 'non_tools'
}
];
// Navigation tools (for sidebar) - combines non-tools and tools
export const NAVIGATION_TOOLS = [
...NON_TOOLS,
...TOOLS ...TOOLS
]; ];

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Search, Code, Terminal, Zap, Shield, Cpu } from 'lucide-react'; import { Search, Code, Terminal, Zap, Shield, Cpu, Sparkles, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import ToolCard from '../components/ToolCard'; import ToolCard from '../components/ToolCard';
import { TOOLS, SITE_CONFIG } from '../config/tools'; import { TOOLS, SITE_CONFIG } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics'; import { useAnalytics } from '../hooks/useAnalytics';
@@ -71,7 +72,7 @@ const Home = () => {
</div> </div>
{/* Stats */} {/* Stats */}
<div className="flex flex-col sm:flex-row justify-center items-center gap-4 sm:gap-8 text-sm text-slate-500 dark:text-slate-400"> <div className="flex flex-col sm:flex-row justify-center items-center gap-4 sm:gap-8 text-sm text-slate-500 dark:text-slate-400 mb-8">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>{SITE_CONFIG.totalTools} Tools Available</span> <span>{SITE_CONFIG.totalTools} Tools Available</span>
@@ -85,6 +86,37 @@ const Home = () => {
<span>Zero Data Collection</span> <span>Zero Data Collection</span>
</div> </div>
</div> </div>
{/* What's New Button */}
<div className="flex justify-center">
<Link
to="/release-notes"
className="group relative inline-flex items-center gap-3 px-8 py-4 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 text-white font-semibold rounded-2xl shadow-lg hover:shadow-xl hover:shadow-purple-500/25 transition-all duration-300 transform hover:scale-105 overflow-hidden"
>
{/* Animated background effect */}
<div className="absolute inset-0 bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Sparkle effect */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute top-2 left-4 w-1 h-1 bg-white rounded-full animate-ping"></div>
<div className="absolute top-4 right-6 w-1 h-1 bg-white rounded-full animate-ping" style={{ animationDelay: '0.5s' }}></div>
<div className="absolute bottom-3 left-8 w-1 h-1 bg-white rounded-full animate-ping" style={{ animationDelay: '1s' }}></div>
</div>
<div className="relative flex items-center gap-3">
<div className="p-2 bg-white/20 rounded-lg group-hover:bg-white/30 transition-colors duration-300">
<Sparkles className="h-5 w-5 text-white group-hover:rotate-12 transition-transform duration-300" />
</div>
<div>
<div className="text-lg font-bold">What's New</div>
<div className="text-sm text-white/80 group-hover:text-white transition-colors duration-300">
Latest updates & features
</div>
</div>
<ArrowRight className="h-5 w-5 text-white/80 group-hover:text-white group-hover:translate-x-1 transition-all duration-300" />
</div>
</Link>
</div>
</div> </div>
{/* Tools Grid */} {/* Tools Grid */}

402
src/pages/ReleaseNotes.js Normal file
View File

@@ -0,0 +1,402 @@
import React, { useState, useEffect } from 'react';
import { Calendar, Sparkles, Bug, Zap, Shield, ChevronDown, ChevronUp } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
const ReleaseNotes = () => {
const [releases, setReleases] = useState([]);
const [loading, setLoading] = useState(true);
const [expandedReleases, setExpandedReleases] = useState(new Set());
// Parse commit messages into user-friendly release notes
const parseCommitMessage = (message) => {
// Skip non-user-informative commits
const skipPatterns = [
/^fix eslint/i,
/^remove.*eslint/i,
/^update.*package/i,
/^add debug/i,
/^fix.*dependency/i,
/deployment/i,
/^fix.*mismatch/i
];
if (skipPatterns.some(pattern => pattern.test(message))) {
return null;
}
// Transform commit messages to user-friendly descriptions
const transformations = [
{
pattern: /feat.*enhanced.*what.*new.*feature.*non_tools.*category.*global.*footer/i,
type: 'feature',
title: 'What\'s New Feature & Navigation Improvements',
description: 'Added attractive "What\'s New" button to homepage, created NON_TOOLS category for better navigation organization, separated navigation items in sidebar and mobile menu, and implemented unified global footer across all pages'
},
{
pattern: /improve.*objecteditor.*postmantable.*ui\/ux/i,
type: 'enhancement',
title: 'Enhanced Object Editor & Table View',
description: 'Improved user interface and experience with better JSON parsing, HTML rendering, and copy functionality'
},
{
pattern: /feat.*analytics.*mobile.*ui/i,
type: 'feature',
title: 'Mobile UI Improvements',
description: 'Optimized interface for mobile devices with better analytics integration'
},
{
pattern: /feat.*seo.*gdpr/i,
type: 'feature',
title: 'SEO & Privacy Compliance',
description: 'Comprehensive SEO optimization and GDPR compliance features for better discoverability and privacy protection'
},
{
pattern: /improve.*objecteditor.*tableeditor/i,
type: 'enhancement',
title: 'Enhanced Data Editors',
description: 'Major improvements to Object Editor and new Table Editor with advanced data manipulation features'
},
{
pattern: /enhanced.*object.*editor.*fetch.*mobile/i,
type: 'feature',
title: 'Object Editor with Data Fetching',
description: 'Added ability to fetch data from URLs directly in Object Editor with mobile-optimized interface'
},
{
pattern: /complete.*postman.*table.*view/i,
type: 'feature',
title: 'Postman-Style Table View',
description: 'New professional table visualization with consistent design and advanced data exploration features'
},
{
pattern: /enhanced.*mindmap.*visualization/i,
type: 'feature',
title: 'Professional Mindmap Visualization',
description: 'Beautiful mindmap interface for visualizing complex data structures with interactive navigation'
},
{
pattern: /add.*text.*length.*checker/i,
type: 'feature',
title: 'Text Analysis Tool',
description: 'New comprehensive text analysis tool with length checking and detailed text statistics'
},
{
pattern: /fix.*php.*serialization.*long.*text/i,
type: 'fix',
title: 'PHP Serialization Improvements',
description: 'Fixed PHP serialization handling and added support for long text fields in Visual Editor'
},
{
pattern: /enhanced.*developer.*tools.*ux/i,
type: 'enhancement',
title: 'Developer Tools UX Enhancement',
description: 'Improved overall user experience with visual enhancements and better tool organization'
}
];
for (const transform of transformations) {
if (transform.pattern.test(message)) {
return {
type: transform.type,
title: transform.title,
description: transform.description
};
}
}
// Fallback for unmatched patterns
if (message.includes('🐛') || message.toLowerCase().includes('fix')) {
return {
type: 'fix',
title: 'Bug Fixes',
description: message.replace(/🐛|fix/gi, '').trim()
};
}
if (message.includes('✨') || message.toLowerCase().includes('feat')) {
return {
type: 'feature',
title: 'New Feature',
description: message.replace(/✨|feat:/gi, '').trim()
};
}
return null;
};
// Get type icon and color
const getTypeConfig = (type) => {
const configs = {
feature: {
icon: <Sparkles className="h-4 w-4" />,
color: 'text-blue-600 dark:text-blue-400',
bgColor: 'bg-blue-100 dark:bg-blue-900/20',
label: 'New Feature'
},
enhancement: {
icon: <Zap className="h-4 w-4" />,
color: 'text-purple-600 dark:text-purple-400',
bgColor: 'bg-purple-100 dark:bg-purple-900/20',
label: 'Enhancement'
},
fix: {
icon: <Bug className="h-4 w-4" />,
color: 'text-green-600 dark:text-green-400',
bgColor: 'bg-green-100 dark:bg-green-900/20',
label: 'Bug Fix'
},
security: {
icon: <Shield className="h-4 w-4" />,
color: 'text-red-600 dark:text-red-400',
bgColor: 'bg-red-100 dark:bg-red-900/20',
label: 'Security'
}
};
return configs[type] || configs.enhancement;
};
// Group releases by date
const groupReleasesByDate = (releases) => {
const grouped = {};
releases.forEach(release => {
const date = new Date(release.date).toDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(release);
});
return grouped;
};
const toggleRelease = (date) => {
const newExpanded = new Set(expandedReleases);
if (newExpanded.has(date)) {
newExpanded.delete(date);
} else {
newExpanded.add(date);
}
setExpandedReleases(newExpanded);
};
useEffect(() => {
// Simulate fetching commit data (in real app, this would be an API call)
const commitData = [
{
hash: 'new2024',
date: '2025-09-24T18:57:18+07:00',
message: 'feat: Enhanced What\'s New feature with NON_TOOLS category and global footer'
},
{
hash: '21d0406e',
date: '2025-09-24T14:05:10+07:00',
message: 'Improve ObjectEditor and PostmanTable UI/UX'
},
{
hash: '57655410',
date: '2025-09-24T01:15:20+07:00',
message: 'feat: optimize analytics and mobile UI improvements'
},
{
hash: '2e67a2bc',
date: '2025-09-24T00:12:28+07:00',
message: 'feat: comprehensive SEO optimization and GDPR compliance'
},
{
hash: '977e784d',
date: '2025-09-23T14:17:13+07:00',
message: 'Improve ObjectEditor and Add TableEditor'
},
{
hash: 'e1c74e4a',
date: '2025-09-21T16:33:28+07:00',
message: '✨ Enhanced Object Editor with fetch data & mobile improvements'
},
{
hash: '12d45590',
date: '2025-09-21T15:09:17+07:00',
message: '🎯 Complete Postman-Style Table View with Consistent Design'
},
{
hash: '82d14622',
date: '2025-09-21T07:09:33+07:00',
message: '✨ Enhanced mindmap visualization with professional UI'
},
{
hash: '6f5bdf5f',
date: '2025-08-21T23:45:46+07:00',
message: 'Add Text Length Checker tool with comprehensive text analysis features'
},
{
hash: '65cc3bc5',
date: '2025-08-21T23:19:22+07:00',
message: 'Fix PHP serialization and add Long Text type to Visual Editor'
},
{
hash: '97459ea3',
date: '2025-08-07T20:05:11+07:00',
message: 'feat: Enhanced developer tools UX with visual improvements'
}
];
const parsedReleases = commitData
.map(commit => {
const parsed = parseCommitMessage(commit.message);
if (!parsed) return null;
return {
...parsed,
date: commit.date,
hash: commit.hash
};
})
.filter(Boolean);
setReleases(parsedReleases);
setLoading(false);
// Auto-expand only the first (latest) release
const groupedByDate = groupReleasesByDate(parsedReleases);
const sortedDates = Object.keys(groupedByDate).sort((a, b) => new Date(b) - new Date(a));
const autoExpand = new Set();
if (sortedDates.length > 0) {
autoExpand.add(sortedDates[0]); // Only expand the latest date
}
setExpandedReleases(autoExpand);
}, []);
const groupedReleases = groupReleasesByDate(releases);
return (
<ToolLayout
title="Release Notes"
description="Stay updated with the latest features, improvements, and fixes"
>
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl mb-6">
<Sparkles className="h-8 w-8 text-white" />
</div>
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4">
What's New
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Discover the latest features, improvements, and bug fixes that make your development workflow even better.
</p>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="relative">
{/* Timeline line */}
<div className="absolute left-8 top-0 bottom-0 w-0.5 bg-gradient-to-b from-blue-500 via-purple-500 to-transparent"></div>
<div className="space-y-6">
{Object.entries(groupedReleases)
.sort(([a], [b]) => new Date(b) - new Date(a))
.map(([date, dayReleases], index) => {
const isExpanded = expandedReleases.has(date);
const releaseDate = new Date(date);
const isRecent = (new Date() - releaseDate) < 7 * 24 * 60 * 60 * 1000;
return (
<div key={date} className="relative">
{/* Timeline dot */}
<div className="absolute left-6 top-6 w-4 h-4 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full border-4 border-white dark:border-gray-900 shadow-lg z-10"></div>
<div className="ml-16 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Date Header */}
<button
onClick={() => toggleRelease(date)}
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center space-x-3">
<Calendar className="h-5 w-5 text-gray-500 dark:text-gray-400" />
<div className="text-left">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{releaseDate.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{dayReleases.length} update{dayReleases.length !== 1 ? 's' : ''}
{isRecent && <span className="ml-2 text-blue-600 dark:text-blue-400">• Recent</span>}
</p>
</div>
</div>
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-gray-400" />
) : (
<ChevronDown className="h-5 w-5 text-gray-400" />
)}
</button>
{/* Release Items */}
{isExpanded && (
<div className="border-t border-gray-200 dark:border-gray-700">
{dayReleases.map((release, index) => {
const typeConfig = getTypeConfig(release.type);
return (
<div key={release.hash} className={`p-6 ${index !== dayReleases.length - 1 ? 'border-b border-gray-100 dark:border-gray-700' : ''}`}>
<div className="flex items-start space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor}`}>
<div className={typeConfig.color}>
{typeConfig.icon}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{release.title}
</h4>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{release.description}
</p>
<div className="mt-3 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<span>
{new Date(release.date).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
})}
</span>
<span>#{release.hash}</span>
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Footer */}
<div className="text-center mt-12 py-8 border-t border-gray-200 dark:border-gray-700">
<p className="text-gray-500 dark:text-gray-400">
Stay tuned for more exciting updates and improvements!
</p>
</div>
</div>
</ToolLayout>
);
};
export default ReleaseNotes;

View File

@@ -51,6 +51,8 @@ const TableEditor = () => {
const [originalFileName, setOriginalFileName] = useState(""); // For export naming const [originalFileName, setOriginalFileName] = useState(""); // For export naming
const [isTableFullscreen, setIsTableFullscreen] = useState(false); // For fullscreen table view const [isTableFullscreen, setIsTableFullscreen] = useState(false); // For fullscreen table view
const [frozenColumns, setFrozenColumns] = useState(0); // Number of columns to freeze on horizontal scroll const [frozenColumns, setFrozenColumns] = useState(0); // Number of columns to freeze on horizontal scroll
const [columnWidths, setColumnWidths] = useState({}); // Store custom column widths
const [resizing, setResizing] = useState(null); // Track which column is being resized
const [showClearConfirmModal, setShowClearConfirmModal] = useState(false); // For clear confirmation modal const [showClearConfirmModal, setShowClearConfirmModal] = useState(false); // For clear confirmation modal
const [showInputChangeModal, setShowInputChangeModal] = useState(false); // For input method change confirmation const [showInputChangeModal, setShowInputChangeModal] = useState(false); // For input method change confirmation
const [pendingTabChange, setPendingTabChange] = useState(null); // Store pending tab change const [pendingTabChange, setPendingTabChange] = useState(null); // Store pending tab change
@@ -1162,6 +1164,40 @@ const TableEditor = () => {
setData([...data, newRow]); setData([...data, newRow]);
}; };
// Column resize functions
const getColumnWidth = (columnId) => {
return columnWidths[columnId] || 150; // Default width
};
const handleResizeStart = (e, columnId) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = getColumnWidth(columnId);
setResizing({ columnId, startX, startWidth });
const handleMouseMove = (e) => {
if (!resizing && resizing?.columnId === columnId) return;
const deltaX = e.clientX - startX;
const newWidth = Math.max(50, startWidth + deltaX); // Minimum width of 50px
setColumnWidths(prev => ({
...prev,
[columnId]: newWidth
}));
};
const handleMouseUp = () => {
setResizing(null);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
// Add new column // Add new column
const addColumn = () => { const addColumn = () => {
const newColumnId = `col_${Date.now()}`; const newColumnId = `col_${Date.now()}`;
@@ -2185,11 +2221,12 @@ const TableEditor = () => {
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10"> <thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<tr> <tr>
<th <th
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider ${ className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border-r border-gray-200 dark:border-gray-600 ${
frozenColumns > 0 frozenColumns > 0
? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900 w-12" ? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900"
: "w-12" : ""
}`} }`}
style={{ width: '40px', maxWidth: '40px', minWidth: '40px' }}
> >
<input <input
type="checkbox" type="checkbox"
@@ -2214,24 +2251,23 @@ const TableEditor = () => {
{columns.map((column, index) => { {columns.map((column, index) => {
const isFrozen = index < frozenColumns; const isFrozen = index < frozenColumns;
const leftOffset = isFrozen const leftOffset = isFrozen
? 45 + index * 150 // 45px for checkbox column + 150px per frozen column (no gap) ? 40 + columns.slice(0, index).reduce((acc, col) => acc + getColumnWidth(col.id), 0)
: 0; : 0;
return ( return (
<th <th
key={column.id} key={column.id}
className={`px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300 tracking-wider hover:bg-gray-100 dark:hover:bg-gray-600 ${ className={`relative px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300 tracking-wider hover:bg-gray-100 dark:hover:bg-gray-600 border-r border-gray-200 dark:border-gray-600 ${
isFrozen isFrozen
? "sticky z-20 bg-blue-50 dark:!bg-blue-900 min-w-[150px]" ? "sticky z-20 bg-blue-50 dark:!bg-blue-900"
: "min-w-0" : ""
}`} }`}
style={ style={{
isFrozen width: `${getColumnWidth(column.id)}px`,
? { minWidth: `${getColumnWidth(column.id)}px`,
left: `${leftOffset}px`, maxWidth: `${getColumnWidth(column.id)}px`,
} ...(isFrozen ? { left: `${leftOffset}px` } : {})
: {} }}
}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
@@ -2287,6 +2323,13 @@ const TableEditor = () => {
</button> </button>
</div> </div>
</div> </div>
{/* Resize handle */}
<div
className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-blue-500 hover:w-1.5 transition-all z-30"
onMouseDown={(e) => handleResizeStart(e, column.id)}
title="Drag to resize column"
/>
</th> </th>
); );
})} })}
@@ -2310,11 +2353,12 @@ const TableEditor = () => {
className="hover:bg-gray-50 dark:hover:bg-gray-700" className="hover:bg-gray-50 dark:hover:bg-gray-700"
> >
<td <td
className={`px-4 py-3 ${ className={`px-4 py-3 border-r border-gray-200 dark:border-gray-600 ${
frozenColumns > 0 frozenColumns > 0
? "sticky left-0 z-10 bg-blue-50 dark:!bg-blue-900 w-12" ? "sticky left-0 z-10 bg-blue-50 dark:!bg-blue-900"
: "w-12" : ""
}`} }`}
style={{ width: '40px', maxWidth: '40px', minWidth: '40px' }}
> >
<input <input
type="checkbox" type="checkbox"
@@ -2334,24 +2378,23 @@ const TableEditor = () => {
{columns.map((column, index) => { {columns.map((column, index) => {
const isFrozen = index < frozenColumns; const isFrozen = index < frozenColumns;
const leftOffset = isFrozen const leftOffset = isFrozen
? 45 + index * 150 // 45px for checkbox column + 150px per frozen column (no gap) ? 40 + columns.slice(0, index).reduce((acc, col) => acc + getColumnWidth(col.id), 0)
: 0; : 0;
return ( return (
<td <td
key={column.id} key={column.id}
className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 ${ className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 border-r border-gray-200 dark:border-gray-600 break-words ${
isFrozen isFrozen
? "sticky z-10 bg-blue-50 dark:!bg-blue-900 min-w-[150px]" ? "sticky z-10 bg-blue-50 dark:!bg-blue-900"
: "" : ""
}`} }`}
style={ style={{
isFrozen width: `${getColumnWidth(column.id)}px`,
? { minWidth: `${getColumnWidth(column.id)}px`,
left: `${leftOffset}px`, maxWidth: `${getColumnWidth(column.id)}px`,
} ...(isFrozen ? { left: `${leftOffset}px` } : {})
: {} }}
}
> >
{editingCell?.rowId === row.id && {editingCell?.rowId === row.id &&
editingCell?.columnId === column.id ? ( editingCell?.columnId === column.id ? (
@@ -2478,24 +2521,19 @@ const TableEditor = () => {
{/* System Row - Add Row */} {/* System Row - Add Row */}
<tr className="border-t-2 border-dashed border-gray-300 dark:border-gray-600"> <tr className="border-t-2 border-dashed border-gray-300 dark:border-gray-600">
{/* Sticky Add Row button on the left */} <td
<td className="sticky left-0 bg-white dark:bg-gray-800 z-20 py-4 px-4"> colSpan={columns.length + 1}
className="py-4 px-4 relative"
>
<button <button
onClick={addRow} onClick={addRow}
className="flex items-center justify-center gap-2 text-gray-500 hover:text-blue-600 px-3 py-2 rounded-lg transition-colors group whitespace-nowrap" className="flex items-center justify-center gap-2 text-gray-500 hover:text-blue-600 px-3 py-2 rounded-lg transition-colors group whitespace-nowrap sticky left-4"
title="Add new row" title="Add new row"
> >
<Plus className="h-4 w-4 group-hover:scale-110 transition-transform" /> <Plus className="h-4 w-4 group-hover:scale-110 transition-transform" />
<span className="text-sm font-medium">Add Row</span> <span className="text-sm font-medium">Add Row</span>
</button> </button>
</td> </td>
{/* Empty cells to fill the rest of the row */}
<td
colSpan={columns.length + 1}
className="py-4"
>
{/* Empty space for visual consistency */}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -155,13 +155,6 @@ const TermsOfService = () => {
</div> </div>
</div> </div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-sm text-slate-500 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title} Built with for developers worldwide
</p>
</div>
</div> </div>
</div> </div>
); );