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:
@@ -13,6 +13,7 @@ import DiffTool from './pages/DiffTool';
|
||||
import TextLengthTool from './pages/TextLengthTool';
|
||||
import ObjectEditor from './pages/ObjectEditor';
|
||||
import TableEditor from './pages/TableEditor';
|
||||
import ReleaseNotes from './pages/ReleaseNotes';
|
||||
import TermsOfService from './pages/TermsOfService';
|
||||
import PrivacyPolicy from './pages/PrivacyPolicy';
|
||||
import { initGA } from './utils/analytics';
|
||||
@@ -41,6 +42,7 @@ function App() {
|
||||
<Route path="/text-length" element={<TextLengthTool />} />
|
||||
<Route path="/object-editor" element={<ObjectEditor />} />
|
||||
<Route path="/table-editor" element={<TableEditor />} />
|
||||
<Route path="/release-notes" element={<ReleaseNotes />} />
|
||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
||||
<Route path="/terms" element={<TermsOfService />} />
|
||||
</Routes>
|
||||
|
||||
@@ -5,7 +5,7 @@ import ThemeToggle from './ThemeToggle';
|
||||
import ToolSidebar from './ToolSidebar';
|
||||
import SEOHead from './SEOHead';
|
||||
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';
|
||||
|
||||
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="space-y-2">
|
||||
{/* Non-Tools Section */}
|
||||
{NON_TOOLS.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
const categoryConfig = getCategoryConfig(tool.category);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/"
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
|
||||
isActive('/')
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
<span>Home</span>
|
||||
<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="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
|
||||
@@ -216,54 +227,27 @@ const Layout = ({ children }) => {
|
||||
{/* Tool Sidebar - only show on tool pages */}
|
||||
{isToolPage && (
|
||||
<div className="hidden lg:block flex-shrink-0">
|
||||
<ToolSidebar />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className={`flex-1 flex flex-col ${isToolPage ? 'overflow-hidden' : ''}`}>
|
||||
<main className="flex-1 flex">
|
||||
{isToolPage ? (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-1">
|
||||
<ToolSidebar />
|
||||
<div className="flex-1 p-6">
|
||||
{children}
|
||||
</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 className="flex-1">
|
||||
{children}
|
||||
{/* Footer for homepage */}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</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">
|
||||
@@ -286,7 +270,7 @@ const Layout = ({ children }) => {
|
||||
<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}
|
||||
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">
|
||||
@@ -304,6 +288,13 @@ const Layout = ({ children }) => {
|
||||
</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"
|
||||
@@ -322,10 +313,6 @@ const Layout = ({ children }) => {
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* GDPR Consent Banner */}
|
||||
<ConsentBanner />
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
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 location = useLocation();
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
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.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
@@ -64,6 +70,76 @@ const ToolSidebar = () => {
|
||||
{/* Tools List */}
|
||||
<div className="flex-1 overflow-y-auto py-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) => {
|
||||
const IconComponent = tool.icon;
|
||||
const isActiveItem = isActive(tool.path);
|
||||
@@ -109,6 +185,13 @@ const ToolSidebar = () => {
|
||||
titleColor: 'text-orange-700 dark:text-orange-300',
|
||||
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:
|
||||
return {
|
||||
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',
|
||||
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:
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
|
||||
@@ -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
|
||||
export const TOOL_CATEGORIES = {
|
||||
@@ -36,6 +36,13 @@ export const TOOL_CATEGORIES = {
|
||||
hoverColor: 'orange-600',
|
||||
textColor: 'text-orange-600',
|
||||
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)
|
||||
export const NAVIGATION_TOOLS = [
|
||||
// Non-tool navigation items (homepage, what's new, etc.)
|
||||
export const NON_TOOLS = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
icon: Home,
|
||||
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
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { TOOLS, SITE_CONFIG } from '../config/tools';
|
||||
import { useAnalytics } from '../hooks/useAnalytics';
|
||||
@@ -71,7 +72,7 @@ const Home = () => {
|
||||
</div>
|
||||
|
||||
{/* 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="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>{SITE_CONFIG.totalTools} Tools Available</span>
|
||||
@@ -85,6 +86,37 @@ const Home = () => {
|
||||
<span>Zero Data Collection</span>
|
||||
</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>
|
||||
|
||||
{/* Tools Grid */}
|
||||
|
||||
402
src/pages/ReleaseNotes.js
Normal file
402
src/pages/ReleaseNotes.js
Normal 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;
|
||||
@@ -51,6 +51,8 @@ const TableEditor = () => {
|
||||
const [originalFileName, setOriginalFileName] = useState(""); // For export naming
|
||||
const [isTableFullscreen, setIsTableFullscreen] = useState(false); // For fullscreen table view
|
||||
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 [showInputChangeModal, setShowInputChangeModal] = useState(false); // For input method change confirmation
|
||||
const [pendingTabChange, setPendingTabChange] = useState(null); // Store pending tab change
|
||||
@@ -1162,6 +1164,40 @@ const TableEditor = () => {
|
||||
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
|
||||
const addColumn = () => {
|
||||
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">
|
||||
<tr>
|
||||
<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
|
||||
? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900 w-12"
|
||||
: "w-12"
|
||||
? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900"
|
||||
: ""
|
||||
}`}
|
||||
style={{ width: '40px', maxWidth: '40px', minWidth: '40px' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -2214,24 +2251,23 @@ const TableEditor = () => {
|
||||
{columns.map((column, index) => {
|
||||
const isFrozen = index < frozenColumns;
|
||||
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;
|
||||
|
||||
return (
|
||||
<th
|
||||
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
|
||||
? "sticky z-20 bg-blue-50 dark:!bg-blue-900 min-w-[150px]"
|
||||
: "min-w-0"
|
||||
? "sticky z-20 bg-blue-50 dark:!bg-blue-900"
|
||||
: ""
|
||||
}`}
|
||||
style={
|
||||
isFrozen
|
||||
? {
|
||||
left: `${leftOffset}px`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
style={{
|
||||
width: `${getColumnWidth(column.id)}px`,
|
||||
minWidth: `${getColumnWidth(column.id)}px`,
|
||||
maxWidth: `${getColumnWidth(column.id)}px`,
|
||||
...(isFrozen ? { left: `${leftOffset}px` } : {})
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -2287,6 +2323,13 @@ const TableEditor = () => {
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
@@ -2310,11 +2353,12 @@ const TableEditor = () => {
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<td
|
||||
className={`px-4 py-3 ${
|
||||
className={`px-4 py-3 border-r border-gray-200 dark:border-gray-600 ${
|
||||
frozenColumns > 0
|
||||
? "sticky left-0 z-10 bg-blue-50 dark:!bg-blue-900 w-12"
|
||||
: "w-12"
|
||||
? "sticky left-0 z-10 bg-blue-50 dark:!bg-blue-900"
|
||||
: ""
|
||||
}`}
|
||||
style={{ width: '40px', maxWidth: '40px', minWidth: '40px' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -2334,24 +2378,23 @@ const TableEditor = () => {
|
||||
{columns.map((column, index) => {
|
||||
const isFrozen = index < frozenColumns;
|
||||
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;
|
||||
|
||||
return (
|
||||
<td
|
||||
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
|
||||
? "sticky z-10 bg-blue-50 dark:!bg-blue-900 min-w-[150px]"
|
||||
? "sticky z-10 bg-blue-50 dark:!bg-blue-900"
|
||||
: ""
|
||||
}`}
|
||||
style={
|
||||
isFrozen
|
||||
? {
|
||||
left: `${leftOffset}px`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
style={{
|
||||
width: `${getColumnWidth(column.id)}px`,
|
||||
minWidth: `${getColumnWidth(column.id)}px`,
|
||||
maxWidth: `${getColumnWidth(column.id)}px`,
|
||||
...(isFrozen ? { left: `${leftOffset}px` } : {})
|
||||
}}
|
||||
>
|
||||
{editingCell?.rowId === row.id &&
|
||||
editingCell?.columnId === column.id ? (
|
||||
@@ -2478,24 +2521,19 @@ const TableEditor = () => {
|
||||
|
||||
{/* System Row - Add Row */}
|
||||
<tr className="border-t-2 border-dashed border-gray-300 dark:border-gray-600">
|
||||
{/* Sticky Add Row button on the left */}
|
||||
<td className="sticky left-0 bg-white dark:bg-gray-800 z-20 py-4 px-4">
|
||||
<td
|
||||
colSpan={columns.length + 1}
|
||||
className="py-4 px-4 relative"
|
||||
>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-sm font-medium">Add Row</span>
|
||||
</button>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -155,13 +155,6 @@ const TermsOfService = () => {
|
||||
|
||||
</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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user