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