Initial commit: Developer Tools MVP with visual editor

- Complete React app with 7 developer tools
- JSON Tool with visual structured editor
- Serialize Tool with visual structured editor
- URL, Base64, CSV/JSON, Beautifier, Diff tools
- Responsive navigation with dropdown menu
- Dark/light mode toggle
- Mobile-responsive design with sticky header
- All tools working with copy/paste functionality
This commit is contained in:
dwindown
2025-08-02 09:31:26 +07:00
commit 7f2dd5260f
45657 changed files with 4730486 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import React, { useState } from 'react';
import { Copy, Check } from 'lucide-react';
const CopyButton = ({ text, className = '' }) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
}
};
return (
<button
onClick={handleCopy}
className={`copy-button ${className}`}
title={copied ? 'Copied!' : 'Copy to clipboard'}
>
{copied ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4 text-gray-600 dark:text-gray-400" />
)}
</button>
);
};
export default CopyButton;

195
src/components/Layout.js Normal file
View File

@@ -0,0 +1,195 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Code2, Home, ChevronDown, Menu, X, Database, FileText, Link as LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
const Layout = ({ children }) => {
const location = useLocation();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null);
const isActive = (path) => {
return location.pathname === path;
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Close mobile menu when route changes
useEffect(() => {
setIsMobileMenuOpen(false);
setIsDropdownOpen(false);
}, [location.pathname]);
const tools = [
{ path: '/json', name: 'JSON Tool', icon: FileText, description: 'Format & validate JSON' },
{ path: '/serialize', name: 'Serialize Tool', icon: Database, description: 'PHP serialize/unserialize' },
{ 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' }
];
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* 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">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<Link to="/" className="flex items-center space-x-2">
<Code2 className="h-8 w-8 text-primary-600" />
<span className="text-xl font-bold text-gray-900 dark:text-white">
DevTools
</span>
</Link>
<div className="flex items-center space-x-4">
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6">
<Link
to="/"
className={`flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive('/')
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
}`}
>
<Home className="h-4 w-4" />
<span>Home</span>
</Link>
{/* Tools Dropdown */}
<div className="relative" ref={dropdownRef}>
<button
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"
>
<span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform ${
isDropdownOpen ? 'rotate-180' : ''
}`} />
</button>
{/* Dropdown Menu */}
{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">
{tools.map((tool) => {
const IconComponent = tool.icon;
return (
<Link
key={tool.path}
to={tool.path}
onClick={() => setIsDropdownOpen(false)}
className={`flex items-center space-x-3 px-4 py-3 text-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${
isActive(tool.path)
? 'bg-primary-50 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-700 dark:text-gray-300'
}`}
>
<IconComponent className="h-4 w-4" />
<div>
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div>
</div>
</Link>
);
})}
</div>
)}
</div>
</nav>
<ThemeToggle />
{/* Mobile Menu Button */}
<button
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"
>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
</div>
</div>
</div>
</header>
{/* Mobile Navigation Menu */}
{isMobileMenuOpen && (
<div className="md:hidden bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
<Link
to="/"
onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive('/')
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
}`}
>
<Home className="h-5 w-5" />
<span>Home</span>
</Link>
<div className="border-t border-gray-200 dark:border-gray-700 pt-2 mt-2">
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-3 py-1">
Tools
</div>
{tools.map((tool) => {
const IconComponent = tool.icon;
return (
<Link
key={tool.path}
to={tool.path}
onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive(tool.path)
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
}`}
>
<IconComponent className="h-5 w-5" />
<div>
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div>
</div>
</Link>
);
})}
</div>
</div>
</div>
</div>
)}
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{children}
</main>
{/* Footer */}
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
<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">
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p>
</div>
</div>
</footer>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,332 @@
import React, { useState } from 'react';
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react';
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const [data, setData] = useState(initialData);
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
const updateData = (newData) => {
setData(newData);
onDataChange(newData);
};
const toggleNode = (path) => {
const newExpanded = new Set(expandedNodes);
if (newExpanded.has(path)) {
newExpanded.delete(path);
} else {
newExpanded.add(path);
}
setExpandedNodes(newExpanded);
};
const addProperty = (obj, path) => {
const newObj = { ...obj };
const keys = Object.keys(newObj);
const newKey = `property${keys.length + 1}`;
newObj[newKey] = '';
updateData(newObj);
setExpandedNodes(new Set([...expandedNodes, path]));
};
const addArrayItem = (arr, path) => {
const newArr = [...arr, ''];
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
for (let i = 1; i < pathParts.length - 1; i++) {
current = current[pathParts[i]];
}
if (pathParts.length === 2) {
newData[pathParts[1]] = newArr;
} else {
current[pathParts[pathParts.length - 1]] = newArr;
}
updateData(newData);
};
const removeProperty = (key, parentPath) => {
const pathParts = parentPath.split('.');
const newData = { ...data };
let current = newData;
for (let i = 1; i < pathParts.length; i++) {
if (i === pathParts.length - 1) {
delete current[key];
} else {
current = current[pathParts[i]];
}
}
if (pathParts.length === 1) {
delete newData[key];
}
updateData(newData);
};
const updateValue = (value, path) => {
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
for (let i = 1; i < pathParts.length - 1; i++) {
current = current[pathParts[i]];
}
const key = pathParts[pathParts.length - 1];
// Auto-detect type
if (value === 'true' || value === 'false') {
current[key] = value === 'true';
} else if (value === 'null') {
current[key] = null;
} else if (!isNaN(value) && value !== '') {
current[key] = Number(value);
} else {
current[key] = value;
}
updateData(newData);
};
const changeType = (newType, path) => {
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
for (let i = 1; i < pathParts.length - 1; i++) {
current = current[pathParts[i]];
}
const key = pathParts[pathParts.length - 1];
switch (newType) {
case 'string':
current[key] = '';
break;
case 'number':
current[key] = 0;
break;
case 'boolean':
current[key] = false;
break;
case 'array':
current[key] = [];
break;
case 'object':
current[key] = {};
break;
case 'null':
current[key] = null;
break;
default:
current[key] = '';
}
updateData(newData);
setExpandedNodes(new Set([...expandedNodes, path]));
};
const renameKey = (oldKey, newKey, path) => {
if (oldKey === newKey || !newKey.trim()) return;
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
// Navigate to parent object
for (let i = 1; i < pathParts.length - 1; i++) {
current = current[pathParts[i]];
}
// Check if new key already exists
if (current.hasOwnProperty(newKey)) {
return; // Don't rename if key already exists
}
// Rename the key
const value = current[oldKey];
delete current[oldKey];
current[newKey] = value;
updateData(newData);
};
const getTypeIcon = (value) => {
if (value === null) return <span className="text-gray-500"></span>;
if (value === undefined) return <span className="text-gray-400">?</span>;
if (typeof value === 'string') return <Type className="h-4 w-4 text-green-600" />;
if (typeof value === 'number') return <Hash className="h-4 w-4 text-blue-600" />;
if (typeof value === 'boolean') return <ToggleLeft className="h-4 w-4 text-purple-600" />;
if (Array.isArray(value)) return <List className="h-4 w-4 text-orange-600" />;
if (typeof value === 'object') return <Braces className="h-4 w-4 text-red-600" />;
return <Type className="h-4 w-4 text-gray-600" />;
};
const renderValue = (value, key, path, parentPath) => {
const isExpanded = expandedNodes.has(path);
const canExpand = typeof value === 'object' && value !== null;
return (
<div key={path} className="ml-4 border-l border-gray-200 dark:border-gray-700 pl-4 mb-2 overflow-hidden">
<div className="flex items-center space-x-2 mb-2">
{canExpand && (
<button
onClick={() => toggleNode(path)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
)}
{!canExpand && <div className="w-6" />}
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-2 flex-1">
<div className="flex items-center space-x-2 flex-1">
{getTypeIcon(value)}
<input
type="text"
defaultValue={key}
onBlur={(e) => {
const newKey = e.target.value.trim();
if (newKey && newKey !== key) {
renameKey(key, newKey, path);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.target.blur(); // Trigger blur to save changes
}
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0 flex-1"
placeholder="Property name"
/>
<span className="text-gray-500 hidden sm:inline">:</span>
</div>
{!canExpand ? (
<input
type="text"
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Value"
/>
) : (
<span className="text-sm text-gray-600 dark:text-gray-400">
{Array.isArray(value) ? `Array (${value.length} items)` : `Object (${Object.keys(value).length} properties)`}
</span>
)}
<div className="flex items-center space-x-2 sm:space-x-2">
<select
value={
value === null ? 'null' :
value === undefined ? 'string' :
typeof value === 'string' ? 'string' :
typeof value === 'number' ? 'number' :
typeof value === 'boolean' ? 'boolean' :
Array.isArray(value) ? 'array' : 'object'
}
onChange={(e) => changeType(e.target.value, path)}
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="array">Array</option>
<option value="object">Object</option>
<option value="null">Null</option>
</select>
<button
onClick={() => removeProperty(key, parentPath)}
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
title="Remove property"
>
<Minus className="h-4 w-4" />
</button>
</div>
</div>
</div>
{canExpand && isExpanded && (
<div className="ml-6">
{Array.isArray(value) ? (
<>
{value.map((item, index) =>
renderValue(item, index.toString(), `${path}.${index}`, path)
)}
<button
onClick={() => addArrayItem(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Item</span>
</button>
</>
) : (
<>
{Object.entries(value).map(([k, v]) =>
renderValue(v, k, `${path}.${k}`, path)
)}
<button
onClick={() => addProperty(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
</>
)}
</div>
)}
</div>
);
};
return (
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-gray-800 min-h-96 w-full">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex-shrink-0"
>
<Plus className="h-4 w-4" />
<span className="hidden sm:inline">Add Property</span>
<span className="sm:hidden">Add</span>
</button>
</div>
<div className="overflow-x-hidden">
{Object.keys(data).length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
</div>
) : (
Object.entries(data).map(([key, value]) =>
renderValue(value, key, `root.${key}`, 'root')
)
)}
</div>
</div>
);
};
export default StructuredEditor;

View File

@@ -0,0 +1,60 @@
import React, { useState, useEffect } from 'react';
import { Sun, Moon } from 'lucide-react';
const ThemeToggle = () => {
const [isDark, setIsDark] = useState(() => {
// Check if there's a saved preference, otherwise use system preference
const saved = localStorage.getItem('theme');
if (saved) {
return saved === 'dark';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
// Apply theme to document and save preference
if (isDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [isDark]);
useEffect(() => {
// Listen for system theme changes only if no manual preference is set
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
// Only update if user hasn't manually set a preference
const saved = localStorage.getItem('theme');
if (!saved) {
setIsDark(e.matches);
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
const toggleTheme = () => {
setIsDark(!isDark);
};
return (
<button
onClick={toggleTheme}
className="p-2 rounded-md text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</button>
);
};
export default ThemeToggle;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
const ToolCard = ({ icon: Icon, title, description, path, tags }) => {
return (
<Link to={path} className="block">
<div className="tool-card group cursor-pointer">
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<Icon className="h-8 w-8 text-primary-600" />
</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">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mt-1">
{description}
</p>
{tags && (
<div className="flex flex-wrap gap-2 mt-3">
{tags.map((tag, index) => (
<span
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"
>
{tag}
</span>
))}
</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>
</Link>
);
};
export default ToolCard;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { ArrowLeft } from 'lucide-react';
import { Link } from 'react-router-dom';
const ToolLayout = ({ title, description, children, icon: Icon }) => {
return (
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center text-primary-600 hover:text-primary-700 mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Tools
</Link>
<div className="flex items-center space-x-3 mb-2">
{Icon && <Icon className="h-8 w-8 text-primary-600" />}
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
{title}
</h1>
</div>
{description && (
<p className="text-gray-600 dark:text-gray-300 text-lg">
{description}
</p>
)}
</div>
{/* Tool Content */}
<div className="space-y-6">
{children}
</div>
</div>
);
};
export default ToolLayout;