kattoolbox/src/app/tools/html-entities/page.tsx
2026-08-31 07:58:10 +05:30

458 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useCallback, useId } from "react";
import ToolLayout from "@/components/ToolLayout";
const BASIC_ENTITIES_MAP: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
const EXTENDED_ENTITIES_MAP: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
"©": "&copy;",
"®": "&reg;",
"™": "&trade;",
"€": "&euro;",
"£": "&pound;",
"¥": "&yen;",
"¢": "&cent;",
"§": "&sect;",
"°": "&deg;",
"±": "&plusmn;",
"«": "&laquo;",
"»": "&raquo;",
"•": "&bull;",
"…": "&hellip;",
"—": "&mdash;",
"–": "&ndash;",
"“": "&ldquo;",
"”": "&rdquo;",
"‘": "&lsquo;",
"’": "&rsquo;",
"×": "&times;",
"÷": "&divide;",
"µ": "&micro;",
"¶": "&para;",
"¼": "&frac14;",
"½": "&frac12;",
"¾": "&frac34;",
};
type EncodeMode = "basic" | "extended" | "decimal" | "hex";
export default function HtmlEntitiesPage() {
const inputId = useId();
const outputId = useId();
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [encodeMode, setEncodeMode] = useState<EncodeMode>("basic");
const [lastAction, setLastAction] = useState<"encode" | "decode" | null>(null);
const encodeText = useCallback((text: string, mode: EncodeMode) => {
if (!text) {
setOutput("");
setError(null);
return;
}
try {
let result = "";
if (mode === "basic") {
result = text.replace(/[&<>"']/g, (char) => BASIC_ENTITIES_MAP[char] || char);
} else if (mode === "extended") {
result = text.replace(
/[&<>"'©®™€£¥¢§°±«»•…—–“”‘’×÷µ¶¼½¾]/g,
(char) => EXTENDED_ENTITIES_MAP[char] || char
);
} else if (mode === "decimal") {
result = text.replace(/[&<>"']|[^\x20-\x7E\r\n\t]/g, (char) => {
const code = char.codePointAt(0);
return code !== undefined ? `&#${code};` : char;
});
} else if (mode === "hex") {
result = text.replace(/[&<>"']|[^\x20-\x7E\r\n\t]/g, (char) => {
const code = char.codePointAt(0);
return code !== undefined ? `&#x${code.toString(16).toUpperCase()};` : char;
});
}
setOutput(result);
setError(null);
setLastAction("encode");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to encode HTML entities.");
setOutput("");
}
}, []);
const decodeText = useCallback((text: string) => {
if (!text) {
setOutput("");
setError(null);
return;
}
try {
const textarea = document.createElement("textarea");
textarea.innerHTML = text;
const decoded = textarea.value;
setOutput(decoded);
setError(null);
setLastAction("decode");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to decode HTML entities.");
setOutput("");
}
}, []);
const handleEncode = () => {
encodeText(input, encodeMode);
};
const handleDecode = () => {
decodeText(input);
};
const handleCopy = async () => {
if (!output) return;
try {
await navigator.clipboard.writeText(output);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
const textArea = document.createElement("textarea");
textArea.value = output;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleSwap = () => {
const temp = output;
setInput(temp);
setOutput(input);
setError(null);
if (lastAction === "encode") {
setLastAction("decode");
} else if (lastAction === "decode") {
setLastAction("encode");
}
};
const handleClear = () => {
setInput("");
setOutput("");
setError(null);
setLastAction(null);
};
const loadSample = (type: "html" | "symbols" | "entities") => {
setError(null);
if (type === "html") {
const sample = '<div class="alert alert-info">\n <h1>Title: "Welcome & Enjoy"</h1>\n <p>5 > 3 and 2 < 4 are true \'math\' facts!</p>\n</div>';
setInput(sample);
encodeText(sample, encodeMode);
} else if (type === "symbols") {
const sample = 'Price: €99.99 / £85.00 © 2026 DevToolbox™ — "Fast & Secure" — 100° angle • ½ cup';
setInput(sample);
setEncodeMode("extended");
encodeText(sample, "extended");
} else {
const sample = '&lt;div class=&quot;card&quot;&gt;\n &lt;h2&gt;Hello &amp;amp; Welcome!&lt;/h2&gt;\n &lt;p&gt;Price: &amp;euro;50 &amp;copy; 2026&lt;/p&gt;\n&lt;/div&gt;';
setInput(sample);
decodeText(sample);
}
};
return (
<ToolLayout
title="HTML Entity Encode / Decode"
description="Convert special characters to their corresponding HTML entity codes or decode HTML entities back to plain text."
>
<title>HTML Entity Encode &amp; Decode Online — DevToolbox</title>
<meta
name="description"
content="Free online HTML entity encoder and decoder. Convert <, >, &, quotes, symbols to named, decimal, and hex HTML entities."
/>
<div className="space-y-6">
{/* Controls Bar */}
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={handleEncode}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Encode HTML Entities
</button>
<button
type="button"
onClick={handleDecode}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Decode to Plain Text
</button>
<button
type="button"
onClick={handleSwap}
disabled={!output && !input}
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-200 rounded-lg text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
title="Swap input and output"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
Swap
</button>
<button
type="button"
onClick={handleClear}
disabled={!input && !output}
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-700 dark:text-gray-300 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed rounded-lg text-sm font-medium transition-colors cursor-pointer"
>
Clear
</button>
</div>
{/* Sample buttons */}
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-500 dark:text-gray-400 font-medium">Samples:</span>
<button
type="button"
onClick={() => loadSample("html")}
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
>
HTML Code
</button>
<button
type="button"
onClick={() => loadSample("symbols")}
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
>
Symbols &amp; Quotes
</button>
<button
type="button"
onClick={() => loadSample("entities")}
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
>
Encoded Entities
</button>
</div>
</div>
{/* Encoding Mode Selector */}
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800/50 p-3 rounded-lg border border-gray-200 dark:border-gray-700/60">
<span className="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Encoding Format:
</span>
<div className="flex flex-wrap items-center gap-4">
<label className="inline-flex items-center gap-1.5 cursor-pointer">
<input
type="radio"
name="encodeMode"
value="basic"
checked={encodeMode === "basic"}
onChange={() => {
setEncodeMode("basic");
if (lastAction === "encode" && input) {
encodeText(input, "basic");
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
/>
<span>Basic Special Chars <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&lt; &gt; &amp; &quot; &#39;)</code></span>
</label>
<label className="inline-flex items-center gap-1.5 cursor-pointer">
<input
type="radio"
name="encodeMode"
value="extended"
checked={encodeMode === "extended"}
onChange={() => {
setEncodeMode("extended");
if (lastAction === "encode" && input) {
encodeText(input, "extended");
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
/>
<span>Extended Named <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&amp;copy;, &amp;euro;, etc.)</code></span>
</label>
<label className="inline-flex items-center gap-1.5 cursor-pointer">
<input
type="radio"
name="encodeMode"
value="decimal"
checked={encodeMode === "decimal"}
onChange={() => {
setEncodeMode("decimal");
if (lastAction === "encode" && input) {
encodeText(input, "decimal");
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
/>
<span>Decimal Numeric <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&amp;#60;)</code></span>
</label>
<label className="inline-flex items-center gap-1.5 cursor-pointer">
<input
type="radio"
name="encodeMode"
value="hex"
checked={encodeMode === "hex"}
onChange={() => {
setEncodeMode("hex");
if (lastAction === "encode" && input) {
encodeText(input, "hex");
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
/>
<span>Hexadecimal <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&amp;#x3C;)</code></span>
</label>
</div>
</div>
{/* Error Notification */}
{error && (
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-300 text-sm">
<svg className="w-5 h-5 text-red-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="font-semibold">Error</p>
<p className="mt-0.5">{error}</p>
</div>
</div>
)}
{/* Two-Column Editor Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Input Area */}
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<label htmlFor={inputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
Input Text / HTML
</label>
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
{input.length} chars
</span>
</div>
<textarea
id={inputId}
value={input}
onChange={(e) => {
setInput(e.target.value);
if (error) setError(null);
}}
placeholder="Type or paste HTML code or text to encode/decode..."
rows={12}
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed shadow-inner"
/>
</div>
{/* Output Area */}
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<label htmlFor={outputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
Output Result
</label>
{lastAction && (
<span className="px-2 py-0.5 text-xs rounded-full font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
{lastAction === "encode" ? "HTML Entities Encoded" : "Decoded Plain Text"}
</span>
)}
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
{output.length} chars
</span>
<button
type="button"
onClick={handleCopy}
disabled={!output}
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
>
{copied ? (
<>
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
</>
) : (
<>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
<span>Copy</span>
</>
)}
</button>
</div>
</div>
<textarea
id={outputId}
value={output}
readOnly
placeholder="Output will appear here..."
rows={12}
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/60 text-gray-900 dark:text-gray-100 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed select-all"
/>
</div>
</div>
{/* HTML Entities Reference Table */}
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">Key HTML Entities Reference</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2 text-xs font-mono">
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400 font-sans">&lt; (Less than)</span>
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;lt;</span>
</div>
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400 font-sans">&gt; (Greater)</span>
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;gt;</span>
</div>
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400 font-sans">&amp; (Ampersand)</span>
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;amp;</span>
</div>
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400 font-sans">&quot; (Double quote)</span>
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;quot;</span>
</div>
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400 font-sans">&#39; (Single quote)</span>
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;#39;</span>
</div>
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
<span className="text-gray-600 dark:text-gray-400 font-sans">&copy; (Copyright)</span>
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;copy;</span>
</div>
</div>
</div>
</div>
</ToolLayout>
);
}