ipify/frontend/src/App.jsx
2025-12-23 21:05:31 +02:00

363 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import axios from 'axios'
import './App.css'
const API_URL = 'http://localhost:8000'
function App() {
const [ipAddress, setIpAddress] = useState('')
const [cidr, setCidr] = useState('24')
const [subnetMask, setSubnetMask] = useState('')
const [inputType, setInputType] = useState('cidr') // 'cidr' or 'mask'
const [result, setResult] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
// Validate IP address format
const validateIpAddress = (ip) => {
if (!ip || !ip.trim()) {
return 'IP address is required'
}
const ipPattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
if (!ipPattern.test(ip)) {
return 'Invalid IP format. Expected: xxx.xxx.xxx.xxx'
}
const octets = ip.split('.')
for (let i = 0; i < octets.length; i++) {
const num = parseInt(octets[i])
if (isNaN(num) || num < 0 || num > 255) {
return `Invalid octet ${i + 1}: must be between 0-255`
}
}
return null
}
// Validate subnet mask format
const validateSubnetMask = (mask) => {
if (!mask || !mask.trim()) {
return 'Subnet mask is required'
}
const parts = mask.split('.')
if (parts.length !== 4) {
return 'Subnet mask must have 4 octets'
}
// Check each octet is 0-255
for (let part of parts) {
const num = parseInt(part)
if (isNaN(num) || num < 0 || num > 255) {
return 'Each octet must be between 0-255'
}
}
// Common valid subnet masks
const validMasks = [
'255.255.255.255', '255.255.255.254', '255.255.255.252', '255.255.255.248',
'255.255.255.240', '255.255.255.224', '255.255.255.192', '255.255.255.128',
'255.255.255.0', '255.255.254.0', '255.255.252.0', '255.255.248.0',
'255.255.240.0', '255.255.224.0', '255.255.192.0', '255.255.128.0',
'255.255.0.0', '255.254.0.0', '255.252.0.0', '255.248.0.0',
'255.240.0.0', '255.224.0.0', '255.192.0.0', '255.128.0.0',
'255.0.0.0', '254.0.0.0', '252.0.0.0', '248.0.0.0',
'240.0.0.0', '224.0.0.0', '192.0.0.0', '128.0.0.0', '0.0.0.0'
]
if (!validMasks.includes(mask)) {
return 'Invalid subnet mask (bits must be contiguous)'
}
return null
}
const calculateSubnet = async (e) => {
e.preventDefault()
setError('')
setResult(null)
// Frontend validation
const ipError = validateIpAddress(ipAddress)
if (ipError) {
setError(ipError)
return
}
if (inputType === 'cidr') {
const cidrNum = parseInt(cidr)
if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) {
setError('CIDR must be between 0 and 32')
return
}
} else {
const maskError = validateSubnetMask(subnetMask)
if (maskError) {
setError(maskError)
return
}
}
setLoading(true)
try {
const payload = {
ip_address: ipAddress,
}
if (inputType === 'cidr') {
payload.cidr = parseInt(cidr)
} else {
payload.subnet_mask = subnetMask
}
const response = await axios.post(`${API_URL}/calculate`, payload)
setResult(response.data)
} catch (err) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to calculate subnet'
setError(errorMessage)
} finally {
setLoading(false)
}
}
const InfoCard = ({ label, value, highlight }) => (
<div className={`info-card ${highlight ? 'highlight' : ''}`}>
<div className="info-label">{label}</div>
<div className="info-value">{value}</div>
</div>
)
const BinaryCard = ({ label, decimal, binary }) => (
<div className="binary-card">
<div className="binary-label">{label}</div>
<div className="binary-decimal">{decimal}</div>
<div className="binary-value">{binary}</div>
</div>
)
// Calculate ALL next sequential networks based on the current network
const calculateNetworkRanges = (result) => {
const networkId = result.network_id
const cidr = result.cidr
const totalAddresses = result.total_hosts
// Parse network ID to get starting IP
const parts = networkId.split('.').map(Number)
let networkNum = (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
const networks = []
// Calculate all possible networks until end of IPv4 space
// For performance, limit to 10000 networks max
const maxNetworks = 10000
let count = 0
// Generate next sequential networks
while (count < maxNetworks) {
const currentNetworkNum = networkNum + (count * totalAddresses)
// Check if we're still within valid IPv4 range
const broadcastNum = currentNetworkNum + totalAddresses - 1
if (currentNetworkNum > 0xFFFFFFFF || broadcastNum > 0xFFFFFFFF) break
// Convert number back to IP address
const networkAddress = `${(currentNetworkNum >>> 24) & 0xFF}.${(currentNetworkNum >>> 16) & 0xFF}.${(currentNetworkNum >>> 8) & 0xFF}.${currentNetworkNum & 0xFF}`
const broadcastAddress = `${(broadcastNum >>> 24) & 0xFF}.${(broadcastNum >>> 16) & 0xFF}.${(broadcastNum >>> 8) & 0xFF}.${broadcastNum & 0xFF}`
// Calculate first and last usable IPs
let firstUsable, lastUsable
if (cidr === 32) {
firstUsable = networkAddress
lastUsable = networkAddress
} else if (cidr === 31) {
firstUsable = networkAddress
lastUsable = broadcastAddress
} else {
const firstUsableNum = currentNetworkNum + 1
const lastUsableNum = broadcastNum - 1
firstUsable = `${(firstUsableNum >>> 24) & 0xFF}.${(firstUsableNum >>> 16) & 0xFF}.${(firstUsableNum >>> 8) & 0xFF}.${firstUsableNum & 0xFF}`
lastUsable = `${(lastUsableNum >>> 24) & 0xFF}.${(lastUsableNum >>> 16) & 0xFF}.${(lastUsableNum >>> 8) & 0xFF}.${lastUsableNum & 0xFF}`
}
networks.push({
networkAddress: `${networkAddress}/${cidr}`,
firstUsable,
lastUsable,
broadcastAddress,
usableHosts: result.usable_hosts
})
count++
}
return networks
}
return (
<div className="app">
<div className="container">
<h1 className="title">🌐 IP Subnet Calculator</h1>
<form onSubmit={calculateSubnet} className="form">
<div className="input-group">
<label htmlFor="ipAddress">IP Address</label>
<input
id="ipAddress"
type="text"
value={ipAddress}
onChange={(e) => setIpAddress(e.target.value)}
placeholder="e.g., 192.168.1.1"
required
/>
</div>
<div className="input-type-selector">
<button
type="button"
className={`type-btn ${inputType === 'cidr' ? 'active' : ''}`}
onClick={() => setInputType('cidr')}
>
CIDR Notation
</button>
<button
type="button"
className={`type-btn ${inputType === 'mask' ? 'active' : ''}`}
onClick={() => setInputType('mask')}
>
Subnet Mask
</button>
</div>
{inputType === 'cidr' ? (
<div className="input-group">
<label htmlFor="cidr">CIDR (1-32)</label>
<input
id="cidr"
type="number"
min="1"
max="32"
value={cidr}
onChange={(e) => setCidr(e.target.value)}
required
/>
</div>
) : (
<div className="input-group">
<label htmlFor="subnetMask">Subnet Mask</label>
<input
id="subnetMask"
type="text"
value={subnetMask}
onChange={(e) => setSubnetMask(e.target.value)}
placeholder="e.g., 255.255.255.0"
required
/>
</div>
)}
<button type="submit" className="calculate-btn" disabled={loading}>
{loading ? 'Calculating...' : 'Calculate Subnet'}
</button>
</form>
{error && (
<div className="error-message">
{error}
</div>
)}
{result && (
<div className="results">
<h2 className="results-title">Subnet Information</h2>
{/* Binary Representation Section */}
<div className="binary-section">
<h3 className="section-title">Binary Representation</h3>
<div className="binary-grid">
<BinaryCard
label="Address"
decimal={result.ip_address}
binary={result.ip_address_binary}
/>
<BinaryCard
label="Netmask"
decimal={`${result.subnet_mask} = /${result.cidr}`}
binary={result.subnet_mask_binary}
/>
<BinaryCard
label="Wildcard"
decimal={result.wildcard_mask}
binary={result.wildcard_binary}
/>
<BinaryCard
label="Network"
decimal={`${result.network_id}/${result.cidr}`}
binary={result.network_id_binary}
/>
<BinaryCard
label="Broadcast"
decimal={result.broadcast_address}
binary={result.broadcast_binary}
/>
</div>
</div>
{/* Main Information Section */}
<h3 className="section-title">Network Details</h3>
<div className="results-grid">
<InfoCard label="Network ID" value={`${result.network_id}/${result.cidr}`} highlight />
<InfoCard label="Broadcast Address" value={result.broadcast_address} highlight />
<InfoCard label="First Usable IP" value={result.first_usable_ip} />
<InfoCard label="Last Usable IP" value={result.last_usable_ip} />
<InfoCard label="Total Addresses" value={result.total_hosts.toLocaleString()} highlight />
<InfoCard label="Usable Hosts" value={result.usable_hosts.toLocaleString()} highlight />
<InfoCard label="Subnet Mask" value={result.subnet_mask} />
<InfoCard label="Wildcard Mask" value={result.wildcard_mask} />
<InfoCard label="Network Class" value={result.network_class} />
<InfoCard label="IP Type" value={result.ip_type} />
<InfoCard
label="Private IP"
value={result.is_private ? '✅ Yes' : '❌ No'}
/>
</div>
{/* Next Networks Section */}
<h3 className="section-title">
📊 All Next Sequential Networks ({calculateNetworkRanges(result).length.toLocaleString()} networks)
</h3>
<div className="ranges-table">
<div className="table-header">
<div className="table-cell">#</div>
<div className="table-cell">Network</div>
<div className="table-cell">Usable Range</div>
<div className="table-cell">Broadcast</div>
<div className="table-cell">Hosts</div>
</div>
<div className="table-body">
{calculateNetworkRanges(result).map((network, index) => (
<div key={index} className="table-row">
<div className="table-cell range-id">{index + 1}</div>
<div className="table-cell network-address">{network.networkAddress}</div>
<div className="table-cell">{network.firstUsable} - {network.lastUsable}</div>
<div className="table-cell">{network.broadcastAddress}</div>
<div className="table-cell">{network.usableHosts.toLocaleString()}</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
)
}
export default App