feat: comprehensive SEO optimization and GDPR compliance

- Added Terms of Service and Privacy Policy pages with contact info
- Implemented Google Analytics with Consent Mode v2 for GDPR compliance
- Created sitemap.xml and robots.txt for search engine optimization
- Added dynamic meta tags, Open Graph, and structured data (JSON-LD)
- Implemented GDPR consent banner with TCF 2.2 compatibility
- Enhanced sidebar with category-colored hover states and proper active/inactive styling
- Fixed all ESLint warnings for clean deployment
- Added comprehensive SEO utilities and privacy-first analytics tracking

Ready for production deployment with full legal compliance and SEO optimization.
This commit is contained in:
dwindown
2025-09-24 00:12:28 +07:00
parent dd03a7213f
commit 2e67a2bca2
19 changed files with 2327 additions and 287 deletions

29
public/robots.txt Normal file
View File

@@ -0,0 +1,29 @@
# Robots.txt for https://dewe.dev
# Generated automatically
User-agent: *
Allow: /
# Sitemap location
Sitemap: https://dewe.dev/sitemap.xml
# Block any future admin or private routes
Disallow: /admin/
Disallow: /api/
Disallow: /.well-known/
# Allow all major search engines
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Slurp
Allow: /
User-agent: DuckDuckBot
Allow: /
# Crawl delay for politeness
Crawl-delay: 1

66
public/sitemap.xml Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://dewe.dev/</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://dewe.dev/object-editor</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/table-editor</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/url</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/base64</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/beautifier</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/diff</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/text-length</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/privacy</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://dewe.dev/terms</loc>
<lastmod>2025-01-23</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout'; import Layout from './components/Layout';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
@@ -13,10 +13,18 @@ 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 TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy';
import { initGA } from './utils/analytics';
import './index.css'; import './index.css';
function App() { function App() {
// Initialize Google Analytics on app startup
useEffect(() => {
initGA();
}, []);
return ( return (
<ErrorBoundary> <ErrorBoundary>
<Router> <Router>
@@ -33,7 +41,8 @@ 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="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
</Routes> </Routes>
</Layout> </Layout>
</Router> </Router>

View File

@@ -0,0 +1,191 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { X, Shield, Settings, Check } from 'lucide-react';
import {
shouldShowConsentBanner,
updateConsent,
CONSENT_CONFIGS,
getConsentBannerData
} from '../utils/consentManager';
const ConsentBanner = () => {
const [isVisible, setIsVisible] = useState(false);
const [showCustomize, setShowCustomize] = useState(false);
const [customConsent, setCustomConsent] = useState({
analytics_storage: false,
ad_storage: false,
ad_personalization: false,
ad_user_data: false
});
const bannerData = getConsentBannerData();
useEffect(() => {
setIsVisible(shouldShowConsentBanner());
}, []);
const handleAcceptAll = () => {
updateConsent(CONSENT_CONFIGS.ACCEPT_ALL);
setIsVisible(false);
};
const handleEssentialOnly = () => {
updateConsent(CONSENT_CONFIGS.ESSENTIAL_ONLY);
setIsVisible(false);
};
const handleCustomSave = () => {
const consentChoices = {
necessary: 'granted',
analytics_storage: customConsent.analytics_storage ? 'granted' : 'denied',
ad_storage: customConsent.ad_storage ? 'granted' : 'denied',
ad_personalization: customConsent.ad_personalization ? 'granted' : 'denied',
ad_user_data: customConsent.ad_user_data ? 'granted' : 'denied'
};
updateConsent(consentChoices);
setIsVisible(false);
};
const toggleCustomConsent = (category) => {
setCustomConsent(prev => ({
...prev,
[category]: !prev[category]
}));
};
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-t border-slate-200 dark:border-slate-700 shadow-2xl">
<div className="max-w-7xl mx-auto p-4 sm:p-6">
{!showCustomize ? (
// Main consent banner
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-4">
<div className="flex items-start gap-3 flex-1">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex-shrink-0">
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-slate-800 dark:text-white mb-1">
{bannerData.title}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
{bannerData.description}
</p>
<div className="flex flex-wrap gap-2 text-xs">
<Link
to="/privacy"
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 underline"
>
Privacy Policy
</Link>
<span className="text-slate-400"></span>
<Link
to="/terms"
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 underline"
>
Terms of Service
</Link>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 lg:flex-shrink-0">
<button
onClick={handleEssentialOnly}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
>
Essential Only
</button>
<button
onClick={() => setShowCustomize(true)}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors flex items-center gap-2"
>
<Settings className="h-4 w-4" />
Customize
</button>
<button
onClick={handleAcceptAll}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors flex items-center gap-2"
>
<Check className="h-4 w-4" />
Accept All
</button>
</div>
</div>
) : (
// Customization panel
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800 dark:text-white">
Customize Cookie Preferences
</h3>
<button
onClick={() => setShowCustomize(false)}
className="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-4 mb-6">
{bannerData.purposes.map(purpose => (
<div key={purpose.id} className="flex items-start gap-3">
<div className="flex items-center h-5">
{purpose.required ? (
<div className="w-4 h-4 bg-green-500 rounded border flex items-center justify-center">
<Check className="h-3 w-3 text-white" />
</div>
) : (
<input
type="checkbox"
id={purpose.id}
checked={customConsent[purpose.id] || false}
onChange={() => toggleCustomConsent(purpose.id)}
className="w-4 h-4 text-blue-600 bg-slate-100 border-slate-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600"
/>
)}
</div>
<div className="flex-1">
<label
htmlFor={purpose.id}
className="text-sm font-medium text-slate-800 dark:text-white cursor-pointer"
>
{purpose.name}
{purpose.required && (
<span className="ml-1 text-xs text-green-600 dark:text-green-400">
(Required)
</span>
)}
</label>
<p className="text-xs text-slate-600 dark:text-slate-300 mt-1">
{purpose.description}
</p>
</div>
</div>
))}
</div>
<div className="flex flex-col sm:flex-row gap-2 justify-end">
<button
onClick={handleEssentialOnly}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
>
Essential Only
</button>
<button
onClick={handleCustomSave}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
Save Preferences
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default ConsentBanner;

View File

@@ -1,8 +1,12 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3, Table } from 'lucide-react'; import { Home, Menu, X, ChevronDown, Terminal, Sparkles } from 'lucide-react';
import ThemeToggle from './ThemeToggle'; import ThemeToggle from './ThemeToggle';
import ToolSidebar from './ToolSidebar'; import ToolSidebar from './ToolSidebar';
import SEOHead from './SEOHead';
import ConsentBanner from './ConsentBanner';
import { TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
const Layout = ({ children }) => { const Layout = ({ children }) => {
const location = useLocation(); const location = useLocation();
@@ -10,6 +14,9 @@ const Layout = ({ children }) => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
// Initialize analytics tracking
useAnalytics();
const isActive = (path) => { const isActive = (path) => {
return location.pathname === path; return location.pathname === path;
}; };
@@ -34,30 +41,27 @@ const Layout = ({ children }) => {
setIsDropdownOpen(false); setIsDropdownOpen(false);
}, [location.pathname]); }, [location.pathname]);
const tools = [
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },
];
// Check if we're on a tool page (not homepage) // Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/'; const isToolPage = location.pathname !== '/';
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col">
{/* SEO Head Management */}
<SEOHead />
{/* Header */} {/* Header */}
<header className="sticky top-0 z-50 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-shrink-0"> <header className="sticky top-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
<Link to="/" className="flex items-center space-x-2"> <Link to="/" className="flex items-center space-x-3 group">
<Code2 className="h-8 w-8 text-primary-600" /> <div className="relative">
<span className="text-xl font-bold text-gray-900 dark:text-white"> <div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
DevTools <div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
<Terminal className="h-6 w-6 text-white" />
</div>
</div>
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span> </span>
</Link> </Link>
@@ -67,10 +71,10 @@ const Layout = ({ children }) => {
<nav className="hidden md:flex items-center space-x-6"> <nav className="hidden md:flex items-center space-x-6">
<Link <Link
to="/" to="/"
className={`flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${ className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/') isActive('/')
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' ? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white' : '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-4 w-4" /> <Home className="h-4 w-4" />
@@ -81,38 +85,49 @@ const Layout = ({ children }) => {
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<button <button
onClick={() => setIsDropdownOpen(!isDropdownOpen)} onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors" className="flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium 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 transition-all duration-300"
> >
<Sparkles className="h-4 w-4" />
<span>Tools</span> <span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform ${ <ChevronDown className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? 'rotate-180' : '' isDropdownOpen ? 'rotate-180' : ''
}`} /> }`} />
</button> </button>
{/* Dropdown Menu */} {/* Dropdown Menu */}
{isDropdownOpen && ( {isDropdownOpen && (
<div className="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-50"> <div className="absolute top-full left-0 mt-3 w-80 bg-white/90 dark:bg-slate-800/90 backdrop-blur-md rounded-2xl shadow-2xl border border-slate-200/50 dark:border-slate-700/50 py-3 z-50 overflow-hidden">
{tools.map((tool) => { <div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-purple-50/50 dark:from-slate-800/50 dark:to-slate-700/50"></div>
const IconComponent = tool.icon; <div className="relative">
return ( {TOOLS.map((tool) => {
<Link const IconComponent = tool.icon;
key={tool.path} const categoryConfig = getCategoryConfig(tool.category);
to={tool.path}
onClick={() => setIsDropdownOpen(false)} return (
className={`flex items-center space-x-3 px-4 py-3 text-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${ <Link
isActive(tool.path) key={tool.path}
? 'bg-primary-50 text-primary-700 dark:bg-primary-900 dark:text-primary-300' to={tool.path}
: 'text-gray-700 dark:text-gray-300' onClick={() => setIsDropdownOpen(false)}
}`} className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 ${
> isActive(tool.path)
<IconComponent className="h-4 w-4" /> ? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
<div> : 'text-slate-700 dark:text-slate-300'
<div className="font-medium">{tool.name}</div> }`}
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div> >
</div> <div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}>
</Link> <IconComponent className="h-4 w-4 text-white" />
); </div>
})} <div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-400" />
</div>
</Link>
);
})}
</div>
</div> </div>
)} )}
</div> </div>
@@ -124,7 +139,7 @@ const Layout = ({ children }) => {
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="md:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors" className="md:hidden p-2 rounded-xl 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 transition-all duration-300"
> >
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />} {isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button> </button>
@@ -135,43 +150,48 @@ const Layout = ({ children }) => {
{/* Mobile Navigation Menu */} {/* Mobile Navigation Menu */}
{isMobileMenuOpen && ( {isMobileMenuOpen && (
<div className="md:hidden bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> <div className="md:hidden bg-white/90 dark:bg-slate-800/90 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg">
<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 <Link
to="/" to="/"
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${ className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/') isActive('/')
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' ? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white' : '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" /> <Home className="h-5 w-5" />
<span>Home</span> <span>Home</span>
</Link> </Link>
<div className="border-t border-gray-200 dark:border-gray-700 pt-2 mt-2"> <div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-3 py-1"> <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">
<Sparkles className="h-3 w-3" />
{isToolPage ? 'Switch Tools' : 'Tools'} {isToolPage ? 'Switch Tools' : 'Tools'}
</div> </div>
{tools.map((tool) => { {TOOLS.map((tool) => {
const IconComponent = tool.icon; const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return ( return (
<Link <Link
key={tool.path} key={tool.path}
to={tool.path} to={tool.path}
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${ className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive(tool.path) isActive(tool.path)
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' ? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white' : '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'
}`} }`}
> >
<IconComponent className="h-5 w-5" /> <div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}>
<div> <IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div> <div className="font-medium">{tool.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div> <div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
</div> </div>
</Link> </Link>
); );
@@ -199,24 +219,97 @@ const Layout = ({ children }) => {
{children} {children}
</div> </div>
{/* Footer for tool pages - inside scrollable content */} {/* Footer for tool pages - inside scrollable content */}
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center text-gray-600 dark:text-gray-400"> <div className="text-center">
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p> <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>
</div> </div>
</footer> </footer>
</div> </div>
) : ( ) : (
<div className="flex-1"> <div className="flex-1">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> {children}
{children}
</div>
{/* Footer for homepage */} {/* Footer for homepage */}
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16"> <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-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center text-gray-600 dark:text-gray-400"> <div className="text-center">
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p> <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>
</div> </div>
</footer> </footer>
@@ -224,6 +317,9 @@ const Layout = ({ children }) => {
)} )}
</main> </main>
</div> </div>
{/* GDPR Consent Banner */}
<ConsentBanner />
</div> </div>
); );
}; };

75
src/components/SEOHead.js Normal file
View File

@@ -0,0 +1,75 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { generateMetaTags } from '../utils/seo';
// SEO Head component that manually updates document head
// This works without additional dependencies until we add react-helmet-async
const SEOHead = () => {
const location = useLocation();
useEffect(() => {
const { title, meta, link, structuredData } = generateMetaTags(location.pathname);
// Update document title
document.title = title;
// Remove existing meta tags that we manage
const existingMeta = document.querySelectorAll('meta[data-seo="true"]');
existingMeta.forEach(tag => tag.remove());
const existingLinks = document.querySelectorAll('link[data-seo="true"]');
existingLinks.forEach(tag => tag.remove());
const existingStructuredData = document.querySelectorAll('script[data-seo="structured-data"]');
existingStructuredData.forEach(tag => tag.remove());
// Add new meta tags
meta.forEach(({ name, property, content }) => {
const metaTag = document.createElement('meta');
if (name) metaTag.setAttribute('name', name);
if (property) metaTag.setAttribute('property', property);
metaTag.setAttribute('content', content);
metaTag.setAttribute('data-seo', 'true');
document.head.appendChild(metaTag);
});
// Add canonical link
link.forEach(({ rel, href }) => {
const linkTag = document.createElement('link');
linkTag.setAttribute('rel', rel);
linkTag.setAttribute('href', href);
linkTag.setAttribute('data-seo', 'true');
document.head.appendChild(linkTag);
});
// Add structured data
if (structuredData) {
const script = document.createElement('script');
script.type = 'application/ld+json';
script.setAttribute('data-seo', 'structured-data');
script.textContent = JSON.stringify(structuredData);
document.head.appendChild(script);
}
// Add preconnect for performance
const preconnectLinks = [
'https://www.googletagmanager.com',
'https://www.google-analytics.com'
];
preconnectLinks.forEach(href => {
if (!document.querySelector(`link[rel="preconnect"][href="${href}"]`)) {
const preconnect = document.createElement('link');
preconnect.rel = 'preconnect';
preconnect.href = href;
preconnect.setAttribute('data-seo', 'true');
document.head.appendChild(preconnect);
}
});
}, [location.pathname]);
return null; // This component doesn't render anything
};
export default SEOHead;

View File

@@ -146,18 +146,28 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
} else if (currentValue === null) { } else if (currentValue === null) {
current[key] = value === 'null' ? null : value; current[key] = value === 'null' ? null : value;
} else { } else {
// For strings and initial empty values, use auto-detection // For strings and initial empty values, use smart detection
if (currentValue === '' || currentValue === undefined) { if (currentValue === '' || currentValue === undefined) {
if (value === 'true' || value === 'false') { // Check if this is a newly added property (starts with "property" + number)
current[key] = value === 'true'; const isNewProperty = typeof key === 'string' && key.match(/^property\d+$/);
} else if (value === 'null') {
current[key] = null; if (isNewProperty) {
} else if (!isNaN(value) && value !== '' && value.trim() !== '') { // New properties added by user are always strings (no auto-detection)
current[key] = Number(value);
} else {
current[key] = value; current[key] = value;
} else {
// Existing properties from loaded data - use auto-detection
if (value === 'true' || value === 'false') {
current[key] = value === 'true';
} else if (value === 'null') {
current[key] = null;
} else if (!isNaN(value) && value !== '' && value.trim() !== '') {
current[key] = Number(value);
} else {
current[key] = value;
}
} }
} else { } else {
// Existing non-empty values - preserve as string unless user explicitly changes type
current[key] = value; current[key] = value;
} }
} }

View File

@@ -1,28 +1,97 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { getCategoryConfig } from '../config/tools';
const ToolCard = ({ icon: Icon, title, description, path, tags, category }) => {
const categoryConfig = getCategoryConfig(category);
// Define explicit hover classes for Tailwind CSS purging
const getHoverClasses = (category) => {
switch (category) {
case 'editor':
return {
border: 'hover:border-blue-300 dark:hover:border-blue-500',
shadow: 'hover:shadow-blue-500/20',
titleColor: 'group-hover:text-blue-600 dark:group-hover:text-blue-400',
arrowColor: 'group-hover:text-blue-600',
badgeColor: 'group-hover:bg-blue-100 dark:group-hover:bg-blue-900/30 group-hover:text-blue-700 dark:group-hover:text-blue-300'
};
case 'encoder':
return {
border: 'hover:border-purple-300 dark:hover:border-purple-500',
shadow: 'hover:shadow-purple-500/20',
titleColor: 'group-hover:text-purple-600 dark:group-hover:text-purple-400',
arrowColor: 'group-hover:text-purple-600',
badgeColor: 'group-hover:bg-purple-100 dark:group-hover:bg-purple-900/30 group-hover:text-purple-700 dark:group-hover:text-purple-300'
};
case 'formatter':
return {
border: 'hover:border-green-300 dark:hover:border-green-500',
shadow: 'hover:shadow-green-500/20',
titleColor: 'group-hover:text-green-600 dark:group-hover:text-green-400',
arrowColor: 'group-hover:text-green-600',
badgeColor: 'group-hover:bg-green-100 dark:group-hover:bg-green-900/30 group-hover:text-green-700 dark:group-hover:text-green-300'
};
case 'analyzer':
return {
border: 'hover:border-orange-300 dark:hover:border-orange-500',
shadow: 'hover:shadow-orange-500/20',
titleColor: 'group-hover:text-orange-600 dark:group-hover:text-orange-400',
arrowColor: 'group-hover:text-orange-600',
badgeColor: 'group-hover:bg-orange-100 dark:group-hover:bg-orange-900/30 group-hover:text-orange-700 dark:group-hover:text-orange-300'
};
default:
return {
border: 'hover:border-slate-300 dark:hover:border-slate-500',
shadow: 'hover:shadow-slate-500/20',
titleColor: 'group-hover:text-slate-600 dark:group-hover:text-slate-400',
arrowColor: 'group-hover:text-slate-600',
badgeColor: 'group-hover:bg-slate-100 dark:group-hover:bg-slate-700 group-hover:text-slate-700 dark:group-hover:text-slate-300'
};
}
};
const hoverClasses = getHoverClasses(category);
const ToolCard = ({ icon: Icon, title, description, path, tags }) => {
return ( return (
<Link to={path} className="block"> <Link to={path} className="block group">
<div className="tool-card group cursor-pointer"> <div className={`relative overflow-hidden rounded-2xl bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border border-slate-200 dark:border-slate-700 ${hoverClasses.border} transition-all duration-300 hover:shadow-2xl ${hoverClasses.shadow} hover:-translate-y-1`}>
<div className="flex items-start space-x-4"> {/* Gradient overlay on hover */}
<div className="flex-shrink-0"> <div className={`absolute inset-0 bg-gradient-to-br ${categoryConfig.color} opacity-0 group-hover:opacity-5 transition-opacity duration-300`}></div>
<Icon className="h-8 w-8 text-primary-600" />
<div className="relative p-6">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className={`flex-shrink-0 p-3 rounded-xl bg-gradient-to-br ${categoryConfig.color} shadow-lg group-hover:scale-110 transition-transform duration-300`}>
<Icon className="h-6 w-6 text-white" />
</div>
<div className="flex-shrink-0 ml-4">
<ArrowRight className={`h-5 w-5 text-slate-400 ${hoverClasses.arrowColor} group-hover:translate-x-1 transition-all duration-300`} />
</div>
</div> </div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-primary-600 transition-colors"> {/* Content */}
{title} <div className="space-y-3">
</h3> <div className="flex items-center gap-2">
<p className="text-gray-600 dark:text-gray-300 mt-1"> <h3 className={`text-xl font-bold text-slate-800 dark:text-white ${hoverClasses.titleColor} transition-colors`}>
{title}
</h3>
<span className={`px-2 py-1 text-xs font-medium bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded-full ${hoverClasses.badgeColor} transition-colors`}>
{categoryConfig.name}
</span>
</div>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed group-hover:text-slate-700 dark:group-hover:text-slate-200 transition-colors">
{description} {description}
</p> </p>
{tags && (
<div className="flex flex-wrap gap-2 mt-3"> {tags && tags.length > 0 && (
<div className="flex flex-wrap gap-2 pt-2">
{tags.map((tag, index) => ( {tags.map((tag, index) => (
<span <span
key={index} key={index}
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full" className="px-3 py-1 text-xs font-medium bg-slate-50 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400 rounded-full border border-slate-200 dark:border-slate-600 group-hover:border-slate-300 dark:group-hover:border-slate-500 transition-colors"
> >
{tag} {tag}
</span> </span>
@@ -30,9 +99,6 @@ const ToolCard = ({ icon: Icon, title, description, path, tags }) => {
</div> </div>
)} )}
</div> </div>
<div className="flex-shrink-0">
<ArrowRight className="h-5 w-5 text-gray-400 group-hover:text-primary-600 transition-colors" />
</div>
</div> </div>
</div> </div>
</Link> </Link>

View File

@@ -1,24 +1,14 @@
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, LinkIcon, Hash, Table, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react'; import { Search, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
import { NAVIGATION_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 tools = [ const filteredTools = NAVIGATION_TOOLS.filter(tool =>
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },
];
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())
); );
@@ -26,69 +16,220 @@ const ToolSidebar = () => {
const isActive = (path) => location.pathname === path; const isActive = (path) => location.pathname === path;
return ( return (
<div className={`bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 sticky top-16 ${ <div className={`bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 transition-all duration-300 sticky top-16 ${
isCollapsed ? 'w-16' : 'w-64' isCollapsed ? 'w-16' : 'w-64'
}`} style={{ height: 'calc(100vh - 4rem)' }}> }`} style={{ height: 'calc(100vh - 4rem)' }}>
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
{/* Sidebar Header */} {/* Sidebar Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700"> <div className="p-4 border-b border-slate-200/50 dark:border-slate-700/50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{!isCollapsed && ( {!isCollapsed && (
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> <div className="flex items-center gap-2">
Tools <Sparkles className="h-4 w-4 text-blue-500" />
</h2> <h2 className="text-lg font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Tools
</h2>
</div>
)} )}
<button <button
onClick={() => setIsCollapsed(!isCollapsed)} onClick={() => setIsCollapsed(!isCollapsed)}
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" className="p-2 rounded-xl hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 group"
> >
{isCollapsed ? ( {isCollapsed ? (
<ChevronRight className="h-4 w-4 text-gray-500" /> <ChevronRight className="h-4 w-4 text-slate-500 group-hover:text-blue-500 transition-colors" />
) : ( ) : (
<ChevronLeft className="h-4 w-4 text-gray-500" /> <ChevronLeft className="h-4 w-4 text-slate-500 group-hover:text-blue-500 transition-colors" />
)} )}
</button> </button>
</div> </div>
{/* Search - only show when not collapsed */} {/* Search - only show when not collapsed */}
{!isCollapsed && ( {!isCollapsed && (
<div className="relative mt-3"> <div className="relative mt-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <div className="absolute inset-0 bg-gradient-to-r from-blue-500/10 to-purple-500/10 rounded-xl blur opacity-50"></div>
<input <div className="relative">
type="text" <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
placeholder="Search tools..." <input
value={searchTerm} type="text"
onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search tools..."
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent" value={searchTerm}
/> onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-3 py-2.5 text-sm border border-slate-200 dark:border-slate-600 rounded-xl bg-white/80 dark:bg-slate-700/80 backdrop-blur-sm text-slate-900 dark:text-slate-100 placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300"
/>
</div>
</div> </div>
)} )}
</div> </div>
{/* Tools List */} {/* Tools List */}
<div className="flex-1 overflow-y-auto py-2"> <div className="flex-1 overflow-y-auto py-3">
<nav className="space-y-1 px-2"> <nav className="space-y-2 px-3">
{filteredTools.map((tool) => { {filteredTools.map((tool) => {
const IconComponent = tool.icon; const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const isHome = tool.path === '/';
// Get category-specific colors for active states
const getActiveClasses = (category, isHome) => {
if (isHome) {
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
titleColor: 'text-slate-700 dark:text-slate-300',
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600' // Active icon has colored background
};
}
switch (category) {
case 'editor':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30',
titleColor: 'text-blue-700 dark:text-blue-300',
iconBg: 'bg-gradient-to-br from-blue-500 to-cyan-500'
};
case 'encoder':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30',
titleColor: 'text-purple-700 dark:text-purple-300',
iconBg: 'bg-gradient-to-br from-purple-500 to-pink-500'
};
case 'formatter':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30',
titleColor: 'text-green-700 dark:text-green-300',
iconBg: 'bg-gradient-to-br from-green-500 to-emerald-500'
};
case 'analyzer':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30',
titleColor: 'text-orange-700 dark:text-orange-300',
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
};
default:
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
titleColor: 'text-slate-700 dark:text-slate-300',
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600'
};
}
};
const getInactiveClasses = (category, isHome) => {
if (isHome) {
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-slate-700 dark:group-hover:text-slate-300',
iconBorder: 'border-2 border-slate-300 dark:border-slate-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-slate-500 group-hover:to-slate-600', // Hover: colored background
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white' // Hover: white icon
};
}
switch (category) {
case 'editor':
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-blue-600 dark:group-hover:text-blue-400',
iconBorder: 'border-2 border-blue-300 dark:border-blue-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-blue-500 group-hover:to-cyan-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'encoder':
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-purple-600 dark:group-hover:text-purple-400',
iconBorder: 'border-2 border-purple-300 dark:border-purple-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-purple-500 group-hover:to-pink-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'formatter':
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-green-600 dark:group-hover:text-green-400',
iconBorder: 'border-2 border-green-300 dark:border-green-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-green-500 group-hover:to-emerald-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'analyzer':
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-orange-600 dark:group-hover:text-orange-400',
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'
};
default:
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-slate-700 dark:group-hover:text-slate-300',
iconBorder: 'border-2 border-slate-300 dark:border-slate-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-slate-500 group-hover:to-slate-600',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
}
};
const activeClasses = getActiveClasses(tool.category, isHome);
const inactiveClasses = getInactiveClasses(tool.category, isHome);
return ( return (
<Link <Link
key={tool.path} key={tool.path}
to={tool.path} to={tool.path}
className={`group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${ className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 ${
isActive(tool.path) isActiveItem
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300' ? isCollapsed
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700' ? activeClasses.collapsed + ' justify-center py-3' // Center for folded
: activeClasses.expanded + ' shadow-lg px-3 py-3'
: isCollapsed
? inactiveClasses.collapsed + ' justify-center py-3' // Center for folded
: inactiveClasses.expanded + ' px-3 py-3'
}`} }`}
title={isCollapsed ? tool.name : ''} title={isCollapsed ? tool.name : ''}
> >
<IconComponent className={`h-5 w-5 ${isCollapsed ? '' : 'mr-3'} flex-shrink-0`} /> {isCollapsed ? (
{!isCollapsed && ( // Folded sidebar - clean icon squares only, centered
<div className="flex-1 min-w-0"> <div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
<div className="font-medium truncate">{tool.name}</div> isActiveItem
<div className="text-xs text-gray-500 dark:text-gray-400 truncate"> ? activeClasses.iconBg + ' p-3' // Active: bigger padding (no border)
{tool.description} : inactiveClasses.iconBorder + ' p-2' // Inactive: normal padding (has border)
</div> }`}>
<IconComponent className={`${
isActiveItem
? 'h-5 w-5 text-white' // Active: bigger icon, white
: 'h-4 w-4 ' + inactiveClasses.iconColor // Inactive: normal size, grayscale/hover
}`} />
</div> </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
? activeClasses.iconBg // Active: colored background
: inactiveClasses.iconBorder // Inactive: transparent with colored border
}`}>
<IconComponent className={`h-4 w-4 ${
isActiveItem
? 'text-white' // Active: white icon
: inactiveClasses.iconColor // Inactive: grayscale icon
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${
isActiveItem ? activeClasses.titleColor : inactiveClasses.titleColor
}`}>
{tool.name}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
{tool.description}
</div>
</div>
</>
)} )}
</Link> </Link>
); );
@@ -98,9 +239,18 @@ const ToolSidebar = () => {
{/* Footer */} {/* Footer */}
{!isCollapsed && ( {!isCollapsed && (
<div className="p-4 border-t border-gray-200 dark:border-gray-700"> <div className="p-4 border-t border-slate-200/50 dark:border-slate-700/50">
<div className="text-xs text-gray-500 dark:text-gray-400 text-center"> <div className="text-center">
Quick access to all tools <div className="flex items-center justify-center gap-2 mb-2">
<div className="w-1.5 h-1.5 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full animate-pulse"></div>
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">
Quick Access
</span>
<div className="w-1.5 h-1.5 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500">
{SITE_CONFIG.totalTools} tools available
</p>
</div> </div>
</div> </div>
)} )}

137
src/config/tools.js Normal file
View File

@@ -0,0 +1,137 @@
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home } from 'lucide-react';
// Master tools configuration - single source of truth
export const TOOL_CATEGORIES = {
navigation: {
name: 'Navigation',
color: 'from-slate-500 to-slate-600',
hoverColor: 'slate-600',
textColor: 'text-slate-600',
hoverTextColor: 'hover:text-slate-700 dark:hover:text-slate-400'
},
editor: {
name: 'Editor',
color: 'from-blue-500 to-cyan-500',
hoverColor: 'blue-600',
textColor: 'text-blue-600',
hoverTextColor: 'hover:text-blue-700 dark:hover:text-blue-400'
},
encoder: {
name: 'Encoder',
color: 'from-purple-500 to-pink-500',
hoverColor: 'purple-600',
textColor: 'text-purple-600',
hoverTextColor: 'hover:text-purple-700 dark:hover:text-purple-400'
},
formatter: {
name: 'Formatter',
color: 'from-green-500 to-emerald-500',
hoverColor: 'green-600',
textColor: 'text-green-600',
hoverTextColor: 'hover:text-green-700 dark:hover:text-green-400'
},
analyzer: {
name: 'Analyzer',
color: 'from-orange-500 to-red-500',
hoverColor: 'orange-600',
textColor: 'text-orange-600',
hoverTextColor: 'hover:text-orange-700 dark:hover:text-orange-400'
}
};
export const TOOLS = [
{
path: '/object-editor',
name: 'Object Editor',
icon: Edit3,
description: 'Visual editor for JSON and PHP serialized objects with mindmap visualization',
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor'],
category: 'editor'
},
{
path: '/table-editor',
name: 'Table Editor',
icon: Table,
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON',
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'],
category: 'editor'
},
{
path: '/url',
name: 'URL Encoder/Decoder',
icon: LinkIcon,
description: 'Encode and decode URLs and query parameters',
tags: ['URL', 'Encode', 'Decode'],
category: 'encoder'
},
{
path: '/base64',
name: 'Base64 Encoder/Decoder',
icon: Hash,
description: 'Convert text to Base64 and back with support for files',
tags: ['Base64', 'Encode', 'Binary'],
category: 'encoder'
},
{
path: '/beautifier',
name: 'Code Beautifier/Minifier',
icon: Wand2,
description: 'Format and minify JSON, XML, SQL, CSS, and HTML code',
tags: ['Format', 'Minify', 'Beautify'],
category: 'formatter'
},
{
path: '/diff',
name: 'Text Diff Checker',
icon: GitCompare,
description: 'Compare two texts and highlight differences line by line',
tags: ['Diff', 'Compare', 'Text'],
category: 'analyzer'
},
{
path: '/text-length',
name: 'Text Length Checker',
icon: Type,
description: 'Analyze text length, word count, and other text statistics',
tags: ['Text', 'Length', 'Statistics'],
category: 'analyzer'
}
];
// Navigation tools (for sidebar)
export const NAVIGATION_TOOLS = [
{
path: '/',
name: 'Home',
icon: Home,
description: 'Back to homepage',
category: 'navigation'
},
...TOOLS
];
// Site configuration
export const SITE_CONFIG = {
domain: 'https://dewe.dev',
title: 'Dewe.Dev',
subtitle: 'Professional Developer Utilities',
slogan: 'Code faster, debug smarter, ship better',
description: 'Professional-grade utilities for modern developers',
year: new Date().getFullYear(),
totalTools: TOOLS.length
};
// Helper functions
export const getCategoryConfig = (categoryKey) => TOOL_CATEGORIES[categoryKey] || TOOL_CATEGORIES.navigation;
export const getToolsByCategory = (categoryKey) => TOOLS.filter(tool => tool.category === categoryKey);
export const getCategoryStats = () => {
const stats = {};
Object.keys(TOOL_CATEGORIES).forEach(key => {
if (key !== 'navigation') {
stats[key] = getToolsByCategory(key).length;
}
});
return stats;
};

60
src/hooks/useAnalytics.js Normal file
View File

@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { trackPageView, trackToolUsage, trackSearch, trackThemeChange } from '../utils/analytics';
// Custom hook for analytics tracking in React components
export const useAnalytics = () => {
const location = useLocation();
// Track page views on route changes
useEffect(() => {
const path = location.pathname;
let title = 'Dewe.Dev';
// Generate meaningful page titles
switch (path) {
case '/':
title = 'Dewe.Dev - Professional Developer Utilities';
break;
case '/object-editor':
title = 'Object Editor - Dewe.Dev';
break;
case '/table-editor':
title = 'Table Editor - Dewe.Dev';
break;
case '/url':
title = 'URL Encoder/Decoder - Dewe.Dev';
break;
case '/base64':
title = 'Base64 Encoder/Decoder - Dewe.Dev';
break;
case '/beautifier':
title = 'Code Beautifier/Minifier - Dewe.Dev';
break;
case '/diff':
title = 'Text Diff Checker - Dewe.Dev';
break;
case '/text-length':
title = 'Text Length Checker - Dewe.Dev';
break;
case '/privacy':
title = 'Privacy Policy - Dewe.Dev';
break;
case '/terms':
title = 'Terms of Service - Dewe.Dev';
break;
default:
title = `${path} - Dewe.Dev`;
}
// Track the page view
trackPageView(path, title);
}, [location]);
// Return tracking functions for components to use
return {
trackToolUsage,
trackSearch,
trackThemeChange,
};
};

View File

@@ -1,152 +1,187 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3, Table } from 'lucide-react'; import { Search, Code, Terminal, Zap, Shield, Cpu } from 'lucide-react';
import ToolCard from '../components/ToolCard'; import ToolCard from '../components/ToolCard';
import { TOOLS, SITE_CONFIG } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
const Home = () => { const Home = () => {
console.log('🏠 NEW Home component loaded - Object Editor should be visible!'); console.log('🏠 NEW Home component loaded - Object Editor should be visible!');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [mounted, setMounted] = useState(false);
const { trackSearch } = useAnalytics();
const tools = [ useEffect(() => {
{ setMounted(true);
icon: Edit3, }, []);
title: 'Object Editor',
description: 'Visual editor for JSON and PHP serialized objects with mindmap visualization', // Handle search with analytics tracking
path: '/object-editor', const handleSearchChange = (e) => {
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor'] const value = e.target.value;
}, setSearchTerm(value);
{
icon: Table, // Track search after user stops typing (debounced)
title: 'Table Editor', if (value.length > 2) {
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON', trackSearch(value);
path: '/table-editor',
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor']
},
{
icon: Link2,
title: 'URL Encoder/Decoder',
description: 'Encode and decode URLs and query parameters',
path: '/url',
tags: ['URL', 'Encode', 'Decode']
},
{
icon: Hash,
title: 'Base64 Encoder/Decoder',
description: 'Convert text to Base64 and back with support for files',
path: '/base64',
tags: ['Base64', 'Encode', 'Binary']
},
{
icon: FileText,
title: 'Code Beautifier/Minifier',
description: 'Format and minify JSON, XML, SQL, CSS, and HTML code',
path: '/beautifier',
tags: ['Format', 'Minify', 'Beautify']
},
{
icon: GitCompare,
title: 'Text Diff Checker',
description: 'Compare two texts and highlight differences line by line',
path: '/diff',
tags: ['Diff', 'Compare', 'Text']
},
{
icon: Type,
title: 'Text Length Checker',
description: 'Analyze text length, word count, and other text statistics',
path: '/text-length',
tags: ['Text', 'Length', 'Statistics']
} }
]; };
const filteredTools = tools.filter(tool => const filteredTools = TOOLS.filter(tool =>
tool.title.toLowerCase().includes(searchTerm.toLowerCase()) || tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase()) || tool.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())) tool.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
); );
return ( return (
<div className="max-w-6xl mx-auto"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900">
{/* Hero Section */} <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12"> {/* Hero Section */}
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4"> <div className={`text-center pt-16 pb-20 transition-all duration-1000 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
Developer Tools {/* Terminal-style header */}
</h1> <div className="inline-flex items-center gap-2 px-4 py-2 bg-slate-800 dark:bg-slate-700 rounded-full text-green-400 font-mono text-sm mb-8 shadow-lg">
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8"> <Terminal className="h-4 w-4" />
Essential utilities for web developers - fast, local, and easy to use <span>~/dewe.dev $</span>
</p> <span className="animate-pulse">_</span>
{/* Search */}
<div className="relative max-w-md mx-auto">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
{/* Tools Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredTools.map((tool, index) => (
<ToolCard
key={index}
icon={tool.icon}
title={tool.title}
description={tool.description}
path={tool.path}
tags={tool.tags}
/>
))}
</div>
{/* No Results */}
{filteredTools.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400 text-lg">
No tools found matching "{searchTerm}"
</p>
</div>
)}
{/* Features */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center">
<div className="bg-primary-100 dark:bg-primary-900 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<RefreshCw className="h-8 w-8 text-primary-600" />
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Lightning Fast <h1 className="text-5xl md:text-7xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-6">
</h3> {SITE_CONFIG.title}
<p className="text-gray-600 dark:text-gray-300"> </h1>
All processing happens locally in your browser for maximum speed and privacy
<p className="text-xl md:text-2xl text-slate-600 dark:text-slate-300 mb-4 max-w-3xl mx-auto leading-relaxed">
{SITE_CONFIG.subtitle}
</p> </p>
</div>
<p className="text-lg text-slate-500 dark:text-slate-400 mb-12 max-w-2xl mx-auto">
<div className="text-center"> {SITE_CONFIG.slogan} {SITE_CONFIG.description}
<div className="bg-primary-100 dark:bg-primary-900 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4"> </p>
<FileText className="h-8 w-8 text-primary-600" />
{/* Enhanced Search */}
<div className="relative max-w-lg mx-auto mb-8">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl blur opacity-20"></div>
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={handleSearchChange}
className="w-full pl-12 pr-6 py-4 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border border-slate-200 dark:border-slate-600 rounded-2xl text-slate-900 dark:text-slate-100 placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300 shadow-lg"
/>
</div>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Handle Large Files {/* Stats */}
</h3> <div className="flex justify-center items-center gap-8 text-sm text-slate-500 dark:text-slate-400">
<p className="text-gray-600 dark:text-gray-300"> <div className="flex items-center gap-2">
Process large text files and data with ease, no size limitations <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</p> <span>{SITE_CONFIG.totalTools} Tools Available</span>
</div> </div>
<div className="flex items-center gap-2">
<div className="text-center"> <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<div className="bg-primary-100 dark:bg-primary-900 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4"> <span>100% Client-Side</span>
<Code className="h-8 w-8 text-primary-600" /> </div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span>Zero Data Collection</span>
</div>
</div>
</div>
{/* Tools Grid */}
<div className={`transition-all duration-1000 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20">
{filteredTools.map((tool, index) => (
<div
key={index}
className={`transition-all duration-500 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}
style={{ transitionDelay: `${400 + index * 100}ms` }}
>
<ToolCard
icon={tool.icon}
title={tool.name}
description={tool.description}
path={tool.path}
tags={tool.tags}
category={tool.category}
/>
</div>
))}
</div>
</div>
{/* No Results */}
{filteredTools.length === 0 && (
<div className="text-center py-20">
<div className="text-6xl mb-4">🔍</div>
<p className="text-slate-500 dark:text-slate-400 text-xl mb-2">
No tools found matching "{searchTerm}"
</p>
<p className="text-slate-400 dark:text-slate-500">
Try searching for "editor", "encode", or "format"
</p>
</div>
)}
{/* Features Section */}
<div className={`py-20 transition-all duration-1000 delay-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 dark:text-white mb-4">
Built for Developers
</h2>
<p className="text-xl text-slate-600 dark:text-slate-300 max-w-2xl mx-auto">
Every tool is crafted with developer experience and performance in mind
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-300 hover:shadow-xl hover:shadow-blue-500/10">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Zap className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
Lightning Fast
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Optimized algorithms and local processing ensure instant results
</p>
</div>
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/10">
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Shield className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
Privacy First
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Your data never leaves your browser. Zero tracking, zero storage
</p>
</div>
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-green-300 dark:hover:border-green-600 transition-all duration-300 hover:shadow-xl hover:shadow-green-500/10">
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Cpu className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
No Limits
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Handle massive files and complex data without restrictions
</p>
</div>
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-indigo-300 dark:hover:border-indigo-600 transition-all duration-300 hover:shadow-xl hover:shadow-indigo-500/10">
<div className="bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Code className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
Dev Focused
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Syntax highlighting, shortcuts, and workflows developers love
</p>
</div>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Developer Friendly
</h3>
<p className="text-gray-600 dark:text-gray-300">
Clean interface with syntax highlighting and easy copy-paste functionality
</p>
</div> </div>
</div> </div>
</div> </div>

265
src/pages/PrivacyPolicy.js Normal file
View File

@@ -0,0 +1,265 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, Shield, Lock, Eye, Server, Cookie, BarChart3, Globe } from 'lucide-react';
import { SITE_CONFIG } from '../config/tools';
const PrivacyPolicy = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft className="h-4 w-4" />
Back to Home
</Link>
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-gradient-to-br from-green-500 to-emerald-500 rounded-xl shadow-lg">
<Lock className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-800 dark:text-white">
Privacy Policy
</h1>
<p className="text-slate-600 dark:text-slate-300">
Last updated: {new Date().toLocaleDateString()}
</p>
</div>
</div>
</div>
{/* Content */}
<div className="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700 p-8 shadow-xl">
<div className="prose prose-slate dark:prose-invert max-w-none">
{/* Privacy-First Commitment */}
<section className="mb-8">
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 rounded-xl p-6 mb-6">
<h2 className="text-2xl font-bold text-slate-800 dark:text-white mb-4 flex items-center gap-3">
<Shield className="h-6 w-6 text-green-600" />
Our Privacy-First Commitment
</h2>
<p className="text-slate-700 dark:text-slate-200 text-lg leading-relaxed mb-4">
At {SITE_CONFIG.title}, "Privacy-First" isn't just a marketing term—it's our core architectural principle. Your data privacy is protected by design, not by policy alone.
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<Server className="h-5 w-5 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-white">100% Client-Side</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">All processing happens in your browser</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<Lock className="h-5 w-5 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-white">Zero Data Upload</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">Your sensitive data never leaves your device</p>
</div>
</div>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Eye className="h-5 w-5 text-blue-600" />
1. Information We Collect
</h2>
<div className="space-y-6">
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
What We DON'T Collect:
</h3>
<ul className="list-disc list-inside text-red-700 dark:text-red-300 space-y-1 text-sm">
<li>Your input data (JSON, CSV, URLs, text, etc.)</li>
<li>Files you upload or paste into our tools</li>
<li>Personal information (name, email, address)</li>
<li>Login credentials or user accounts</li>
<li>IP addresses or device fingerprints</li>
<li>Browsing history or cross-site tracking</li>
</ul>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
✅ What We DO Collect (via Google Analytics):
</h3>
<ul className="list-disc list-inside text-green-700 dark:text-green-300 space-y-1 text-sm">
<li>Anonymous page views and session duration</li>
<li>Which tools are most popular (aggregated data only)</li>
<li>General geographic region (country/state level)</li>
<li>Browser type and device type (for compatibility)</li>
<li>Referral sources (how you found our site)</li>
</ul>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-purple-600" />
2. Google Analytics Usage
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mb-4">
We use Google Analytics to understand how our tools are used and to improve the service. This helps us answer questions like:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4 mb-4">
<li>Which tools are most helpful to developers?</li>
<li>Are there performance issues on certain devices?</li>
<li>How can we improve the user experience?</li>
</ul>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<p className="text-blue-800 dark:text-blue-200 text-sm">
<strong>Important:</strong> Google Analytics only sees that someone visited "dewe.dev/beautifier" - it never sees the actual JSON code you're beautifying or any data you process with our tools.
</p>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Server className="h-5 w-5 text-indigo-600" />
3. How Our Tools Work
</h2>
<div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-6">
<h3 className="font-semibold text-slate-800 dark:text-white mb-3">Technical Architecture:</h3>
<div className="space-y-3 text-sm">
<div className="flex items-start gap-3">
<span className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 px-2 py-1 rounded text-xs font-medium">CLIENT</span>
<p className="text-slate-600 dark:text-slate-300">Your browser downloads our JavaScript code</p>
</div>
<div className="flex items-start gap-3">
<span className="bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-2 py-1 rounded text-xs font-medium">LOCAL</span>
<p className="text-slate-600 dark:text-slate-300">All processing happens locally in your browser's memory</p>
</div>
<div className="flex items-start gap-3">
<span className="bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-2 py-1 rounded text-xs font-medium">SECURE</span>
<p className="text-slate-600 dark:text-slate-300">No data transmission to our servers for processing</p>
</div>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Cookie className="h-5 w-5 text-orange-600" />
4. Cookies and Local Storage
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mb-4">
We use minimal cookies and local storage for:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4">
<li><strong>Google Analytics:</strong> Anonymous tracking cookies (you can opt-out)</li>
<li><strong>Theme Preference:</strong> Remembering if you prefer dark/light mode</li>
<li><strong>No Personal Data:</strong> We never store your processed data locally</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Globe className="h-5 w-5 text-teal-600" />
5. Future Advertising (Google AdSense)
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 mb-4">
<h3 className="font-semibold text-amber-800 dark:text-amber-200 mb-2">
🔮 Planned Implementation:
</h3>
<p className="text-amber-700 dark:text-amber-300 text-sm leading-relaxed mb-3">
To keep our tools free, we plan to display Google AdSense advertisements. When implemented:
</p>
<ul className="list-disc list-inside text-amber-700 dark:text-amber-300 space-y-1 text-sm">
<li>Ads will be clearly marked and non-intrusive</li>
<li>No impact on tool functionality or performance</li>
<li>Google may use cookies for ad personalization</li>
<li>You can opt-out of personalized ads via Google settings</li>
<li><strong>We will NEVER share your tool usage data with advertisers</strong></li>
</ul>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
6. Your Rights and Controls
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">Analytics Opt-Out:</h3>
<p className="text-blue-700 dark:text-blue-300 text-sm">
Install browser extensions like uBlock Origin or use Google's opt-out tools to disable analytics tracking.
</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">Data Control:</h3>
<p className="text-green-700 dark:text-green-300 text-sm">
Since we don't collect your data, there's nothing to delete or export. Your data stays with you.
</p>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
7. Third-Party Services
</h2>
<div className="space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-semibold text-slate-800 dark:text-white">Google Analytics</h3>
<p className="text-slate-600 dark:text-slate-300 text-sm">
Privacy Policy: <a href="https://policies.google.com/privacy" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://policies.google.com/privacy</a>
</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<h3 className="font-semibold text-slate-800 dark:text-white">Google AdSense (Future)</h3>
<p className="text-slate-600 dark:text-slate-300 text-sm">
Privacy Policy: <a href="https://policies.google.com/privacy" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://policies.google.com/privacy</a>
</p>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
8. Changes to This Policy
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
We may update this privacy policy from time to time. We will notify users of any material changes by updating the "Last updated" date at the top of this policy. Your continued use of the service after any changes constitutes acceptance of the new policy.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
9. Contact Us
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
If you have any questions about this Privacy Policy or our privacy practices, please contact us at{' '}
<a href="mailto:dewe.developer@gmail.com" className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
dewe.developer@gmail.com
</a>
{' '}or visit {SITE_CONFIG.domain}.
</p>
</section>
</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} Your privacy is our priority
</p>
</div>
</div>
</div>
);
};
export default PrivacyPolicy;

View File

@@ -1276,13 +1276,45 @@ const TableEditor = () => {
})), })),
); );
// Auto-scroll to the right to show the new column // Auto-trigger header editing for the new column
setEditingHeader(newColumnId);
// Auto-scroll to the right to show the new column and focus on header input
setTimeout(() => { setTimeout(() => {
const tableContainer = document.querySelector(".overflow-auto"); // Try multiple selectors to find the table container
if (tableContainer) { const selectors = [
tableContainer.scrollLeft = tableContainer.scrollWidth; '[class*="overflow-auto"][class*="max-h-"]',
'.overflow-auto',
'div[class*="overflow-auto"]'
];
let tableContainer = null;
for (const selector of selectors) {
tableContainer = document.querySelector(selector);
if (tableContainer) break;
} }
}, 100);
if (tableContainer) {
// Check if horizontal scrolling is needed
const needsScroll = tableContainer.scrollWidth > tableContainer.clientWidth;
if (needsScroll) {
// Smooth scroll to the far right to show the new column
tableContainer.scrollTo({
left: tableContainer.scrollWidth - tableContainer.clientWidth,
behavior: 'smooth'
});
}
}
// Focus on the header input field after scroll
setTimeout(() => {
const headerInput = document.querySelector(`input[value="${newColumn.name}"]`);
if (headerInput) {
headerInput.focus();
headerInput.select(); // Select all text for easy replacement
}
}, 100);
}, 200);
}; };
// Delete selected rows // Delete selected rows
@@ -2419,7 +2451,7 @@ const TableEditor = () => {
{editingCell?.rowId === row.id && {editingCell?.rowId === row.id &&
editingCell?.columnId === column.id ? ( editingCell?.columnId === column.id ? (
(() => { (() => {
const cellValue = row[column.id] || ""; const cellValue = String(row[column.id] || "");
const isLongValue = const isLongValue =
cellValue.length > 100 || cellValue.length > 100 ||
cellValue.includes("\n"); cellValue.includes("\n");
@@ -2474,7 +2506,7 @@ const TableEditor = () => {
const format = detectCellFormat( const format = detectCellFormat(
row[column.id], row[column.id],
); );
const cellValue = row[column.id] || ""; const cellValue = String(row[column.id] || "");
const isLongValue = cellValue.length > 50; const isLongValue = cellValue.length > 50;
if (format) { if (format) {
@@ -2540,20 +2572,25 @@ const TableEditor = () => {
))} ))}
{/* System Row - Add Row */} {/* System Row - Add Row */}
<tr className="border-t-2 border-dashed border-gray-300 dark:border-gray-600 hover:bg-blue-50 dark:hover:bg-blue-900/20"> <tr className="border-t-2 border-dashed border-gray-300 dark:border-gray-600">
<td {/* Sticky Add Row button on the left */}
colSpan={columns.length + 2} <td className="sticky left-0 bg-white dark:bg-gray-800 z-20 py-4 px-4">
className="text-center py-4"
>
<button <button
onClick={addRow} onClick={addRow}
className="flex items-center justify-center gap-2 text-gray-500 hover:text-blue-600 px-4 py-2 rounded-lg transition-colors group" 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"
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>

170
src/pages/TermsOfService.js Normal file
View File

@@ -0,0 +1,170 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, Shield, Code, Globe } from 'lucide-react';
import { SITE_CONFIG } from '../config/tools';
const TermsOfService = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft className="h-4 w-4" />
Back to Home
</Link>
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-gradient-to-br from-blue-500 to-purple-500 rounded-xl shadow-lg">
<Shield className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-800 dark:text-white">
Terms of Service
</h1>
<p className="text-slate-600 dark:text-slate-300">
Last updated: {new Date().toLocaleDateString()}
</p>
</div>
</div>
</div>
{/* Content */}
<div className="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700 p-8 shadow-xl">
<div className="prose prose-slate dark:prose-invert max-w-none">
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Code className="h-5 w-5 text-blue-600" />
1. Acceptance of Terms
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
By accessing and using {SITE_CONFIG.title} ("{SITE_CONFIG.domain}"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by the above, please do not use this service.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Globe className="h-5 w-5 text-green-600" />
2. Service Description
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mb-4">
{SITE_CONFIG.title} provides a collection of developer tools including but not limited to:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4">
<li>Object and Table Editors for JSON, CSV, and other data formats</li>
<li>URL and Base64 Encoders/Decoders</li>
<li>Code Beautifiers and Minifiers</li>
<li>Text Analysis and Comparison Tools</li>
<li>Other web-based developer utilities</li>
</ul>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mt-4">
All tools run entirely in your browser - no data is sent to our servers for processing.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
3. Privacy-First Approach
</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-4">
<p className="text-blue-800 dark:text-blue-200 font-medium mb-2">
🔒 What "Privacy-First" means at {SITE_CONFIG.title}:
</p>
<ul className="list-disc list-inside text-blue-700 dark:text-blue-300 space-y-1 text-sm">
<li><strong>Client-Side Processing:</strong> All tools process your data locally in your browser</li>
<li><strong>No Data Upload:</strong> Your sensitive data never leaves your device</li>
<li><strong>No Storage:</strong> We don't store, cache, or log your input data</li>
<li><strong>Minimal Analytics:</strong> We only collect anonymous usage statistics via Google Analytics</li>
<li><strong>No Tracking:</strong> No user accounts, no personal data collection</li>
</ul>
</div>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
We use Google Analytics to understand how our tools are used (page views, popular tools, etc.) but we never track or store the actual data you process with our tools.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
4. Use License
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Permission is granted to temporarily use {SITE_CONFIG.title} for personal and commercial purposes. This is the grant of a license, not a transfer of title, and under this license you may not:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4 mt-4">
<li>Use the service for any illegal or unauthorized purpose</li>
<li>Attempt to reverse engineer or extract source code</li>
<li>Use automated tools to overload our servers</li>
<li>Redistribute or resell access to the service</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
5. Disclaimer
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
The materials on {SITE_CONFIG.title} are provided on an 'as is' basis. {SITE_CONFIG.title} makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
6. Limitations
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
In no event shall {SITE_CONFIG.title} or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on {SITE_CONFIG.title}, even if {SITE_CONFIG.title} or an authorized representative has been notified orally or in writing of the possibility of such damage.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
7. Future Monetization
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4">
<p className="text-amber-800 dark:text-amber-200 leading-relaxed">
<strong>Transparency Notice:</strong> We plan to implement Google AdSense advertisements in the future to support the free operation of this service. When implemented, ads will be clearly marked and will not interfere with tool functionality. Our privacy-first approach will remain unchanged - we will never sell or share your usage data with advertisers.
</p>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
8. Revisions
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
{SITE_CONFIG.title} may revise these terms of service at any time without notice. By using this service, you are agreeing to be bound by the then current version of these terms of service.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
9. Contact Information
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
If you have any questions about these Terms of Service, please contact us at{' '}
<a href="mailto:dewe.developer@gmail.com" className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
dewe.developer@gmail.com
</a>
{' '}or through our website at {SITE_CONFIG.domain}.
</p>
</section>
</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>
);
};
export default TermsOfService;

143
src/utils/analytics.js Normal file
View File

@@ -0,0 +1,143 @@
// Google Analytics utility for React SPA
// Implements best practices for Single Page Applications
// Google Analytics configuration
const GA_MEASUREMENT_ID = 'G-S3K5P2PWV6';
// Initialize Google Analytics with Consent Mode v2
export const initGA = () => {
// Only initialize in production and if not already loaded
if (process.env.NODE_ENV !== 'production' || window.gtag) {
return;
}
// Initialize gtag function first (required for Consent Mode)
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
window.gtag = gtag;
// Initialize Consent Mode v2 BEFORE loading GA script
const { initConsentMode, applyStoredConsent } = require('./consentManager');
initConsentMode();
// Create script elements
const gtagScript = document.createElement('script');
gtagScript.async = true;
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
document.head.appendChild(gtagScript);
// Configure Google Analytics after script loads
gtagScript.onload = () => {
gtag('js', new Date());
gtag('config', GA_MEASUREMENT_ID, {
// SPA-specific configurations
send_page_view: false, // We'll manually send page views
anonymize_ip: true, // Privacy-first approach
allow_google_signals: false, // Disable advertising features for privacy
allow_ad_personalization_signals: false, // Disable ad personalization
});
// Apply any stored consent preferences
applyStoredConsent();
console.log('🔍 Google Analytics initialized with Consent Mode v2');
};
};
// Track page views for SPA navigation
export const trackPageView = (path, title) => {
if (process.env.NODE_ENV !== 'production' || !window.gtag) {
console.log(`📊 [DEV] Page view: ${path} - ${title}`);
return;
}
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: path,
page_title: title,
});
console.log(`📊 Page view tracked: ${path}`);
};
// Track custom events
export const trackEvent = (eventName, parameters = {}) => {
if (process.env.NODE_ENV !== 'production' || !window.gtag) {
console.log(`📊 [DEV] Event: ${eventName}`, parameters);
return;
}
window.gtag('event', eventName, {
...parameters,
// Add privacy-friendly defaults
anonymize_ip: true,
});
console.log(`📊 Event tracked: ${eventName}`);
};
// Predefined events for common actions
export const trackToolUsage = (toolName, action = 'use') => {
trackEvent('tool_interaction', {
tool_name: toolName,
action: action,
event_category: 'tools',
});
};
export const trackSearch = (searchTerm) => {
// Only track that a search happened, not the actual term for privacy
trackEvent('search', {
event_category: 'engagement',
// Don't send the actual search term for privacy
has_results: searchTerm.length > 0,
});
};
export const trackThemeChange = (theme) => {
trackEvent('theme_change', {
theme: theme,
event_category: 'preferences',
});
};
export const trackError = (errorType, errorMessage) => {
trackEvent('exception', {
description: `${errorType}: ${errorMessage}`,
fatal: false,
event_category: 'errors',
});
};
// Check if user has opted out of analytics
export const isAnalyticsEnabled = () => {
// Check for common opt-out methods
if (navigator.doNotTrack === '1' ||
window.doNotTrack === '1' ||
navigator.msDoNotTrack === '1') {
return false;
}
// Check for ad blockers or analytics blockers
if (!window.gtag && process.env.NODE_ENV === 'production') {
return false;
}
return true;
};
// Privacy-friendly analytics info
export const getAnalyticsInfo = () => {
return {
enabled: isAnalyticsEnabled(),
measurementId: GA_MEASUREMENT_ID,
environment: process.env.NODE_ENV,
privacyFeatures: {
anonymizeIp: true,
disableAdvertising: true,
disablePersonalization: true,
clientSideOnly: true,
}
};
};

163
src/utils/consentManager.js Normal file
View File

@@ -0,0 +1,163 @@
// GDPR Consent Management with Google Consent Mode v2
// Implements TCF 2.2 compatible consent management
// Consent categories
export const CONSENT_CATEGORIES = {
NECESSARY: 'necessary',
ANALYTICS: 'analytics_storage',
ADVERTISING: 'ad_storage',
PERSONALIZATION: 'ad_personalization',
USER_DATA: 'ad_user_data'
};
// Default consent state (denied until user consents)
const DEFAULT_CONSENT = {
[CONSENT_CATEGORIES.NECESSARY]: 'granted', // Always granted for essential functionality
[CONSENT_CATEGORIES.ANALYTICS]: 'denied',
[CONSENT_CATEGORIES.ADVERTISING]: 'denied',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'denied',
[CONSENT_CATEGORIES.USER_DATA]: 'denied'
};
// Check if user is in EEA (European Economic Area)
export const isEEAUser = () => {
// Simple timezone-based detection (not 100% accurate but good enough)
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const eeaTimezones = [
'Europe/', 'Atlantic/Reykjavik', 'Atlantic/Faroe', 'Atlantic/Canary',
'Africa/Ceuta', 'Arctic/Longyearbyen'
];
return eeaTimezones.some(tz => timezone.startsWith(tz));
};
// Initialize Google Consent Mode
export const initConsentMode = () => {
if (typeof window === 'undefined' || !window.gtag) return;
// Set default consent state
window.gtag('consent', 'default', {
...DEFAULT_CONSENT,
region: isEEAUser() ? ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', 'LI', 'NO'] : ['US'],
wait_for_update: 500 // Wait 500ms for consent update
});
console.log('🍪 Consent Mode v2 initialized');
};
// Update consent based on user choice
export const updateConsent = (consentChoices) => {
if (typeof window === 'undefined' || !window.gtag) return;
window.gtag('consent', 'update', consentChoices);
// Store consent in localStorage
localStorage.setItem('consent_preferences', JSON.stringify({
...consentChoices,
timestamp: Date.now(),
version: '2.0'
}));
console.log('🍪 Consent updated:', consentChoices);
};
// Get stored consent preferences
export const getStoredConsent = () => {
try {
const stored = localStorage.getItem('consent_preferences');
if (stored) {
const parsed = JSON.parse(stored);
// Check if consent is less than 1 year old
if (Date.now() - parsed.timestamp < 365 * 24 * 60 * 60 * 1000) {
return parsed;
}
}
} catch (error) {
console.error('Error reading stored consent:', error);
}
return null;
};
// Check if consent banner should be shown
export const shouldShowConsentBanner = () => {
// Only show for EEA users who haven't consented
return isEEAUser() && !getStoredConsent();
};
// Predefined consent configurations
export const CONSENT_CONFIGS = {
// Accept all (for users who want full functionality)
ACCEPT_ALL: {
[CONSENT_CATEGORIES.NECESSARY]: 'granted',
[CONSENT_CATEGORIES.ANALYTICS]: 'granted',
[CONSENT_CATEGORIES.ADVERTISING]: 'granted',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'granted',
[CONSENT_CATEGORIES.USER_DATA]: 'granted'
},
// Essential only (minimal consent)
ESSENTIAL_ONLY: {
[CONSENT_CATEGORIES.NECESSARY]: 'granted',
[CONSENT_CATEGORIES.ANALYTICS]: 'denied',
[CONSENT_CATEGORIES.ADVERTISING]: 'denied',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'denied',
[CONSENT_CATEGORIES.USER_DATA]: 'denied'
},
// Analytics only (for users who want to help improve the service)
ANALYTICS_ONLY: {
[CONSENT_CATEGORIES.NECESSARY]: 'granted',
[CONSENT_CATEGORIES.ANALYTICS]: 'granted',
[CONSENT_CATEGORIES.ADVERTISING]: 'denied',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'denied',
[CONSENT_CATEGORIES.USER_DATA]: 'denied'
}
};
// Apply stored consent on page load
export const applyStoredConsent = () => {
const stored = getStoredConsent();
if (stored && window.gtag) {
const { timestamp, version, ...consentChoices } = stored;
window.gtag('consent', 'update', consentChoices);
console.log('🍪 Applied stored consent:', consentChoices);
}
};
// Consent banner component data
export const getConsentBannerData = () => {
return {
title: 'We respect your privacy',
description: 'We use cookies and similar technologies to improve your experience, analyze site usage, and assist in our marketing efforts. Your data stays private with our client-side tools.',
purposes: [
{
id: CONSENT_CATEGORIES.NECESSARY,
name: 'Essential',
description: 'Required for basic site functionality',
required: true
},
{
id: CONSENT_CATEGORIES.ANALYTICS,
name: 'Analytics',
description: 'Help us understand how you use our tools (Google Analytics)',
required: false
},
{
id: CONSENT_CATEGORIES.ADVERTISING,
name: 'Advertising',
description: 'Future ad personalization (not yet implemented)',
required: false
}
],
buttons: {
acceptAll: 'Accept All',
essentialOnly: 'Essential Only',
customize: 'Customize',
save: 'Save Preferences'
},
links: {
privacy: '/privacy',
terms: '/terms'
}
};
};

212
src/utils/seo.js Normal file
View File

@@ -0,0 +1,212 @@
import { TOOLS, SITE_CONFIG } from '../config/tools';
// SEO metadata generator
export const generateSEOData = (path) => {
const baseUrl = SITE_CONFIG.domain;
const defaultTitle = `${SITE_CONFIG.title} - ${SITE_CONFIG.subtitle}`;
const defaultDescription = SITE_CONFIG.description;
// Find tool by path
const tool = TOOLS.find(t => t.path === path);
// Generate SEO data based on route
switch (path) {
case '/':
return {
title: defaultTitle,
description: `${SITE_CONFIG.totalTools} professional developer utilities. ${defaultDescription}. JSON editor, URL encoder, Base64 converter, code beautifier, and more.`,
keywords: 'developer tools, JSON editor, URL encoder, Base64 converter, code beautifier, text diff, web utilities, programming tools',
canonical: baseUrl,
ogType: 'website',
ogImage: `${baseUrl}/og-image.png`,
twitterCard: 'summary_large_image',
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_CONFIG.title,
description: defaultDescription,
url: baseUrl,
potentialAction: {
'@type': 'SearchAction',
target: `${baseUrl}/?search={search_term_string}`,
'query-input': 'required name=search_term_string'
},
publisher: {
'@type': 'Organization',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/privacy':
return {
title: `Privacy Policy - ${SITE_CONFIG.title}`,
description: 'Our privacy-first approach to developer tools. Learn how we protect your data with 100% client-side processing and minimal analytics.',
keywords: 'privacy policy, data protection, client-side processing, developer tools privacy',
canonical: `${baseUrl}/privacy`,
ogType: 'article',
noindex: false,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Privacy Policy',
description: 'Privacy policy for Dewe.Dev developer tools',
url: `${baseUrl}/privacy`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/terms':
return {
title: `Terms of Service - ${SITE_CONFIG.title}`,
description: 'Terms of service for using our developer tools. Professional-grade utilities with transparent policies.',
keywords: 'terms of service, developer tools terms, usage policy',
canonical: `${baseUrl}/terms`,
ogType: 'article',
noindex: false,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Terms of Service',
description: 'Terms of service for Dewe.Dev developer tools',
url: `${baseUrl}/terms`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
default:
if (tool) {
const toolKeywords = tool.tags.join(', ').toLowerCase();
return {
title: `${tool.name} - ${SITE_CONFIG.title}`,
description: `${tool.description}. Free online ${tool.name.toLowerCase()} tool. ${defaultDescription}.`,
keywords: `${toolKeywords}, ${tool.name.toLowerCase()}, developer tools, online tools, web utilities`,
canonical: `${baseUrl}${tool.path}`,
ogType: 'website',
ogImage: `${baseUrl}/og-tools.png`,
twitterCard: 'summary',
structuredData: {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: tool.name,
description: tool.description,
url: `${baseUrl}${tool.path}`,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD'
},
publisher: {
'@type': 'Organization',
name: SITE_CONFIG.title,
url: baseUrl
},
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
},
keywords: toolKeywords,
featureList: tool.tags
}
};
}
// Fallback for unknown routes
return {
title: `Page Not Found - ${SITE_CONFIG.title}`,
description: defaultDescription,
keywords: 'developer tools, web utilities',
canonical: `${baseUrl}${path}`,
ogType: 'website',
noindex: true
};
}
};
// Generate Open Graph meta tags
export const generateOGTags = (seoData) => {
return [
{ property: 'og:type', content: seoData.ogType || 'website' },
{ property: 'og:title', content: seoData.title },
{ property: 'og:description', content: seoData.description },
{ property: 'og:url', content: seoData.canonical },
{ property: 'og:site_name', content: SITE_CONFIG.title },
...(seoData.ogImage ? [{ property: 'og:image', content: seoData.ogImage }] : []),
{ property: 'og:locale', content: 'en_US' }
];
};
// Generate Twitter Card meta tags
export const generateTwitterTags = (seoData) => {
return [
{ name: 'twitter:card', content: seoData.twitterCard || 'summary' },
{ name: 'twitter:title', content: seoData.title },
{ name: 'twitter:description', content: seoData.description },
...(seoData.ogImage ? [{ name: 'twitter:image', content: seoData.ogImage }] : [])
];
};
// Generate all meta tags for a route
export const generateMetaTags = (path) => {
const seoData = generateSEOData(path);
const basicMeta = [
{ name: 'description', content: seoData.description },
{ name: 'keywords', content: seoData.keywords },
{ name: 'author', content: SITE_CONFIG.title },
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
{ name: 'robots', content: seoData.noindex ? 'noindex,nofollow' : 'index,follow' },
{ name: 'googlebot', content: seoData.noindex ? 'noindex,nofollow' : 'index,follow' }
];
const ogTags = generateOGTags(seoData);
const twitterTags = generateTwitterTags(seoData);
return {
title: seoData.title,
meta: [...basicMeta, ...ogTags, ...twitterTags],
link: [
{ rel: 'canonical', href: seoData.canonical }
],
structuredData: seoData.structuredData
};
};
// Core Web Vitals optimization hints
export const getCoreWebVitalsOptimizations = () => {
return {
// Largest Contentful Paint (LCP)
lcp: {
preloadCriticalResources: true,
optimizeImages: true,
removeRenderBlockingResources: true
},
// First Input Delay (FID)
fid: {
minimizeJavaScript: true,
useWebWorkers: false, // Not needed for our tools
optimizeEventHandlers: true
},
// Cumulative Layout Shift (CLS)
cls: {
setImageDimensions: true,
reserveSpaceForAds: true, // Important for future AdSense
avoidDynamicContent: true,
useTransforms: true
}
};
};

View File

@@ -0,0 +1,126 @@
import { TOOLS, SITE_CONFIG } from '../config/tools';
// Generate sitemap.xml content
export const generateSitemap = () => {
const baseUrl = SITE_CONFIG.domain;
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
// Define all routes with their priorities and change frequencies
const routes = [
{
url: '/',
priority: '1.0',
changefreq: 'weekly',
lastmod: currentDate
},
// Tool pages
...TOOLS.map(tool => ({
url: tool.path,
priority: '0.8',
changefreq: 'monthly',
lastmod: currentDate
})),
// Legal pages
{
url: '/privacy',
priority: '0.3',
changefreq: 'yearly',
lastmod: currentDate
},
{
url: '/terms',
priority: '0.3',
changefreq: 'yearly',
lastmod: currentDate
}
];
// Generate XML sitemap
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${routes.map(route => ` <url>
<loc>${baseUrl}${route.url}</loc>
<lastmod>${route.lastmod}</lastmod>
<changefreq>${route.changefreq}</changefreq>
<priority>${route.priority}</priority>
</url>`).join('\n')}
</urlset>`;
return sitemap;
};
// Generate robots.txt content
export const generateRobotsTxt = () => {
const baseUrl = SITE_CONFIG.domain;
return `# Robots.txt for ${baseUrl}
# Generated automatically
User-agent: *
Allow: /
# Sitemap location
Sitemap: ${baseUrl}/sitemap.xml
# Block any future admin or private routes
Disallow: /admin/
Disallow: /api/
Disallow: /.well-known/
# Allow all major search engines
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Slurp
Allow: /
User-agent: DuckDuckBot
Allow: /
# Crawl delay for politeness
Crawl-delay: 1`;
};
// Build-time sitemap generation script
export const buildSitemap = () => {
const fs = require('fs');
const path = require('path');
const publicDir = path.join(process.cwd(), 'public');
// Generate and write sitemap.xml
const sitemapContent = generateSitemap();
fs.writeFileSync(path.join(publicDir, 'sitemap.xml'), sitemapContent, 'utf8');
// Generate and write robots.txt
const robotsContent = generateRobotsTxt();
fs.writeFileSync(path.join(publicDir, 'robots.txt'), robotsContent, 'utf8');
console.log('✅ Sitemap and robots.txt generated successfully!');
console.log(`📍 Sitemap: ${SITE_CONFIG.domain}/sitemap.xml`);
console.log(`🤖 Robots: ${SITE_CONFIG.domain}/robots.txt`);
};
// Runtime sitemap data for dynamic generation
export const getSitemapData = () => {
return {
routes: [
{ path: '/', priority: 1.0, changefreq: 'weekly' },
...TOOLS.map(tool => ({
path: tool.path,
priority: 0.8,
changefreq: 'monthly'
})),
{ path: '/privacy', priority: 0.3, changefreq: 'yearly' },
{ path: '/terms', priority: 0.3, changefreq: 'yearly' }
],
baseUrl: SITE_CONFIG.domain,
totalUrls: TOOLS.length + 3 // tools + home + privacy + terms
};
};