From c8d6dc54e2ee44daa00b11c6300f4f0d6ef32186 Mon Sep 17 00:00:00 2001 From: Yuval Adam <_@yuv.al> Date: Thu, 8 May 2025 21:31:00 +0200 Subject: Initial migration to AstroJS --- src/components/Cidr.tsx | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/components/Cidr.tsx (limited to 'src/components') 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, 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, 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) => { + 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 ( + +
+
+ {ip.map((octet, i) => ( +
+ 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}`} + /> + {i == 3 ? "/" : "."} +
+ ))} + 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`} + /> +
+ +
+ {bits.map((octet, i) => + {octet.map((bit, j) => {bit})} + )} +
+ +
+ {Object.entries(details).map(([label, val]) => +
+
{val}
+
{label}
+
) + } +
+ +
+
+ +
+
+ +
+
+ +
+ + ); +} \ No newline at end of file -- cgit v1.3.1