diff options
| author | Yuval Adam <_@yuv.al> | 2025-05-08 21:31:00 +0200 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2025-05-08 22:27:03 +0200 |
| commit | c8d6dc54e2ee44daa00b11c6300f4f0d6ef32186 (patch) | |
| tree | 0073da7bc68f5308610ed7d6d7bdc455ad4c6cf7 /src/components | |
| parent | 7450fe585f1253bf30b9ccb7254019aafc5db088 (diff) | |
Initial migration to AstroJS
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/Cidr.tsx | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/src/components/Cidr.tsx b/src/components/Cidr.tsx new file mode 100644 index 0000000..78ec22f --- /dev/null +++ b/src/components/Cidr.tsx @@ -0,0 +1,220 @@ + +import React, { useEffect, useState } from 'react'; +import { Netmask } from 'netmask'; + +export default function Cidr() { + const [ip, setIp] = useState([10, 88, 135, 144]); + const [cidr, setCidr] = useState(28); + const [cols] = useState(["bg-purple-400", "bg-red-400", "bg-green-400", "bg-yellow-400", "bg-slate-300"]); + const [isCopied, setIsCopied] = useState(false); + const [isShared, setIsShared] = useState(false); + + const bits = ip.map(octet => Array.from({ length: 8 }, (_, i) => (octet >> (7 - i)) & 1)); + + const parseOctet = (val: string, max: number) => { + const num = Number(val); + if (isNaN(num) || num < 0) return 0; + if (num > max) return max; + return num; + } + + const setIpOctet = (i: number, val: number) => { + const newIp = [...ip]; + newIp[i] = val; + setIp(newIp); + } + + const handleWheel = (event: React.WheelEvent<HTMLInputElement>, i: number, max: number) => { + event.preventDefault(); + const min = 0; + const target = event.currentTarget as HTMLInputElement; + let value = parseInt(target.value); + + if (event.deltaY > 0 && value > min) { + value -= 1; + } + if (event.deltaY < 0 && value < max) { + value += 1; + } + + if (i == 4) { + setCidr(value); + } + else { + setIpOctet(i, value); + } + } + + const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, i: number, max: number) => { + const min = 0; + const target = event.currentTarget as HTMLInputElement; + let value = parseInt(target.value); + + if (event.key === "ArrowDown" && value > min) { + value -= 1; + } + else if (event.key === "ArrowUp" && value < max) { + value += 1; + } + else if (event.key === '.') { + event.preventDefault(); + const parent = (event.target as Node).parentNode; + const next = parent?.nextSibling?.firstChild; + if (next instanceof HTMLInputElement) { + next.select(); + next.focus(); + } + } + else if (event.key === "/") { + event.preventDefault(); + const parent = (event.target as Node).parentNode; + const mask = parent?.nextSibling; + if (mask instanceof HTMLInputElement) { + mask.select(); + mask.focus(); + } + } + + if (i == 4) { + setCidr(value); + } + else { + setIpOctet(i, value); + } + } + + const updateCidrString = (val: string) => { + const parts = val.split("/"); + const ip = parts[0].split(".").map(Number); + const cidr = Number(parts[1]); + + if (ip.length != 4) { + return + } + + ip.forEach(octet => { + if (Number.isNaN(octet) || octet < 0 || octet > 255) { + return + } + }) + setIp(ip); + + if (Number.isNaN(cidr) || cidr < 0 || cidr > 32) { + return + } + setCidr(cidr); + } + + const handlePaste = (event: React.ClipboardEvent<HTMLInputElement>) => { + event.preventDefault(); + if (event.clipboardData === null) return; + const text = event.clipboardData.getData('Text'); + updateCidrString(text); + } + + const handleCopy = async () => { + await navigator.clipboard.writeText(pretty); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }; + + const handleShare = async () => { + const frag = "#" + ip.join(".") + "/" + cidr; + window.location.hash = frag; + await navigator.clipboard.writeText(window.location.toString()); + setIsShared(true); + setTimeout(() => setIsShared(false), 2000); + }; + + useEffect(() => { + const fragment = window.location.hash.slice(1); + if (fragment) { + updateCidrString(fragment); + } + }, []); + + const pretty = ip.join('.') + '/' + cidr; + const netmask = new Netmask(pretty); + + const details = { + "Netmask": netmask.mask, + "CIDR Base IP": netmask.base, + "Broadcast IP": netmask.broadcast || "None", + "Count": netmask.size.toLocaleString(), + "First Usable IP": netmask.first, + "Last Usable IP": netmask.last + } + + return ( + + <div className="my-6 border-0 sm:border border-gray-300 rounded-lg"> + <div className="flex flex-wrap justify-center gap-4 my-10"> + {ip.map((octet, i) => ( + <div key={`octet-${i}`}> + <input + key={`inp-${i}`} + type="text" + value={octet} + onChange={(e) => setIpOctet(i, parseOctet(e.target.value, 255))} + onWheel={(e) => handleWheel(e, i, 255)} + onKeyDown={(e) => handleKeyDown(e, i, 255)} + onPaste={handlePaste} + className={`w-20 h-20 text-3xl text-center rounded-md ${cols[i]}`} + maxLength={3} + aria-label={`Octet ${i + 1}`} + /> + <span className="text-5xl pl-4" key={`sep-${i}`}>{i == 3 ? "/" : "."}</span> + </div> + ))} + <input + type="text" + key={`inp-cidr`} + value={cidr} + onChange={(e) => setCidr(parseOctet(e.target.value, 32))} + onWheel={(e) => handleWheel(e, 4, 32)} + onKeyDown={(e) => handleKeyDown(e, 4, 32)} + onPaste={handlePaste} + className={`w-20 h-20 text-3xl text-center rounded-md ${cols[4]}`} + maxLength={2} + aria-label={`Network bits`} + /> + </div> + + <div className="flex flex-wrap justify-center gap-4 mt-10"> + {bits.map((octet, i) => <span key={`octet-${i}`} className="px-1"> + {octet.map((bit, j) => <span key={`octet-${i}-bit-${j}`} className={`font-mono border-y ${j == 0 && "border-l"} border-r border-gray-700 px-2 py-1 ${i * 8 + j < cidr ? cols[i] : cols[4]}`}>{bit}</span>)} + </span>)} + </div> + + <div className="text-center text-xl mt-8 grid grid-cols-1 sm:grid-cols-3 gap-4"> + {Object.entries(details).map(([label, val]) => + <div className="sm:p-3 " key={`detail-${label}`}> + <div className="font-mono">{val}</div> + <div className="font-bold tracking-tight text-slate-500 sm:text-3xl text-2xl">{label}</div> + </div>) + } + </div> + + <div className="flex justify-end p-5"> + <div className="mx-2"> + <button + onClick={handleCopy} + className="flex items-center gap-2 px-4 py-2 text-sm text-gray-100 bg-blue-600 rounded-md hover:bg-blue-700" + > + {isCopied ? "Copied!" : "Copy CIDR"} + </button> + </div> + <div className="mx-2"> + <button + onClick={handleShare} + className="flex items-center gap-2 px-4 py-2 text-sm text-gray-100 bg-blue-600 rounded-md hover:bg-blue-700" + > + {isShared ? "Copied!" : "Copy Share Link"} + </button> + </div> + </div> + + </div> + + ); +}
\ No newline at end of file |
