From ec1ffb758237183e7df15219a15096d63952c9e2 Mon Sep 17 00:00:00 2001 From: Tanmay Patil Date: Tue, 17 Jun 2025 12:00:47 +0530 Subject: [PATCH 1/3] feat: add Encode/Decode tools with multiple encoding formats and toast notifications --- package-lock.json | 58 +++ package.json | 1 + src/App.tsx | 50 ++- src/components/EncodeDecodeTools.tsx | 525 +++++++++++++++++++++++++++ src/components/Sidebar.tsx | 7 +- src/components/ThemeToggle.tsx | 162 +++++++-- src/components/ui/TextArea.tsx | 25 ++ src/components/ui/Toast.tsx | 57 +++ src/hooks/useToast.tsx | 31 ++ src/index.css | 26 +- src/types/index.ts | 32 +- 11 files changed, 921 insertions(+), 53 deletions(-) create mode 100644 src/components/EncodeDecodeTools.tsx create mode 100644 src/components/ui/TextArea.tsx create mode 100644 src/components/ui/Toast.tsx create mode 100644 src/hooks/useToast.tsx diff --git a/package-lock.json b/package-lock.json index fe38277..91d3fb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.2.14", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "html2canvas": "^1.4.1", @@ -2104,6 +2105,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2225,6 +2260,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", diff --git a/package.json b/package.json index 5169a31..90a39a5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.2.14", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "html2canvas": "^1.4.1", diff --git a/src/App.tsx b/src/App.tsx index a98875a..54a5d0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,12 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import Sidebar from './components/Sidebar'; import JsonVisualizer from './components/JsonVisualizer'; import DiffChecker from './components/DiffChecker'; import MermaidVisualizer from './components/MermaidVisualizer'; +import EncodeDecodeTools from './components/EncodeDecodeTools'; import { ThemeProvider } from './hooks/useTheme'; import { UtilityTab } from './types'; +import { ToastProvider } from './components/ui/Toast'; const utilities: UtilityTab[] = [ { @@ -25,6 +27,12 @@ const utilities: UtilityTab[] = [ icon: 'workflow', component: MermaidVisualizer, }, + { + id: 'encode-decode', + name: 'Encode / Decode', + icon: 'code', + component: EncodeDecodeTools, + }, ]; function App() { @@ -36,27 +44,29 @@ function App() { return ( -
- {/* Background Effects */} -
-
- - {/* Sidebar */} - setSidebarCollapsed(!sidebarCollapsed)} - /> - - {/* Main Content */} -
-
- + +
+ {/* Background Effects */} +
+
+ + {/* Sidebar */} + setSidebarCollapsed(!sidebarCollapsed)} + /> + + {/* Main Content */} +
+
+ +
-
+ ); } diff --git a/src/components/EncodeDecodeTools.tsx b/src/components/EncodeDecodeTools.tsx new file mode 100644 index 0000000..4eb4bfd --- /dev/null +++ b/src/components/EncodeDecodeTools.tsx @@ -0,0 +1,525 @@ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { Copy, RefreshCw, ArrowRight, ArrowLeft, Info, Shuffle, Sparkles, Code, Lock, Globe, Hash, Binary, Shield, FileText } from 'lucide-react'; +import TextArea from './ui/TextArea'; +import { useToast } from '@/hooks/useToast'; +import { ToastComponent } from './ui/Toast'; + +const EncodeDecodeTools = () => { + const { toasts, addToast, closeToast, removeToast } = useToast(); + const [input, setInput] = useState(''); + const [output, setOutput] = useState(''); + const [activeTab, setActiveTab] = useState('base64'); + const [toastMessage, setToastMessage] = useState(''); + const [toastType, setToastType] = useState<'success' | 'error' | 'warning' | 'info'>('success'); + const [showToast, setShowToast] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + + + // Main encoding/decoding functions + const convertFunctions = useMemo(() => ({ + base64: { + encode: (str: string) => btoa(unescape(encodeURIComponent(str))), + decode: (str: string) => decodeURIComponent(escape(atob(str.replace(/\s/g, '')))), + description: 'Base64 encoding is commonly used for encoding binary data in text format', + isValidEncoded: (str: string) => /^[A-Za-z0-9+/]*={0,2}$/.test(str.replace(/\s/g, '')), + example: { input: 'Hello World!', output: 'SGVsbG8gV29ybGQh' }, + icon: Code + }, + url: { + encode: (str: string) => encodeURIComponent(str), + decode: (str: string) => decodeURIComponent(str), + description: 'URL encoding replaces unsafe ASCII characters with % followed by hex digits', + isValidEncoded: (str: string) => /%[0-9A-Fa-f]{2}/.test(str), + example: { input: 'Hello World!', output: 'Hello%20World%21' }, + icon: Globe + }, + html: { + encode: (str: string) => { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + }, + decode: (str: string) => { + const div = document.createElement('div'); + div.innerHTML = str; + return div.textContent || ''; + }, + description: 'HTML encoding converts special characters to HTML entities', + isValidEncoded: (str: string) => /&[a-zA-Z]+;|&#\d+;|&#x[0-9A-Fa-f]+;/.test(str), + example: { input: '', output: '' }, + icon: Code + }, + hex: { + encode: (str: string) => { + return str.split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(''); + }, + decode: (str: string) => { + const cleaned = str.replace(/[\s\n]/g, ''); + if (cleaned.length % 2 !== 0) throw new Error('Invalid hex string length'); + return cleaned.match(/.{1,2}/g) + ?.map(byte => String.fromCharCode(parseInt(byte, 16))) + .join('') || ''; + }, + description: 'Hexadecimal encoding converts each character to its hex representation', + isValidEncoded: (str: string) => /^[0-9A-Fa-f\s]*$/.test(str) && str.replace(/\s/g, '').length % 2 === 0, + example: { input: 'Hello', output: '48656c6c6f' }, + icon: Hash + }, + binary: { + encode: (str: string) => { + return str.split('').map(char => char.charCodeAt(0).toString(2).padStart(8, '0')).join(' '); + }, + decode: (str: string) => { + const cleaned = str.replace(/[^01]/g, ''); + if (cleaned.length % 8 !== 0) throw new Error('Invalid binary string length'); + return cleaned.match(/.{1,8}/g) + ?.map(byte => String.fromCharCode(parseInt(byte, 2))) + .join('') || ''; + }, + description: 'Binary encoding converts each character to its 8-bit binary representation', + isValidEncoded: (str: string) => /^[01\s]*$/.test(str) && str.replace(/\s/g, '').length % 8 === 0, + example: { input: 'Hi', output: '01001000 01101001' }, + icon: Binary + }, + jwt: { + encode: (str: string) => { + throw new Error('JWT encoding not supported - paste a JWT token to decode it'); + }, + decode: (str: string) => { + try { + const parts = str.split('.'); + if (parts.length !== 3) throw new Error('Invalid JWT format - must have 3 parts separated by dots'); + + const [headerB64, payloadB64, signature] = parts; + + const header = JSON.parse(atob(headerB64.replace(/-/g, '+').replace(/_/g, '/'))); + const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/'))); + + return JSON.stringify({ + header, + payload, + signature: signature || 'No signature' + }, null, 2); + } catch (error) { + throw new Error('Invalid JWT token format or corrupted data'); + } + }, + description: 'JSON Web Token decoder - paste a JWT to see its header and payload', + isValidEncoded: (str: string) => { + const parts = str.split('.'); + return parts.length === 3 && parts.every(part => /^[A-Za-z0-9_-]+$/.test(part)); + }, + example: { input: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', output: 'Decoded JWT content' }, + icon: Shield + } + }), []); + + const performEncode = useCallback((text: string, tab: string) => { + try { + setIsProcessing(true); + const result = convertFunctions[tab as keyof typeof convertFunctions].encode(text); + setOutput(result); + addToast('Text encoded successfully', 'success'); + } catch (error) { + const message = error instanceof Error ? error.message : 'Encoding failed'; + addToast(`Error: ${message}`, 'error'); + console.error(error); + } finally { + setIsProcessing(false); + } + }, [convertFunctions]); + + const performDecode = useCallback((text: string, tab: string) => { + try { + setIsProcessing(true); + const { decode, isValidEncoded } = convertFunctions[tab as keyof typeof convertFunctions]; + + if (isValidEncoded && !isValidEncoded(text.trim())) { + throw new Error(`Input doesn't appear to be valid encoded text`); + } + + const result = decode(text); + setOutput(result); + addToast('Text decoded successfully', 'success'); + } catch (error) { + const message = error instanceof Error ? error.message : 'Decoding failed'; + addToast(`Error: ${message}`, 'error'); + console.error(error); + } finally { + setIsProcessing(false); + } + }, [convertFunctions]); + + const handleAutoProcess = useCallback(() => { + if (!input.trim()) return; + const { isValidEncoded } = convertFunctions[activeTab as keyof typeof convertFunctions]; + const shouldDecode = isValidEncoded ? isValidEncoded(input.trim()) : false; + + if (shouldDecode) { + performDecode(input, activeTab); + } else { + performEncode(input, activeTab); + } + }, [input, activeTab, performDecode, performEncode, convertFunctions]); + + useEffect(() => { + if (input.trim() && activeTab !== 'jwt') { + const timer = setTimeout(() => { + handleAutoProcess(); + }, 300); + return () => clearTimeout(timer); + } else if (!input.trim()) { + setOutput(''); + } + }, [input, activeTab, handleAutoProcess]); + + const handleClearInput = () => { + setInput(''); + setOutput(''); + }; + + const handleClearOutput = () => setOutput(''); + + const handleCopyInput = async () => { + try { + await navigator.clipboard.writeText(input); + addToast('Input copied to clipboard', 'success'); + } catch (err) { + addToast('Failed to copy to clipboard', 'error'); + } + }; + + const handleCopyOutput = async () => { + try { + await navigator.clipboard.writeText(output); + addToast('Output copied to clipboard', 'success'); + } catch (err) { + addToast('Failed to copy to clipboard', 'error'); + } + }; + + const handleEncode = () => { + if (!input.trim()) { + addToast('Please enter some text to encode', 'warning'); + return; + } + performEncode(input, activeTab); + }; + + const handleDecode = () => { + if (!input.trim()) { + addToast('Please enter some text to decode', 'warning'); + return; + } + performDecode(input, activeTab); + }; + + const handleSwap = () => { + const temp = input; + setInput(output); + setOutput(temp); + addToast('Input and output swapped', 'success'); + }; + + const loadExample = () => { + const example = convertFunctions[activeTab as keyof typeof convertFunctions].example; + if (example) { + setInput(example.input); + addToast('Example loaded', 'success'); + } + }; + + const tabConfigs = [ + { id: 'base64', name: 'Base64', color: 'from-blue-500 to-blue-600', hoverColor: 'hover:from-blue-600 hover:to-blue-700', ring: 'ring-blue-500' }, + { id: 'url', name: 'URL', color: 'from-emerald-500 to-emerald-600', hoverColor: 'hover:from-emerald-600 hover:to-emerald-700', ring: 'ring-emerald-500' }, + { id: 'html', name: 'HTML', color: 'from-purple-500 to-purple-600', hoverColor: 'hover:from-purple-600 hover:to-purple-700', ring: 'ring-purple-500' }, + { id: 'hex', name: 'Hex', color: 'from-amber-500 to-amber-600', hoverColor: 'hover:from-amber-600 hover:to-amber-700', ring: 'ring-amber-500' }, + { id: 'binary', name: 'Binary', color: 'from-pink-500 to-pink-600', hoverColor: 'hover:from-pink-600 hover:to-pink-700', ring: 'ring-pink-500' }, + { id: 'jwt', name: 'JWT', color: 'from-orange-500 to-orange-600', hoverColor: 'hover:from-orange-600 hover:to-orange-700', ring: 'ring-orange-500' }, + ]; + + const currentConfig = convertFunctions[activeTab as keyof typeof convertFunctions]; + const currentTabConfig = tabConfigs.find(t => t.id === activeTab)!; + const isJWT = activeTab === 'jwt'; + const hasExample = currentConfig.example; + const IconComponent = currentConfig.icon; + + const inputLooksEncoded = currentConfig.isValidEncoded ? currentConfig.isValidEncoded(input.trim()) : false; + + return ( +
+
+ {/* Header */} +
+
+
+

Encode / Decode Tool

+

Transform text between different encoding formats

+
+
+ + + {input && output && ( + + )} +
+
+
+
+ {/* Tab Selection */} +
+ {tabConfigs.map((tab) => { + const TabIcon = convertFunctions[tab.id as keyof typeof convertFunctions].icon; + return ( + + ); + })} +
+ + {/* Description */} + {currentConfig.description && ( +
+
+
+ +
+
+

+ {currentConfig.description} +

+ {hasExample && ( +
+ Example: "{currentConfig.example.input}" → "{currentConfig.example.output.length > 15 ? currentConfig.example.output.substring(0, 15) + '...' : currentConfig.example.output}" +
+ )} +
+
+
+ )} + + {/* Main Content - Input/Output */} +
+ {/* Input Section */} +
+
+
+

Input

+ {input && inputLooksEncoded && ( +
+ + Encoded +
+ )} +
+
+ {input && ( + <> + + + + )} +
+
+
+