Initial commit, framework/frontend assets setup
This commit is contained in:
commit
9d9858bb37
32 changed files with 4651 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Vendor
|
||||
node_modules
|
||||
vendor
|
||||
public/dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
39
app/Controllers/Api/SubnetController.php
Normal file
39
app/Controllers/Api/SubnetController.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Services\Subnet;
|
||||
use Core\Controllers\Controller;
|
||||
use Core\View\Render;
|
||||
use Exception;
|
||||
|
||||
class SubnetController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get all subnet data
|
||||
*
|
||||
* @return \Core\View\Render
|
||||
*/
|
||||
public function data(): Render
|
||||
{
|
||||
// Get the user input
|
||||
$subnet = $this->request->get('subnet', '');
|
||||
|
||||
try {
|
||||
// Validate the input
|
||||
if (!str_contains($subnet, '/')) {
|
||||
throw new Exception('Invalid subnet format.');
|
||||
}
|
||||
|
||||
// Delegate calculation to subnet service
|
||||
$subnetInfo = Subnet::calculate($subnet);
|
||||
|
||||
return $this->response->json()
|
||||
->with('result', $subnetInfo);
|
||||
} catch (Exception $e) {
|
||||
return $this->response->status(403)
|
||||
->json()
|
||||
->with('message', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
19
app/Controllers/HomeController.php
Normal file
19
app/Controllers/HomeController.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Controllers\Controller;
|
||||
use Core\View\Render;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Render index
|
||||
*
|
||||
* @return \Core\View\Render
|
||||
*/
|
||||
public function index(): Render
|
||||
{
|
||||
return $this->response->view('subnet');
|
||||
}
|
||||
}
|
104
app/Services/Subnet.php
Normal file
104
app/Services/Subnet.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Subnet
|
||||
{
|
||||
/**
|
||||
* Calculate subnet
|
||||
*
|
||||
* @param string $subnet
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function calculate(string $subnet): array
|
||||
{
|
||||
[$ip, $cidr] = explode('/', $subnet);
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return self::calculateIPv4($ip, intval($cidr));
|
||||
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return self::calculateIPv6($ip, intval($cidr));
|
||||
} else {
|
||||
throw new Exception("Invalid IP address.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate IPv4
|
||||
*
|
||||
* @param string $ip
|
||||
* @param int $cidr
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function calculateIPv4(string $ip, int $cidr): array
|
||||
{
|
||||
if ($cidr < 0 || $cidr > 32) {
|
||||
throw new Exception("CIDR must be between 0 and 32.");
|
||||
}
|
||||
|
||||
$hosts = (1 << (32 - $cidr)) - 2; // Excludes network and broadcast
|
||||
$ipLong = ip2long($ip);
|
||||
$mask = -1 << (32 - $cidr);
|
||||
$network = $ipLong & $mask;
|
||||
|
||||
$firstIP = $network + 1;
|
||||
$lastIP = $network + $hosts;
|
||||
|
||||
return [
|
||||
'network' => long2ip($network),
|
||||
'first' => long2ip($firstIP),
|
||||
'last' => long2ip($lastIP),
|
||||
'hosts' => $hosts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate IPv6
|
||||
*
|
||||
* @param string $ip
|
||||
* @param int $cidr
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function calculateIPv6(string $ip, int $cidr): array
|
||||
{
|
||||
if ($cidr < 0 || $cidr > 128) {
|
||||
throw new Exception("CIDR must be between 0 and 128.");
|
||||
}
|
||||
|
||||
// Convert IP to binary representation
|
||||
$binaryIP = inet_pton($ip);
|
||||
if ($binaryIP === false) {
|
||||
throw new Exception("Failed to parse IPv6 address.");
|
||||
}
|
||||
|
||||
// Calculate network address
|
||||
$network = substr($binaryIP, 0, intval($cidr / 8));
|
||||
$remainder = $cidr % 8;
|
||||
|
||||
if ($remainder > 0) {
|
||||
$lastByte = ord($binaryIP[$cidr / 8]) & (0xFF << (8 - $remainder));
|
||||
$network .= chr($lastByte);
|
||||
}
|
||||
|
||||
// Pad the rest with zeros
|
||||
$network = str_pad($network, 16, "\0");
|
||||
|
||||
// Calculate first and last addresses
|
||||
$networkAddress = inet_ntop($network);
|
||||
$totalHosts = $cidr == 128 ? 1 : bcpow(2, 128 - $cidr);
|
||||
$firstHost = ($cidr == 128) ? $networkAddress : inet_ntop($network);
|
||||
$lastHost = inet_ntop(pack("H*", str_repeat("F", 32)));
|
||||
|
||||
return [
|
||||
'network' => $networkAddress,
|
||||
'first' => $firstHost,
|
||||
'last' => $lastHost,
|
||||
'hosts' => $totalHosts,
|
||||
];
|
||||
}
|
||||
}
|
31
composer.json
Normal file
31
composer.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "maartenvr98/bit",
|
||||
"type": "project",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Maarten",
|
||||
"email": "maarten@vrijssel.nl"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.3.0",
|
||||
"filp/whoops": "^2.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/var-dumper": "^7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Core\\": "src/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"composer dump-autoload -o"
|
||||
]
|
||||
}
|
||||
}
|
304
composer.lock
generated
Normal file
304
composer.lock
generated
Normal file
|
@ -0,0 +1,304 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "5b92bb8b85e08563f751eb7e477addfe",
|
||||
"packages": [
|
||||
{
|
||||
"name": "filp/whoops",
|
||||
"version": "2.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/filp/whoops.git",
|
||||
"reference": "befcdc0e5dce67252aa6322d82424be928214fa2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2",
|
||||
"reference": "befcdc0e5dce67252aa6322d82424be928214fa2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3",
|
||||
"symfony/var-dumper": "^4.0 || ^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/var-dumper": "Pretty print complex values better with var-dumper available",
|
||||
"whoops/soap": "Formats errors as SOAP responses"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Whoops\\": "src/Whoops/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Filipe Dobreira",
|
||||
"homepage": "https://github.com/filp",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "php error handling for cool kids",
|
||||
"homepage": "https://filp.github.io/whoops/",
|
||||
"keywords": [
|
||||
"error",
|
||||
"exception",
|
||||
"handling",
|
||||
"library",
|
||||
"throwable",
|
||||
"whoops"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/filp/whoops/issues",
|
||||
"source": "https://github.com/filp/whoops/tree/2.16.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/denis-sokolov",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-25T12:00:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for logging libraries",
|
||||
"homepage": "https://github.com/php-fig/log",
|
||||
"keywords": [
|
||||
"log",
|
||||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/3.0.2"
|
||||
},
|
||||
"time": "2024-09-11T13:17:53+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.31.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v7.1.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "7bb01a47b1b00428d32b5e7b4d3b2d1aa58d3db8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/7bb01a47b1b00428d32b5e7b4d3b2d1aa58d3db8",
|
||||
"reference": "7bb01a47b1b00428d32b5e7b4d3b2d1aa58d3db8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-iconv": "*",
|
||||
"symfony/console": "^6.4|^7.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0",
|
||||
"symfony/process": "^6.4|^7.0",
|
||||
"symfony/uid": "^6.4|^7.0",
|
||||
"twig/twig": "^3.0.4"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/var-dump-server"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions/dump.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\VarDumper\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides mechanisms for walking through any arbitrary PHP variable",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"debug",
|
||||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v7.1.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-08T15:46:42+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
8
config/routes.php
Normal file
8
config/routes.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
use App\Controllers\Api\SubnetController;
|
||||
use App\Controllers\HomeController;
|
||||
use Core\Router\Router;
|
||||
|
||||
Router::get('/', HomeController::class, 'index');
|
||||
Router::post('/api/subnet', SubnetController::class, 'data');
|
4
global.d.ts
vendored
Normal file
4
global.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module '*.scss' {
|
||||
const content: { [className: string]: string };
|
||||
export default content;
|
||||
}
|
1881
package-lock.json
generated
Normal file
1881
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
package.json
Normal file
28
package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "bit",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"watch": "vite build --watch & vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.9.3",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-sass-dts": "^1.3.29"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.7",
|
||||
"postcss": "^8.4.49",
|
||||
"sass": "^1.81.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-toast-notification": "^3"
|
||||
}
|
||||
}
|
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
5
public/.htaccess
Normal file
5
public/.htaccess
Normal file
|
@ -0,0 +1,5 @@
|
|||
RewriteEngine on
|
||||
RewriteCond %{SCRIPT_FILENAME} !-f
|
||||
RewriteCond %{SCRIPT_FILENAME} !-d
|
||||
RewriteCond %{SCRIPT_FILENAME} !-l
|
||||
RewriteRule ^(.*)$ index.php/$1
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
11
public/index.php
Normal file
11
public/index.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use Core\Router\Router;
|
||||
|
||||
require '../vendor/autoload.php';
|
||||
|
||||
// Load routes
|
||||
require '../config/routes.php';
|
||||
|
||||
// Dispatch router
|
||||
Router::dispatch();
|
100
readme.md
Normal file
100
readme.md
Normal file
|
@ -0,0 +1,100 @@
|
|||
# IPcalc-u-later
|
||||
|
||||
## Overview
|
||||
|
||||
**IPcalc-u-later** is a web application that calculates information about an IP subnet. Given an IPv4 or IPv6 address and subnet mask in CIDR notation (e.g., `213.136.12.128/27`), the application will return the following information:
|
||||
|
||||
- **Network Address**
|
||||
- **First IP Address**
|
||||
- **Last IP Address**
|
||||
- **Number of Usable Hosts**
|
||||
|
||||
This application is built using the **MVC (Model-View-Controller)** design pattern, without relying on any PHP frameworks, to meet the challenge requirements. The back-end is developed in PHP, and the user interface is created using HTML, TailwindCSS, TypeScript and Vue.
|
||||
|
||||
### Example Inputs and Outputs:
|
||||
|
||||
#### IPv4 Example:
|
||||
- **Input**: `213.136.12.128/27`
|
||||
- **Output**:
|
||||
- Network: `213.136.12.128`
|
||||
- First: `213.136.12.129`
|
||||
- Last: `213.136.12.158`
|
||||
- Hosts: `30`
|
||||
|
||||
#### IPv6 Example:
|
||||
- **Input**: `2001:db8:85a3:8a2e::/64`
|
||||
- **Output**:
|
||||
- Network: `2001:0db8:85a3:8a2e::`
|
||||
- First: `2001:0db8:85a3:8a2e:0000:0000:0000:0000`
|
||||
- Last: `2001:0db8:85a3:8a2e:ffff:ffff:ffff:ffff`
|
||||
- Hosts: `18446744073709551616`
|
||||
|
||||
## Features
|
||||
|
||||
- **Back-end**:
|
||||
- MVC architecture (without using a framework)
|
||||
- IP address information and calculation logic implemented in PHP
|
||||
- Supports both IPv4 and IPv6
|
||||
- Validation for invalid subnet input
|
||||
- Self-written PHP classes for IP address calculations
|
||||
- **Front-end**:
|
||||
- Simple, responsive design using HTML, TailwindCSS, TypeScript and Vue
|
||||
- Input field to enter the subnet
|
||||
- Displays the result in a user-friendly format
|
||||
- **Extras** (Optional Features):
|
||||
- Optionally, uses a template engine for rendering views (Vue)
|
||||
- Optionally, third-party PHP libraries via Composer
|
||||
- Version control using Git
|
||||
|
||||
## Requirements
|
||||
|
||||
### Server-side
|
||||
- PHP 8.3+ (or higher) for processing the back-end logic
|
||||
- A web server like Apache or Nginx to host the PHP application
|
||||
- Composer (for managing third-party PHP libraries, if used)
|
||||
|
||||
### Client-side
|
||||
- **Node.js**: Version **20** or higher for front-end development
|
||||
- **Yarn**: Version **1.22** or higher for managing JavaScript dependencies.
|
||||
- **Any modern web browser**: Chrome, Firefox, Safari, etc.
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd ipcalc-u-later
|
||||
|
||||
2. **Install PHP dependencies**:
|
||||
```bash
|
||||
composer install
|
||||
|
||||
3. **Set up Node.js and Yarn (for front-end development)**:
|
||||
- Install Node.js (ensure you're using version 20 or higher)
|
||||
- You can download and install the latest version from Node.js [official site](https://nodejs.org/en).
|
||||
- Install Yarn globally via npm (if not already installed):
|
||||
```bash
|
||||
npm install -g yarn
|
||||
|
||||
4. **Install frontend dependencies**
|
||||
```bash
|
||||
yarn install
|
||||
|
||||
5. **Setup the server**:
|
||||
- If you're using Apache, ensure that mod_rewrite is enabled and the .htaccess file is properly configured.
|
||||
- If you're using Nginx, configure the server block to point to the public directory.
|
||||
|
||||
6. **Run the server**:
|
||||
```bash
|
||||
php -S localhost:8000 -t public
|
||||
|
||||
7. **Acces the application**:
|
||||
- Open a web browser and navigate to http://localhost:8000 (or the appropriate URL based on your server configuration).
|
||||
|
||||
## Licence
|
||||
|
||||
This project is licensed under the MIT License
|
||||
|
||||
## Acknowledgements
|
||||
- The MVC design pattern was used to structure the application.
|
||||
- This project was developed as part of a technical challenge.
|
105
resources/scripts/app/Subnet.vue
Normal file
105
resources/scripts/app/Subnet.vue
Normal file
|
@ -0,0 +1,105 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
import Button from '@app/components/Button.vue';
|
||||
import Input from '@app/components/Input.vue';
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const subnet = ref( '' );
|
||||
const isLoading = ref( false );
|
||||
const hasResults = ref( false );
|
||||
const results = ref( {} );
|
||||
|
||||
/**
|
||||
* Load subnet data based on input
|
||||
*/
|
||||
const getSubnetData = () => {
|
||||
// Enable loading icon
|
||||
isLoading.value = true;
|
||||
|
||||
// Create post data
|
||||
const postData = new FormData();
|
||||
postData.append( 'subnet', subnet.value );
|
||||
|
||||
// Send request
|
||||
axios.post( '/api/subnet', postData ).then( (response) => {
|
||||
const { data } = response;
|
||||
|
||||
// Load new block
|
||||
isLoading.value = false;
|
||||
hasResults.value = true;
|
||||
results.value = data.result;
|
||||
} ).catch( (error) => {
|
||||
isLoading.value = false;
|
||||
|
||||
// Show error message
|
||||
const errorMessage = error.response.data.message || 'Something went wrong.';
|
||||
$toast.error(errorMessage, {
|
||||
position: 'top',
|
||||
duration: 1500,
|
||||
});
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset form by resetting al vars
|
||||
*/
|
||||
const resetForm = () => {
|
||||
subnet.value = '';
|
||||
isLoading.value = false;
|
||||
hasResults.value = false;
|
||||
results.value = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center items-center min-h-screen">
|
||||
<div class="sm:w-3/4 md:w-2/3 lg:w-2/4 xl:w-2/5 2xl:w-1/5 -translate-y-24">
|
||||
<img src="https://www.bit.nl/assets/images/bit_logo_white.png" alt="BIT Logo" class="mx-auto w-52 mb-6">
|
||||
|
||||
<div class="bg-white p-10 rounded-xl shadow-2xl">
|
||||
<h1 class="text-2xl font-semibold text-center mb-6">IPcalc-u-later</h1>
|
||||
|
||||
<div v-if="hasResults">
|
||||
<table class="min-w-full table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="">
|
||||
<td class="py-2 font-medium">Network Address</td>
|
||||
<td class="py-2 text-right">{{ results.network }}</td>
|
||||
</tr>
|
||||
<tr class="">
|
||||
<td class="py-2 font-medium">First Usable IP</td>
|
||||
<td class="py-2 text-right">{{ results.first }}</td>
|
||||
</tr>
|
||||
<tr class="">
|
||||
<td class="py-2 font-medium">Last Usable IP</td>
|
||||
<td class="py-2 text-right">{{ results.last }}</td>
|
||||
</tr>
|
||||
<tr class="">
|
||||
<td class="py-2 font-medium">Number of Usable Hosts</td>
|
||||
<td class="py-2 text-right">{{ results.hosts }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button @click="resetForm">Reset</Button>
|
||||
</div>
|
||||
|
||||
<form v-else method="post" class="text-left" @submit.prevent="getSubnetData">
|
||||
<div class="mb-4">
|
||||
<label for="subnet" class="block text-sm font-medium">Enter Subnet</label>
|
||||
|
||||
<Input name="subnet" v-model="subnet" placeholder="(e.g. 192.168.1.0/24 or 2001:db8::/64)" />
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
<i class="fa-solid fa-circle-notch fa-pulse" v-if="isLoading"></i>
|
||||
<span v-else>Submit</span>
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
14
resources/scripts/app/components/Button.vue
Normal file
14
resources/scripts/app/components/Button.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
defineProps( {
|
||||
type: {
|
||||
type: String,
|
||||
default: "button"
|
||||
}
|
||||
} )
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :type class="w-full py-2 mt-4 bg-primary text-white rounded-lg hover:bg-primary-light focus:outline-none focus:ring-2 focus:ring-primary-light">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
45
resources/scripts/app/components/Input.vue
Normal file
45
resources/scripts/app/components/Input.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps( {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
}
|
||||
} );
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const inputValue = ref(props.modelValue);
|
||||
const updateValue = () => {
|
||||
emit('update:modelValue', inputValue.value);
|
||||
};
|
||||
|
||||
// If the modelValue prop changes externally, update the local ref
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
inputValue.value = newVal;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="text"
|
||||
:id="name"
|
||||
:name="name"
|
||||
class="mt-1 p-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
:placeholder
|
||||
:required
|
||||
v-model="inputValue"
|
||||
@input="updateValue"
|
||||
>
|
||||
</template>
|
21
resources/scripts/main.ts
Normal file
21
resources/scripts/main.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import '@styles/main.scss';
|
||||
import { createApp } from 'vue';
|
||||
import ToastPlugin from 'vue-toast-notification';
|
||||
import 'vue-toast-notification/dist/theme-default.css';
|
||||
|
||||
// Import components
|
||||
import Subnet from '@app/Subnet.vue';
|
||||
|
||||
// Initialize vue app
|
||||
function initializeApp(element: string, component: any): void {
|
||||
const app = createApp(component);
|
||||
|
||||
// Use plugins and global settings
|
||||
app.use(ToastPlugin);
|
||||
|
||||
// Mount the app to a specific DOM element
|
||||
app.mount(element);
|
||||
}
|
||||
|
||||
// Load components
|
||||
initializeApp('#subnet-app', Subnet);
|
3
resources/styles/main.scss
Normal file
3
resources/styles/main.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
14
resources/views/subnet.php
Normal file
14
resources/views/subnet.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IPcalc-u-later</title>
|
||||
|
||||
<link rel="stylesheet" href="/dist/css/app.css">
|
||||
<script type="module" src="/dist/scripts/app.js"></script>
|
||||
</head>
|
||||
<body class="bg-gradient-to-r from-primary to-primary-light">
|
||||
<div id="subnet-app"></div>
|
||||
<script src="https://kit.fontawesome.com/02e67c0aed.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
32
src/Controllers/Controller.php
Normal file
32
src/Controllers/Controller.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Controllers;
|
||||
|
||||
use Core\Http\Request;
|
||||
use Core\Http\Response;
|
||||
|
||||
class Controller
|
||||
{
|
||||
/**
|
||||
* Request handler
|
||||
*
|
||||
* @var \Core\Http\Request
|
||||
*/
|
||||
protected Request $request;
|
||||
|
||||
/**
|
||||
* Response handler
|
||||
*
|
||||
* @var \Core\Http\Response
|
||||
*/
|
||||
protected Response $response;
|
||||
|
||||
/**
|
||||
* Load controller helpers
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->response = new Response();
|
||||
}
|
||||
}
|
55
src/Exceptions/Exceptions.php
Normal file
55
src/Exceptions/Exceptions.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Exceptions;
|
||||
|
||||
use Throwable;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run as Whoops;
|
||||
|
||||
class Exceptions
|
||||
{
|
||||
/**
|
||||
* Exceptions handler instance
|
||||
*
|
||||
* @var Whoops
|
||||
*/
|
||||
private static Whoops $instance;
|
||||
|
||||
/**
|
||||
* Get exceptions handler instance
|
||||
*
|
||||
* @return \Whoops\Run
|
||||
*/
|
||||
public static function handler(): Whoops
|
||||
{
|
||||
if (!isset(self::$instance)) {
|
||||
$instance = new Whoops();
|
||||
$instance->pushHandler(new PrettyPageHandler());
|
||||
|
||||
self::$instance = $instance;
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch all exceptions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function catch(): void
|
||||
{
|
||||
self::handler()->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch single exception
|
||||
*
|
||||
* @param \Throwable $exception
|
||||
* @return void
|
||||
*/
|
||||
public static function catchOne(Throwable $exception): void
|
||||
{
|
||||
self::handler()->handleException($exception);
|
||||
}
|
||||
}
|
69
src/Http/Request.php
Normal file
69
src/Http/Request.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Http;
|
||||
|
||||
class Request
|
||||
{
|
||||
/**
|
||||
* Get request method
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function method(): mixed
|
||||
{
|
||||
return $_SERVER['REQUEST_METHOD'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return strtok($_SERVER['REQUEST_URI'], '?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is specific method
|
||||
*
|
||||
* @param string $method
|
||||
* @return bool
|
||||
*/
|
||||
public function is(string $method): bool
|
||||
{
|
||||
return ($this->method() === strtoupper($method));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request has POST data
|
||||
*
|
||||
* @param string $param
|
||||
* @return bool
|
||||
*/
|
||||
public final function has(string $param): bool
|
||||
{
|
||||
return isset($_POST[$param]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get POST data
|
||||
*
|
||||
* @param string|null $param
|
||||
* @param mixed|null $default
|
||||
* @return mixed
|
||||
*/
|
||||
public final function get(string|null $param = null, mixed $default = null): mixed
|
||||
{
|
||||
if($param == null) {
|
||||
return $_POST;
|
||||
}
|
||||
|
||||
if($this->has($param)) {
|
||||
return $_POST[$param];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
}
|
44
src/Http/Response.php
Normal file
44
src/Http/Response.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Http;
|
||||
|
||||
use Core\View\JsonRender;
|
||||
use Core\View\Render;
|
||||
use Core\View\Render\HtmlRender;
|
||||
|
||||
class Response
|
||||
{
|
||||
/**
|
||||
* Set statuscode for response
|
||||
*
|
||||
* @param int $status
|
||||
* @return $this
|
||||
*/
|
||||
public function status(int $status): static
|
||||
{
|
||||
http_response_code($status);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML
|
||||
*
|
||||
* @param string $view
|
||||
* @return \Core\View\Render
|
||||
*/
|
||||
public function view(string $view): Render
|
||||
{
|
||||
return (new HtmlRender())->view($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render JSON
|
||||
*
|
||||
* @return \Core\View\Render
|
||||
*/
|
||||
public function json(): Render
|
||||
{
|
||||
return new JsonRender();
|
||||
}
|
||||
}
|
92
src/Router/Router.php
Normal file
92
src/Router/Router.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace Core\Router;
|
||||
|
||||
use Core\Exceptions\Exceptions;
|
||||
use Core\Http\Request;
|
||||
use Core\View\Render;
|
||||
use Exception;
|
||||
|
||||
class Router
|
||||
{
|
||||
/**
|
||||
* List of routes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $routes = [];
|
||||
|
||||
/**
|
||||
* Add GET route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
*/
|
||||
public static function get(string $route, string $controller, string $action): void
|
||||
{
|
||||
self::register($route, $controller, $action, "GET");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add POST route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @return void
|
||||
*/
|
||||
public static function post(string $route, string $controller, string $action): void
|
||||
{
|
||||
self::register($route, $controller, $action, "POST");
|
||||
}
|
||||
|
||||
/**
|
||||
* Register route
|
||||
*
|
||||
* @param string $route
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param string $method
|
||||
* @return void
|
||||
*/
|
||||
public static function register(string $route, string $controller, string $action, string $method): void
|
||||
{
|
||||
self::$routes[$method][$route] = [
|
||||
'controller' => $controller,
|
||||
'action' => $action,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch router and run application
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function dispatch(): void
|
||||
{
|
||||
// Capture all exceptions
|
||||
Exceptions::catch();
|
||||
|
||||
// Init request
|
||||
$request = new Request();
|
||||
|
||||
$url = $request->url();
|
||||
$method = $request->method();
|
||||
|
||||
if (array_key_exists($url, self::$routes[$method])) {
|
||||
$controller = self::$routes[$method][$url]['controller'];
|
||||
$action = self::$routes[$method][$url]['action'];
|
||||
|
||||
$controller = new $controller($request);
|
||||
$response = $controller->$action();
|
||||
|
||||
if ($response instanceof Render) {
|
||||
$response->render();
|
||||
}
|
||||
} else {
|
||||
Exceptions::catchOne(new Exception("No route found for: $url"));
|
||||
}
|
||||
}
|
||||
}
|
16
src/View/JsonRender.php
Normal file
16
src/View/JsonRender.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Core\View;
|
||||
|
||||
class JsonRender extends Render
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function render(): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($this->data);
|
||||
}
|
||||
}
|
58
src/View/Render.php
Normal file
58
src/View/Render.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Core\View;
|
||||
|
||||
abstract class Render
|
||||
{
|
||||
/**
|
||||
* View data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* View template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected readonly string $view;
|
||||
|
||||
/**
|
||||
* Set view template
|
||||
*
|
||||
* @param string $view
|
||||
* @return $this
|
||||
*/
|
||||
public function view(string $view): static
|
||||
{
|
||||
$this->view = $view;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to the view
|
||||
*
|
||||
* @param array|string $key
|
||||
* @param mixed|null $value
|
||||
* @return $this
|
||||
*/
|
||||
public function with(array|string $key, mixed $value = null): static
|
||||
{
|
||||
if (!is_array($key)) {
|
||||
$key = [$key => $value];
|
||||
}
|
||||
|
||||
$this->data = array_merge($this->data, $key);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML view
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public abstract function render(): void;
|
||||
}
|
28
src/View/Render/HtmlRender.php
Normal file
28
src/View/Render/HtmlRender.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Core\View\Render;
|
||||
|
||||
use Core\View\Render;
|
||||
|
||||
class HtmlRender extends Render
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function render(): void
|
||||
{
|
||||
$basePath = $_SERVER['DOCUMENT_ROOT'];
|
||||
$viewsPath = $basePath . '/../resources/views/' . str_replace('.', '/', $this->view) . '.php';
|
||||
|
||||
if (file_exists($viewsPath)) {
|
||||
extract($this->data);
|
||||
|
||||
include $viewsPath;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \Exception('View not found');
|
||||
}
|
||||
}
|
19
tailwind.config.js
Normal file
19
tailwind.config.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./resources/views/**/*.php',
|
||||
'./resources/scripts/**/*.{js,ts,vue}',
|
||||
'./resources/styles/**/*.{scss,css}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#002649',
|
||||
light: '#76afe3',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
42
vite.config.ts
Normal file
42
vite.config.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import * as path from 'path';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
root: '.',
|
||||
publicDir: 'public/dist',
|
||||
build: {
|
||||
outDir: 'public/dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
app: path.resolve('resources/scripts/main.ts'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'scripts/[name].js',
|
||||
chunkFileNames: 'scripts/[name].js',
|
||||
assetFileNames: '[ext]/[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@styles': path.resolve('resources/styles'),
|
||||
'@scripts': path.resolve('resources/scripts'),
|
||||
'@app': path.resolve('resources/scripts/app')
|
||||
},
|
||||
},
|
||||
css: {
|
||||
postcss: './postcss.config.cjs'
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/': {
|
||||
target: 'http://bit.test',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/$/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue