Compare commits

...

9 Commits

Author SHA1 Message Date
2b02ca4471 v0.1.11 2025-08-01 15:06:16 +03:00
b86b8ff923 v0.1.10 2025-08-01 14:29:50 +03:00
2ab819ff30 v0.1.8 2025-06-22 15:27:26 +03:00
a64ed080bb v0.1.7 2025-01-28 16:44:34 +03:00
6242304843 flash msg fix 2025-01-28 12:47:44 +03:00
2655a793f5 some fix 2025-01-24 16:47:37 +03:00
de0354f9cb add dependencies to view module shop client 2025-01-24 16:26:11 +03:00
4a4d5b083f some fix 2025-01-24 16:20:15 +03:00
68b5741f46 some fix 2025-01-24 15:14:36 +03:00
176 changed files with 13302 additions and 46 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ views_cache
resources/upload resources/upload
resources/tmp resources/tmp
composer.lock composer.lock
kernel/app_themes

View File

@@ -0,0 +1,8 @@
<?php
namespace app\themes\custom;
class CustomTheme extends \kernel\app_themes\custom\CustomTheme
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace app\themes\custom\assets;
use kernel\Assets;
class CustomThemesAssets extends Assets
{
protected function createCSS(): void
{
$this->registerCSS(slug: "main", resource: "/css/styles.css");
}
protected function createJS(): void
{
$this->registerJS(slug: "webpack", resource: "/js/scripts.js");
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace app\themes\custom\controllers;
use kernel\Controller;
class MainController extends Controller
{
protected function init(): void
{
parent::init();
$this->cgView->viewPath = APP_DIR . "/themes/custom/views/main/";
$this->cgView->layout = "main.php";
$this->cgView->layoutPath = APP_DIR . "/themes/custom/views/layout/";
$this->cgView->addVarToLayout("resources", "/resources/themes/custom");
}
public function actionIndex(): void
{
$this->cgView->render("index.php");
}
public function actionAbout(): void
{
$this->cgView->render("about.php");
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "Custom",
"version": "0.1",
"author": "ItGuild",
"slug": "custom",
"type": "theme",
"description": "Custom theme",
"preview": "preview.png",
"resource": "/resources/themes/custom",
"resource_path": "{RESOURCES}/themes/custom",
"theme_class": "app\\themes\\custom\\CustomTheme",
"theme_class_file": "{APP}/themes/custom/CustomTheme.php",
"routs": "routs/custom.php",
"dependence": "photo,tag"
}

View File

@@ -0,0 +1,12 @@
<?php
use kernel\App;
App::$collector->get('/', [\app\themes\custom\controllers\MainController::class, 'actionIndex']);
App::$collector->get('/about', [\app\themes\custom\controllers\MainController::class, 'actionAbout']);
//App::$collector->get('/page/{page_number}', [\app\modules\tag\controllers\TagController::class, 'actionIndex']);
//App::$collector->get('/create', [\app\modules\tag\controllers\TagController::class, 'actionCreate']);

View File

@@ -0,0 +1,92 @@
<?php
/**
* @var string $content
* @var string $resources
* @var string $title
* @var \kernel\CgView $view
*/
$assets = new \app\themes\custom\assets\CustomThemesAssets($resources);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<?php $assets->getCSSAsSTR(); ?>
<meta name="description" content=""/>
<meta name="author" content=""/>
<title><?= $title ?></title>
<?= $view->getMeta() ?>
<link rel="icon" type="image/x-icon" href="<?= $resources ?>/assets/favicon.ico"/>
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet"
type="text/css"/>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800"
rel="stylesheet" type="text/css"/>
<!-- Core theme CSS (includes Bootstrap)-->
</head>
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="/">Custom theme</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="/">На главную</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="/about">О нас</a></li>
</ul>
</div>
</div>
</nav>
<?= $content ?>
<!-- Footer-->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright &copy; IT Guild Micro Framework</div>
</div>
</div>
</div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<?php $assets->getJSAsStr(); ?>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<?php
/**
* @var string $resources;
* @var \kernel\CgView $view
*/
$view->setTitle("Старт Bootstrap");
$view->setMeta([
'description' => 'Дефолтная bootstrap тема'
]);
?>
<!-- Page Header-->
<header class="masthead" style="background-image: url('<?= $resources ?>/assets/img/about-bg.jpeg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="page-heading">
<h1>About Me</h1>
<span class="subheading">This is what I do.</span>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content-->
<main class="mb-4">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe nostrum ullam eveniet pariatur voluptates odit, fuga atque ea nobis sit soluta odio, adipisci quas excepturi maxime quae totam ducimus consectetur?</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius praesentium recusandae illo eaque architecto error, repellendus iusto reprehenderit, doloribus, minus sunt. Numquam at quae voluptatum in officia voluptas voluptatibus, minus!</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut consequuntur magnam, excepturi aliquid ex itaque esse est vero natus quae optio aperiam soluta voluptatibus corporis atque iste neque sit tempora!</p>
</div>
</div>
</div>
</main>

View File

@@ -0,0 +1,86 @@
<?php
/**
* @var string $resources;
* @var \kernel\CgView $view
*/
$view->setTitle("IT Guild Micro Framework");
$view->setMeta([
'description' => 'Default IT Guild Micro Framework theme'
]);
?>
<!-- Page Header-->
<header class="masthead" style="background-image: url('<?= $resources ?>/assets/img/home-bg.jpeg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="site-heading">
<h1>Clean Blog</h1>
<span class="subheading">A Blog Theme by IT Guild Micro Framework</span>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content-->
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<!-- Post preview-->
<div class="post-preview">
<a href="#!">
<h2 class="post-title">Man must explore, and this is exploration at its greatest</h2>
<h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 24, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="#!"><h2 class="post-title">I believe every human has a finite number of heartbeats. I don't intend to waste any of mine.</h2></a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 18, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="#!">
<h2 class="post-title">Science has not yet mastered prophecy</h2>
<h3 class="post-subtitle">We predict too much for the next year and yet far too little for the next ten.</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on August 24, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="#!">
<h2 class="post-title">Failure is not an option</h2>
<h3 class="post-subtitle">Many say exploration is part of our destiny, but its actually our duty to future generations.</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on July 8, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Pager-->
<div class="d-flex justify-content-end mb-4"><a class="btn btn-primary text-uppercase" href="#!">Older Posts →</a></div>
</div>
</div>
</div>

View File

@@ -7,6 +7,7 @@ $dotenv->load();
include_once __DIR__ . "/bootstrap/db.php"; include_once __DIR__ . "/bootstrap/db.php";
include_once __DIR__ . "/bootstrap/header.php"; include_once __DIR__ . "/bootstrap/header.php";
include_once __DIR__ . "/bootstrap/secure.php"; include_once __DIR__ . "/bootstrap/secure.php";
include_once __DIR__ . "/bootstrap/notification.php";
const ROOT_DIR = __DIR__; const ROOT_DIR = __DIR__;
const KERNEL_DIR = __DIR__ . "/kernel"; const KERNEL_DIR = __DIR__ . "/kernel";
const KERNEL_MODULES_DIR = __DIR__ . "/kernel/modules"; const KERNEL_MODULES_DIR = __DIR__ . "/kernel/modules";
@@ -18,7 +19,7 @@ const KERNEL_APP_MODULES_DIR = KERNEL_DIR . "/app_modules";
const APP_DIR = ROOT_DIR . "/app"; const APP_DIR = ROOT_DIR . "/app";
\kernel\Theme::$assetsCollector = new \kernel\AssetsCollector();
function getConst($text): array|false|string function getConst($text): array|false|string
{ {

View File

@@ -0,0 +1,4 @@
<?php
\kernel\App::$notificationDispatcher = new \kernel\modules\notification\NotificationDispatcher();
\kernel\App::$notificationDispatcher->addChannel('email', new \kernel\modules\notification\channels\EmailChannel());

View File

@@ -5,6 +5,7 @@ namespace kernel;
use kernel\helpers\Debug; use kernel\helpers\Debug;
use kernel\modules\notification\NotificationDispatcher;
use kernel\modules\user\models\User; use kernel\modules\user\models\User;
use kernel\services\ModuleService; use kernel\services\ModuleService;
use kernel\services\ThemeService; use kernel\services\ThemeService;
@@ -21,10 +22,14 @@ class App
static User $user; static User $user;
static NotificationDispatcher $notificationDispatcher;
static array $secure; static array $secure;
public ModuleService $moduleService; public ModuleService $moduleService;
static Hook $hook;
public ThemeService $themeService; public ThemeService $themeService;
public static Database $db; public static Database $db;
@@ -41,6 +46,7 @@ class App
public function load(): static public function load(): static
{ {
App::$hook = new Hook();
$this->moduleService = new ModuleService(); $this->moduleService = new ModuleService();
App::$collector = new CgRouteCollector(); App::$collector = new CgRouteCollector();
$this->setRouting(); $this->setRouting();
@@ -53,6 +59,7 @@ class App
include KERNEL_DIR . "/routs/admin.php"; include KERNEL_DIR . "/routs/admin.php";
include ROOT_DIR . "/rout.php"; include ROOT_DIR . "/rout.php";
$modules_routs = $this->moduleService->getModulesRouts(); $modules_routs = $this->moduleService->getModulesRouts();
$this->moduleService->setModulesHooks();
foreach ($modules_routs as $rout){ foreach ($modules_routs as $rout){
include "$rout"; include "$rout";
} }

View File

@@ -7,8 +7,12 @@ class Assets
protected array $jsHeader = []; protected array $jsHeader = [];
protected array $jsBody = []; protected array $jsBody = [];
protected array $collectorJs = [];
protected array $css = []; protected array $css = [];
protected array $collectorCss = [];
protected string $resourceURI = "/resource"; protected string $resourceURI = "/resource";
public function __construct(string $resourceURI) public function __construct(string $resourceURI)
@@ -26,7 +30,7 @@ class Assets
$this->resourceURI = $resourceURI; $this->resourceURI = $resourceURI;
} }
public function registerJS(string $slug, string $resource, bool $body = true, bool $addResourceURI = true): void public function registerJS(string $slug, string $resource, bool $body = true, bool $addResourceURI = true, string $after = null): void
{ {
$resource = $addResourceURI ? $this->resourceURI . $resource : $resource; $resource = $addResourceURI ? $this->resourceURI . $resource : $resource;
if ($body) { if ($body) {
@@ -34,12 +38,14 @@ class Assets
} else { } else {
$this->jsHeader[$slug] = $resource; $this->jsHeader[$slug] = $resource;
} }
$this->collectorJs[$slug] = ['resource' => $resource, 'after' => $after, 'body' => $body];
} }
public function registerCSS(string $slug, string $resource, bool $addResourceURI = true): void public function registerCSS(string $slug, string $resource, bool $addResourceURI = true, string $after = null): void
{ {
$resource = $addResourceURI ? $this->resourceURI . $resource : $resource; $resource = $addResourceURI ? $this->resourceURI . $resource : $resource;
$this->css[$slug] = $resource; $this->css[$slug] = $resource;
$this->collectorCss[$slug] = ['resource' => $resource, 'after' => $after];
} }
public function getJSAsStr(bool $body = true): void public function getJSAsStr(bool $body = true): void
@@ -63,4 +69,14 @@ class Assets
} }
} }
public function getCollectorCss(): array
{
return $this->collectorCss;
}
public function getCollectorJs(): array
{
return $this->collectorJs;
}
} }

128
kernel/AssetsCollector.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
namespace kernel;
use RuntimeException;
class AssetsCollector
{
protected array $assetsPool = [];
public function registerAsset(Assets $assets): void
{
$this->assetsPool[] = $assets;
}
public function renderCss(): void
{
$css = [];
foreach ($this->assetsPool as $item) {
/** @var Assets $item */
$css = array_merge($css, $item->getCollectorCss());
}
try {
$sortedStyles = $this->sortStyles($css);
// Выводим отсортированные стили
foreach ($sortedStyles as $style) {
echo '<link rel="stylesheet" href="' . htmlspecialchars($style) . '">' . "\n";
}
} catch (RuntimeException $e) {
echo 'Ошибка: ' . $e->getMessage();
}
}
public function renderJs(bool $body = true): void
{
$scripts = [];
foreach ($this->assetsPool as $item) {
/** @var Assets $item */
$scripts = array_merge($scripts, $item->getCollectorJs());
}
try {
$sortedScripts = $this->sortScripts($scripts);
// Разделяем скрипты для head и body
$headScripts = [];
$bodyScripts = [];
foreach ($sortedScripts as $script) {
if ($script['body']) {
$bodyScripts[] = $script['resource'];
} else {
$headScripts[] = $script['resource'];
}
}
// Выводим скрипты для head
if ($body){
$scriptsToRender = $bodyScripts;
}
else {
$scriptsToRender = $headScripts;
}
foreach ($scriptsToRender as $script) {
echo '<script src="' . htmlspecialchars($script) . '"></script>' . "\n";
}
}
catch (RuntimeException $e) {
echo 'Ошибка: ' . $e->getMessage();
}
}
protected function sortStyles(array $styles): array
{
$sorted = [];
$added = [];
// Пока не добавим все стили
while (count($sorted) < count($styles)) {
$found = false;
foreach ($styles as $name => $style) {
// Если стиль еще не добавлен и его зависимости выполнены
if (!isset($added[$name]) &&
(empty($style['after']) || isset($added[$style['after']]))) {
$sorted[] = $style['resource'];
$added[$name] = true;
$found = true;
}
}
if (!$found) {
// Если есть циклическая зависимость
throw new RuntimeException('Обнаружена циклическая зависимость в стилях');
}
}
return $sorted;
}
protected function sortScripts(array $scripts): array
{
$sorted = [];
$added = [];
while (count($sorted) < count($scripts)) {
$found = false;
foreach ($scripts as $name => $script) {
if (!isset($added[$name]) &&
(empty($script['after']) || isset($added[$script['after']]))) {
$sorted[] = $script;
$added[$name] = true;
$found = true;
}
}
if (!$found) {
throw new RuntimeException('Обнаружена циклическая зависимость в скриптах');
}
}
return $sorted;
}
}

View File

@@ -0,0 +1,345 @@
<?php
namespace kernel;
use Illuminate\Database\Eloquent\Collection;
class CollectionTableRenderer
{
protected \Illuminate\Database\Eloquent\Collection $collection;
protected array $columns;
protected array $tableAttributes;
protected array $customColumns = [];
protected array $valueProcessors = [];
protected array $filters = [];
protected bool $filterRowEnabled = false;
protected array $filterRowAttributes = [
'class' => 'filter-row'
];
/**
* Конструктор класса
*
* @param Collection $collection
*/
public function __construct(Collection $collection)
{
$this->collection = $collection;
$this->columns = [];
$this->customColumns = [];
$this->valueProcessors = [];
$this->filters = [];
$this->tableAttributes = [
'class' => 'table table-bordered table-striped',
'id' => 'dataTable'
];
}
/**
* Установка столбцов для отображения
*
* @param array $columns Массив столбцов в формате ['field' => 'Заголовок']
* @return $this
*/
public function setColumns(array $columns): static
{
$this->columns = $columns;
return $this;
}
/**
* Добавление кастомной колонки
*
* @param string $columnName Название колонки (ключ)
* @param string $title Заголовок колонки
* @param callable $callback Функция для генерации содержимого
* @return $this
*/
public function addCustomColumn(string $columnName, string $title, callable $callback): static
{
$this->customColumns[$columnName] = [
'title' => $title,
'callback' => $callback
];
return $this;
}
/**
* Добавление обработчика значения для колонки
*
* @param string $columnName Название колонки
* @param callable $processor Функция обработки значения
* @return $this
*/
public function addValueProcessor(string $columnName, callable $processor): static
{
$this->valueProcessors[$columnName] = $processor;
return $this;
}
/**
* Добавление фильтра для колонки
*
* @param string $columnName Название колонки
* @param string $filterHtml HTML-код фильтра
* @return $this
*/
public function addFilter(string $columnName, string $filterHtml): static
{
$this->filters[$columnName] = $filterHtml;
$this->filterRowEnabled = true;
return $this;
}
/**
* Включение/отключение строки фильтров
*
* @param bool $enabled
* @return $this
*/
public function enableFilterRow(bool $enabled = true): static
{
$this->filterRowEnabled = $enabled;
return $this;
}
/**
* Установка атрибутов строки фильтров
*
* @param array $attributes
* @return $this
*/
public function setFilterRowAttributes(array $attributes): static
{
$this->filterRowAttributes = array_merge($this->filterRowAttributes, $attributes);
return $this;
}
/**
* Установка атрибутов для таблицы
*
* @param array $attributes Массив атрибутов HTML
* @return $this
*/
public function setTableAttributes(array $attributes): static
{
$this->tableAttributes = array_merge($this->tableAttributes, $attributes);
return $this;
}
/**
* Получить HTML-код таблицы (без вывода)
*
* @return string
*/
public function getHtml(): string
{
if (empty($this->columns) && empty($this->customColumns) && $this->collection->isNotEmpty()) {
// Автоматически определяем столбцы на основе первой модели
$firstItem = $this->collection->first();
$this->columns = array_combine(
array_keys($firstItem->toArray()),
array_map('ucfirst', array_keys($firstItem->toArray()))
);
}
$html = '<table ' . $this->buildAttributes($this->tableAttributes) . '>';
$html .= $this->renderHeader();
if ($this->filterRowEnabled) {
$html .= $this->renderFilterRow();
}
$html .= $this->renderBody();
$html .= '</table>';
return $html;
}
/**
* Генерация HTML-таблицы
*
* @return string
*/
public function render(): void
{
echo $this->getHtml();
}
public function fetch(): string
{
return $this->getHtml();
}
/**
* Генерация заголовка таблицы
*
* @return string
*/
protected function renderHeader(): string
{
$html = '<thead><tr>';
// Обычные колонки
foreach ($this->columns as $title) {
$html .= '<th>' . htmlspecialchars($title) . '</th>';
}
// Кастомные колонки
foreach ($this->customColumns as $column) {
$html .= '<th>' . htmlspecialchars($column['title']) . '</th>';
}
$html .= '</tr></thead>';
return $html;
}
/**
* Генерация строки фильтров
*
* @return string
*/
protected function renderFilterRow(): string
{
$html = '<tr ' . $this->buildAttributes($this->filterRowAttributes) . '>';
// Обычные колонки
foreach (array_keys($this->columns) as $field) {
$html .= '<td>';
if (isset($this->filters[$field])) {
$html .= $this->filters[$field];
} else {
$html .= '&nbsp;';
}
$html .= '</td>';
}
// Кастомные колонки
foreach (array_keys($this->customColumns) as $columnName) {
$html .= '<td>';
if (isset($this->filters[$columnName])) {
$html .= $this->filters[$columnName];
} else {
$html .= '&nbsp;';
}
$html .= '</td>';
}
$html .= '</tr>';
return $html;
}
/**
* Генерация тела таблицы
*
* @return string
*/
protected function renderBody(): string
{
$html = '<tbody>';
foreach ($this->collection as $item) {
$html .= '<tr>';
// Обычные колонки
foreach (array_keys($this->columns) as $field) {
$value = $this->getValue($item, $field);
$value = $this->processValue($field, $value, $item);
$html .= '<td>' . $value . '</td>';
}
// Кастомные колонки
foreach ($this->customColumns as $columnName => $column) {
$value = call_user_func($column['callback'], $item, $columnName);
$html .= '<td>' . $value . '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody>';
return $html;
}
/**
* Обработка значения ячейки
*
* @param string $field Название поля
* @param mixed $value Значение
* @param mixed $item Весь элемент коллекции
* @return mixed
*/
protected function processValue(string $field, mixed $value, mixed $item): mixed
{
// Если есть обработчик для этого поля - применяем его
if (isset($this->valueProcessors[$field])) {
$value = call_user_func($this->valueProcessors[$field], $value, $item, $field);
}
// Если значение не прошло обработку - экранируем его
// if (is_string($value)) {
// return htmlspecialchars($value);
// }
return $value;
}
/**
* Получение значения из элемента коллекции
*
* @param mixed $item
* @param string $field
* @return mixed
*/
protected function getValue(mixed $item, string $field): mixed
{
// Поддержка dot-notation для отношений
if (str_contains($field, '.')) {
return data_get($item, $field);
}
// Поддержка accessor'ов модели
if (method_exists($item, 'get' . ucfirst($field) . 'Attribute')) {
return $item->{$field};
}
return $item->{$field} ?? null;
}
/**
* Сборка HTML-атрибутов
*
* @param array $attributes
* @return string
*/
protected function buildAttributes(array $attributes): string
{
$result = [];
foreach ($attributes as $key => $value) {
$result[] = $key . '="' . htmlspecialchars($value) . '"';
}
return implode(' ', $result);
}
/**
* Магический метод для преобразования в строку
*
* @return string
*/
public function __toString()
{
return $this->render();
}
}

View File

@@ -9,12 +9,13 @@ class Flash
public static function setMessage(string $type, string $msg): void public static function setMessage(string $type, string $msg): void
{ {
Session::start(); self::start();
Session::set($type, $msg); Session::set($type, $msg);
} }
public static function getMessage(string $type): string public static function getMessage(string $type): string
{ {
self::start();
$msg = Session::get($type, false); $msg = Session::get($type, false);
Session::remove($type); Session::remove($type);
@@ -23,7 +24,16 @@ class Flash
public static function hasMessage(string $type): bool public static function hasMessage(string $type): bool
{ {
self::start();
return Session::has($type); return Session::has($type);
} }
public static function start()
{
if (!Session::isStarted()){
Session::start();
}
}
} }

41
kernel/Hook.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
namespace kernel;
class Hook
{
protected array $pool = [];
public function add(string $entity, \Closure $handler): void
{
$this->pool[] = [
'entity' => $entity,
'handler' => $handler,
];
}
public function getHooksByEntity(string $entity): array
{
$hooks = [];
foreach ($this->pool as $item){
if ($item['entity'] === $entity){
$hooks[] = $item;
}
}
return $hooks;
}
public function runHooksByEntity(string $entity, array $params = []): void
{
$response = '';
$hooks = $this->getHooksByEntity($entity);
foreach ($hooks as $hook){
$response .= call_user_func_array($hook['handler'], $params ?? []);
}
echo $response;
}
}

15
kernel/Theme.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace kernel;
class Theme
{
static AssetsCollector $assetsCollector;
public function __construct()
{
}
}

View File

@@ -5,8 +5,12 @@
* @var string $title * @var string $title
* @var \kernel\CgView $view * @var \kernel\CgView $view
*/ */
use kernel\Theme;
\Josantonius\Session\Facades\Session::start(); \Josantonius\Session\Facades\Session::start();
$assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources) //$assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources);
Theme::$assetsCollector->registerAsset(new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources));
?> ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@@ -18,8 +22,8 @@ $assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources)
<link href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700,800,900" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700,800,900" rel="stylesheet">
<?php $assets->getCSSAsSTR(); ?> <?php Theme::$assetsCollector->renderCss(); ?>
<?php $assets->getJSAsStr(body: false); ?> <?php Theme::$assetsCollector->renderJs(body: false); ?>
</head> </head>
<body> <body>
@@ -92,6 +96,6 @@ $assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources)
</div> </div>
</div> </div>
<?php $assets->getJSAsStr(); ?> <?php Theme::$assetsCollector->renderJs(); ?>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,31 @@
<?php
namespace kernel\app_themes\custom;
use kernel\modules\menu\service\MenuService;
use kernel\services\MigrationService;
class CustomTheme
{
public MenuService $menuService;
public MigrationService $migrationService;
public function __construct()
{
$this->menuService = new MenuService();
$this->migrationService = new MigrationService();
}
/**
* @throws \Exception
*/
public function init(): void
{
//TODO
}
public function deactivate(): void
{
//TODO
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace kernel\app_themes\custom\services;
class CustomThemeService
{
}

View File

@@ -49,6 +49,9 @@ class AdminConsoleController extends ConsoleController
$out = $this->migrationService->runAtPath("kernel/modules/secure/migrations"); $out = $this->migrationService->runAtPath("kernel/modules/secure/migrations");
$this->out->r("create secret_code table", "green"); $this->out->r("create secret_code table", "green");
$out = $this->migrationService->runAtPath("kernel/modules/notification/migrations");
$this->out->r("create notification table", "green");
$this->optionService->createFromParams( $this->optionService->createFromParams(
key: "admin_theme_paths", key: "admin_theme_paths",
value: "{\"paths\": [\"{KERNEL_ADMIN_THEMES}\", \"{APP}/admin_themes\"]}", value: "{\"paths\": [\"{KERNEL_ADMIN_THEMES}\", \"{APP}/admin_themes\"]}",
@@ -170,6 +173,13 @@ class AdminConsoleController extends ConsoleController
]); ]);
$this->out->r("create item menu option", "green"); $this->out->r("create item menu option", "green");
$this->menuService->createItem([
"label" => "Уведомления",
"url" => "/admin/notification",
"slug" => "notification"
]);
$this->out->r("create notification option", "green");
$user = new CreateUserForm(); $user = new CreateUserForm();
$user->load([ $user->load([
'username' => 'admin', 'username' => 'admin',

View File

@@ -71,10 +71,15 @@ class MigrationController extends ConsoleController
$dmr = new DatabaseMigrationRepository(App::$db->capsule->getDatabaseManager(), 'migration'); $dmr = new DatabaseMigrationRepository(App::$db->capsule->getDatabaseManager(), 'migration');
$m = new Migrator($dmr, App::$db->capsule->getDatabaseManager(), new Filesystem()); $m = new Migrator($dmr, App::$db->capsule->getDatabaseManager(), new Filesystem());
if (\kernel\App::$db->schema->hasTable('option')) { if (isset($this->argv['path'])){
$migrationPaths = array_merge($this->moduleService->getModulesMigrationsPaths(), [ROOT_DIR . '/migrations']); $migrationPaths = [ROOT_DIR . $this->argv['path']];
} else { }
$migrationPaths = [ROOT_DIR . '/migrations']; else {
if (\kernel\App::$db->schema->hasTable('option')) {
$migrationPaths = array_merge($this->moduleService->getModulesMigrationsPaths(), [ROOT_DIR . '/migrations']);
} else {
$migrationPaths = [ROOT_DIR . '/migrations'];
}
} }
$res = $m->run($migrationPaths); $res = $m->run($migrationPaths);
@@ -94,7 +99,13 @@ class MigrationController extends ConsoleController
$m = new Migrator($dmr, App::$db->capsule->getDatabaseManager(), new Filesystem()); $m = new Migrator($dmr, App::$db->capsule->getDatabaseManager(), new Filesystem());
//$migrationPaths = array_merge(App::$migrationsPaths, [WORKSPACE_DIR . '/console/migrations']); //$migrationPaths = array_merge(App::$migrationsPaths, [WORKSPACE_DIR . '/console/migrations']);
$migrationPaths = [ROOT_DIR . '/migrations']; if (isset($this->argv['path'])){
$migrationPaths = [ROOT_DIR . $this->argv['path']];
}
else {
$migrationPaths = [ROOT_DIR . '/migrations'];
}
$res = $m->rollback($migrationPaths, ['step' => $step]); $res = $m->rollback($migrationPaths, ['step' => $step]);
print_r($step); print_r($step);
foreach ($res as $re) { foreach ($res as $re) {

View File

@@ -23,6 +23,12 @@ class Html
return "<a href='$link' $paramsStr>"; return "<a href='$link' $paramsStr>";
} }
public static function link(string $title, string $link, array $params = []): string
{
$paramsStr = self::createParams($params);
return "<a href='$link' $paramsStr>$title</a>";
}
/** /**
* @param array $data * @param array $data
* @return string * @return string

View File

@@ -24,7 +24,7 @@ class SMTP
/** /**
* @throws Exception * @throws Exception
*/ */
public function send_html(array $params) public function send_html(array $params): bool
{ {
if (!isset($params['address'])){ if (!isset($params['address'])){
return false; return false;
@@ -35,6 +35,6 @@ class SMTP
$body = $params['body'] ?? 'Нет информации'; $body = $params['body'] ?? 'Нет информации';
$this->mail->msgHTML($body); $this->mail->msgHTML($body);
$this->mail->send(); return $this->mail->send();
} }
} }

View File

@@ -13,4 +13,9 @@ class Version
3 => intval($version), 3 => intval($version),
}; };
} }
public static function compare($current, $version): bool|int
{
return version_compare($current, $version);
}
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "Kernel", "name": "Kernel",
"version": "0.1.5", "version": "0.1.11",
"author": "ITGuild", "author": "ITGuild",
"slug": "kernel", "slug": "kernel",
"type": "kernel", "type": "kernel",

View File

@@ -92,7 +92,7 @@ class ModuleShopClientController extends AdminController
Files::uploadByUrl($_ENV['MODULE_SHOP_URL'] . $module_info['path_to_archive'], RESOURCES_DIR . "/tmp/modules"); Files::uploadByUrl($_ENV['MODULE_SHOP_URL'] . $module_info['path_to_archive'], RESOURCES_DIR . "/tmp/modules");
$this->moduleService->installModule('/resources/tmp/modules/' . basename($module_info['path_to_archive'])); $this->moduleService->installModule('/resources/tmp/modules/' . basename($module_info['path_to_archive']));
Flash::setMessage("success", "Модуль успешно установлен."); Flash::setMessage("success", "Модуль успешно загружен.");
$this->redirect('/admin/module_shop_client', 302); $this->redirect('/admin/module_shop_client', 302);
} }
@@ -254,10 +254,9 @@ class ModuleShopClientController extends AdminController
$adminThemeInfo = json_decode($adminThemeInfo->getBody()->getContents(), true); $adminThemeInfo = json_decode($adminThemeInfo->getBody()->getContents(), true);
Files::uploadByUrl($_ENV['MODULE_SHOP_URL'] . $adminThemeInfo['path_to_archive'], RESOURCES_DIR . "/tmp/admin_themes"); Files::uploadByUrl($_ENV['MODULE_SHOP_URL'] . $adminThemeInfo['path_to_archive'], RESOURCES_DIR . "/tmp/admin_themes");
if ($this->adminThemeService->install('/resources/tmp/admin_themes/' . basename($adminThemeInfo['path_to_archive']))) { if ($this->adminThemeService->install('/resources/tmp/admin_themes/' . basename($adminThemeInfo['path_to_archive']))) {
Flash::setMessage("success", "Тема админ-панели успешно установлена."); Flash::setMessage("success", "Тема админ-панели успешно загружена.");
} else { } else {
Session::start(); Flash::setMessage("error", implode(";", $this->adminThemeService->getErrors()));
Session::set("error", implode(";", $this->adminThemeService->getErrors()));
} }
$this->redirect('/admin/module_shop_client', 302); $this->redirect('/admin/module_shop_client', 302);
@@ -306,7 +305,7 @@ class ModuleShopClientController extends AdminController
$themeInfo = json_decode($themeInfo->getBody()->getContents(), true); $themeInfo = json_decode($themeInfo->getBody()->getContents(), true);
Files::uploadByUrl($_ENV['MODULE_SHOP_URL'] . $themeInfo['path_to_archive'], RESOURCES_DIR . "/tmp/themes"); Files::uploadByUrl($_ENV['MODULE_SHOP_URL'] . $themeInfo['path_to_archive'], RESOURCES_DIR . "/tmp/themes");
if ($this->themeService->install('/resources/tmp/themes/' . basename($themeInfo['path_to_archive']))) { if ($this->themeService->install('/resources/tmp/themes/' . basename($themeInfo['path_to_archive']))) {
Flash::setMessage("success", "Тема сайта успешно установлена."); Flash::setMessage("success", "Тема сайта успешно загружена.");
} else { } else {
Session::start(); Session::start();
Session::set("error", implode(";", $this->themeService->getErrors())); Session::set("error", implode(";", $this->themeService->getErrors()));

View File

@@ -15,7 +15,8 @@ $table_info = [
"version" => "Версия", "version" => "Версия",
"description" => "Описание", "description" => "Описание",
"installations" => "Установки", "installations" => "Установки",
"views" => "Просмотры" "views" => "Просмотры",
"dependence" => "Зависимости"
], ],
"params" => ["class" => "table table-bordered"], "params" => ["class" => "table table-bordered"],
"baseUrl" => "/admin/module_shop_client", "baseUrl" => "/admin/module_shop_client",

View File

@@ -0,0 +1,31 @@
<?php
namespace kernel\modules\notification;
use kernel\Flash;
use kernel\modules\notification\contracts\NotificationChannelInterface;
use kernel\modules\notification\contracts\NotificationMessage;
use kernel\modules\user\models\User;
class NotificationDispatcher
{
protected array $channels = [];
public function addChannel(string $channelName, NotificationChannelInterface $channel): void
{
$this->channels[$channelName] = $channel;
}
public function dispatch(NotificationMessage $notification, User $user): void
{
foreach ($notification->via() as $channelName) {
if (isset($this->channels[$channelName])) {
try {
$this->channels[$channelName]->send($notification, $user);
} catch (\Exception $e) {
Flash::setMessage("error", $e->getMessage());
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace kernel\modules\notification;
use kernel\Module;
use kernel\modules\menu\service\MenuService;
use kernel\services\MigrationService;
class NotificationModule extends Module
{
public MenuService $menuService;
public MigrationService $migrationService;
public function __construct()
{
$this->menuService = new MenuService();
$this->migrationService = new MigrationService();
}
/**
* @throws \Exception
*/
public function init(): void
{
$this->migrationService->runAtPath("kernel/modules/notification/migrations");
$this->menuService->createItem([
"label" => "Уведомления",
"url" => "/admin/notification",
"slug" => "notification"
]);
}
/**
* @throws \Exception
*/
public function deactivate(): void
{
$this->migrationService->rollbackAtPath("kernel/modules/notification/migrations");
$this->menuService->removeItemBySlug("notification");
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace kernel\modules\notification\channels;
use kernel\Flash;
use kernel\modules\notification\contracts\NotificationChannelInterface;
use kernel\modules\notification\contracts\NotificationMessage;
use kernel\modules\notification\models\User;
class DatabaseChannel implements NotificationChannelInterface
{
public function send(NotificationMessage $notification, User $user): bool
{
try {
$user->notifications()->create([
'message' => $notification->getMessage(),
'data' => $notification->toArray()
]);
return true;
} catch (\Exception $e) {
Flash::setMessage("error", $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace kernel\modules\notification\channels;
use kernel\helpers\SMTP;
use kernel\modules\notification\contracts\NotificationChannelInterface;
use kernel\modules\notification\contracts\NotificationMessage;
use kernel\modules\user\models\User;
use PHPMailer\PHPMailer\Exception;
class EmailChannel implements NotificationChannelInterface
{
/**
* @throws Exception
*/
public function send(NotificationMessage $notification, User $user): bool
{
$smtp = new SMTP();
// Здесь можно использовать Laravel Mail
// \Illuminate\Support\Facades\Mail::to($user->email)
// ->send(new \App\Mail\NotificationMail(
// $notification->getSubject(),
// $notification->getMessage()
// ));
return $smtp->send_html([
'address' => $user->email,
'subject' => $notification->getSubject(),
'body' => $notification->getMessage(),
'from_name' => $_ENV['MAIL_SMTP_USERNAME'],
]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace kernel\modules\notification\channels;
use kernel\modules\notification\contracts\NotificationChannelInterface;
use kernel\modules\notification\contracts\NotificationMessage;
use kernel\modules\user\models\User;
class SmsChannel implements NotificationChannelInterface
{
public function send(NotificationMessage $notification, User $user): bool
{
if (empty($user->phone)) {
return false;
}
// Интеграция с SMS-сервисом
//$smsService = new \App\Services\SmsService();
//return $smsService->send($user->phone, $notification->getMessage());
return true;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace kernel\modules\notification\channels;
use kernel\Flash;
use kernel\modules\notification\contracts\NotificationChannelInterface;
use kernel\modules\notification\contracts\NotificationMessage;
use kernel\modules\notification\models\User;
class TelegramChannel implements NotificationChannelInterface
{
protected string $botToken;
public function __construct(string $botToken)
{
$this->botToken = $botToken;
}
public function send(NotificationMessage $notification, User $user): bool
{
if (empty($user->telegram_chat_id)) {
return false;
}
$httpClient = new \GuzzleHttp\Client();
try {
$response = $httpClient->post("https://api.telegram.org/bot{$this->botToken}/sendMessage", [
'form_params' => [
'chat_id' => $user->telegram_chat_id,
'text' => $notification->getMessage(),
'parse_mode' => 'HTML'
]
]);
return $response->getStatusCode() === 200;
} catch (\Exception $e) {
Flash::setMessage("error", $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace kernel\modules\notification\contracts;
use kernel\modules\notification\models\User;
interface NotificationChannelInterface
{
public function send(NotificationMessage $notification, User $user): bool;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace kernel\modules\notification\contracts;
abstract class NotificationMessage
{
protected array $channels = [];
abstract public function getMessage(): string;
abstract public function getSubject(): string;
public function via(): array
{
return $this->channels;
}
public function addChannel(string $channel): void
{
if (!in_array($channel, $this->channels)) {
$this->channels[] = $channel;
}
}
public function toArray(): array
{
return [
'message' => $this->getMessage(),
'subject' => $this->getSubject(),
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace kernel\modules\notification\controllers;
use JetBrains\PhpStorm\NoReturn;
use kernel\AdminController;
use kernel\Flash;
use kernel\helpers\Debug;
use kernel\modules\notification\models\forms\CreateNotificationForm;
use kernel\modules\notification\models\Notification;
use kernel\modules\notification\service\NotificationService;
use kernel\modules\option\models\forms\CreateOptionForm;
class NotificationController extends AdminController
{
private NotificationService $optionService;
public function init(): void
{
parent::init();
$this->cgView->viewPath = KERNEL_MODULES_DIR . '/notification/views/';
$this->optionService = new NotificationService();
}
public function actionCreate(): void
{
$this->cgView->render('form.php');
}
#[NoReturn] public function actionAdd(): void
{
$optionForm = new CreateNotificationForm();
$optionForm->load($_REQUEST);
if ($optionForm->validate()) {
$option = $this->optionService->create($optionForm);
if ($option) {
Flash::setMessage("success", "notification успешно создана.");
$this->redirect('/admin/notification');
}
}
Flash::setMessage("error", $optionForm->getErrorsStr());
$this->redirect('/admin/notification/create');
}
public function actionIndex($page_number = 1): void
{
$this->cgView->render('index.php', ['page_number' => $page_number]);
}
/**
* @throws \Exception
*/
public function actionView(int $id): void
{
$option = Notification::find($id);
if (!$option) {
throw new \Exception('notification not found');
}
$this->cgView->render("view.php", ['option' => $option]);
}
/**
* @throws \Exception
*/
public function actionUpdate(int $id): void
{
$model = Notification::find($id);
if (!$model) {
throw new \Exception('notification not found');
}
$this->cgView->render("form.php", ['model' => $model]);
}
/**
* @throws \Exception
*/
public function actionEdit(int $id): void
{
$option = Notification::find($id);
if (!$option) {
throw new \Exception('Option not found');
}
$optionForm = new CreateOptionForm();
$optionForm->load($_REQUEST);
if ($optionForm->validate()) {
$option = $this->optionService->update($optionForm, $option);
if ($option) {
$this->redirect('/admin/notification/view/' . $option->id);
}
}
$this->redirect('/admin/notification/update/' . $id);
}
#[NoReturn] public function actionDelete(int $id): void
{
Notification::find($id)->delete();
Flash::setMessage("success", "notification успешно удалена.");
$this->redirect('/admin/notification');
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "Notifications",
"version": "0.1",
"author": "ITGuild",
"slug": "notification",
"type": "entity",
"description": "Notifications module",
"module_class": "kernel\\modules\\notification\\NotificationModule",
"module_class_file": "{KERNEL_MODULES}/notification/NotificationModule.php",
"routs": "routs/notification.php",
"migration_path": "migrations",
"dependence": "user,menu"
}

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
\kernel\App::$db->schema->create('notification', function (Blueprint $table) {
$table->id();
// Связь с пользователем
$table->unsignedBigInteger('user_id');
// Основные данные уведомления
$table->string('type')->index(); // Класс уведомления (например, App\Notifications\OrderCreated)
$table->text('message'); // Текст уведомления
$table->string('subject')->nullable(); // Тема (для email)
$table->json('data')->nullable(); // Дополнительные данные в JSON
// Статус уведомления
$table->boolean('is_read')->default(false);
$table->integer('status')->default(1);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
\kernel\App::$db->schema->dropIfExists('notification');
}
};

View File

@@ -0,0 +1,71 @@
<?php
namespace kernel\modules\notification\models;
use Illuminate\Database\Eloquent\Model;
use kernel\modules\user\models\User;
/**
* @property int $id
* @property int $user_id
* @property bool $is_read
* @property array|string $data
* @property string $type
* @property string $message
* @property string $subject
* @property int $status
*/
class Notification extends Model
{
const DISABLE_STATUS = 0;
const TO_SEND_STATUS = 1;
const SENT_STATUS = 2;
protected $table = 'notification';
protected $fillable = ['user_id', 'message', 'is_read', 'data', 'type', 'subject', 'status'];
protected $casts = [
'is_read' => 'boolean',
'data' => 'array'
];
public static function labels(): array
{
return [
'user_id' => 'Пользователь',
'message' => 'Сообщение',
'subject' => 'Тема',
'is_read' => 'Прочитано',
'data' => 'Данные',
'type' => 'Тип',
'status' => 'Статус'
];
}
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
public function markAsRead(): static
{
$this->update(['is_read' => true]);
return $this;
}
/**
* @return string[]
*/
public static function getStatus(): array
{
return [
self::DISABLE_STATUS => "Не активный",
self::TO_SEND_STATUS => "На отправку",
self::SENT_STATUS => "Отправлено",
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace kernel\modules\notification\models;
class User extends \kernel\modules\user\models\User
{
public function notifications(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Notification::class);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace kernel\modules\notification\models\forms;
use kernel\FormModel;
/**
* @property string $key
* @property string $value
* @property string $label
* @property integer $status
*/
class CreateNotificationForm extends FormModel
{
public function rules(): array
{
return [
'user_id' => 'required|integer',
'message' => 'required',
'is_read' => '',
'data' => '',
'type' => 'required',
'subject' => '',
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
use kernel\App;
use Phroute\Phroute\RouteCollector;
App::$collector->group(["prefix" => "admin"], function (RouteCollector $router) {
App::$collector->group(["before" => "auth"], function (RouteCollector $router) {
App::$collector->group(["prefix" => "notification"], callback: function (RouteCollector $router) {
App::$collector->get('/', [\kernel\modules\notification\controllers\NotificationController::class, 'actionIndex']);
App::$collector->get('/page/{page_number}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionIndex']);
App::$collector->get('/create', [\kernel\modules\notification\controllers\NotificationController::class, 'actionCreate']);
App::$collector->post("/", [\kernel\modules\notification\controllers\NotificationController::class, 'actionAdd']);
App::$collector->get('/view/{id}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionView']);
App::$collector->any('/update/{id}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionUpdate']);
App::$collector->any("/edit/{id}", [\kernel\modules\notification\controllers\NotificationController::class, 'actionEdit']);
App::$collector->get('/delete/{id}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionDelete']);
});
});
});

View File

@@ -0,0 +1,57 @@
<?php
namespace kernel\modules\notification\service;
use kernel\FormModel;
use kernel\modules\notification\models\Notification;
class NotificationService
{
public function create(FormModel $form_model): false|Notification
{
$model = new Notification();
$model->user_id = $form_model->getItem('user_id');
$model->message = $form_model->getItem('message');
$model->is_read = $form_model->getItem('is_read');
$model->data = $form_model->getItem('data');
$model->type = $form_model->getItem('type');
$model->subject = $form_model->getItem('subject');
$model->status = $form_model->getItem('status');
if ($model->save()) {
return $model;
}
return false;
}
public function update(FormModel $form_model, Notification $notification): false|Notification
{
$notification->user_id = $form_model->getItem('user_id');
$notification->message = $form_model->getItem('message');
$notification->is_read = $form_model->getItem('is_read');
$notification->data = $form_model->getItem('data');
$notification->type = $form_model->getItem('type');
$notification->subject = $form_model->getItem('subject');
$notification->status = $form_model->getItem('status');
if ($notification->save()) {
return $notification;
}
return false;
}
// public function createOptionArr(): array
// {
// foreach (Option::all()->toArray() as $option) {
// $optionArr[$option['id']] = $option['key'];
// }
// if (!empty($optionArr)) {
// return $optionArr;
// }
// return [];
// }
}

View File

@@ -0,0 +1,16 @@
<?php
namespace kernel\modules\option\table\columns;
use Itguild\Tables\ActionColumn\ActionColumn;
class OptionDeleteActionColumn extends ActionColumn
{
protected string $prefix = "/delete/";
public function fetch(): string
{
$link = $this->baseUrl . $this->prefix . $this->id;
return " <a href='$link' class='btn btn-danger'>Удалить</a> ";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace kernel\modules\option\table\columns;
use Itguild\Tables\ActionColumn\ActionColumn;
class OptionEditActionColumn extends ActionColumn
{
protected string $prefix = "/update/";
public function fetch(): string
{
$link = $this->baseUrl . $this->prefix . $this->id;
return " <a href='$link' class='btn btn-success'>Редактировать</a> ";
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace kernel\modules\option\table\columns;
use Itguild\Tables\ActionColumn\ActionColumn;
class OptionViewActionColumn extends ActionColumn
{
protected string $prefix = "/";
public function fetch()
{
$link = $this->baseUrl . $this->prefix . $this->id;
return " <a href='$link' class='btn btn-primary'>Просмотр</a> ";
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* @var Notification $model
*/
use itguild\forms\ActiveForm;
use kernel\modules\notification\models\Notification;
$form = new ActiveForm();
$form->beginForm(isset($model) ? "/admin/notification/edit/" . $model->id : "/admin/notification");
$form->field(class: \itguild\forms\inputs\Select::class, name: "user_id", params: [
'class' => "form-control",
'value' => $model->user_id ?? ''
])
->setLabel(Notification::labels()['user_id'])
->setOptions(\kernel\modules\user\service\UserService::createUsernameArr())
->render();
$form->field(\itguild\forms\inputs\TextInput::class, 'subject', [
'class' => "form-control",
'placeholder' => Notification::labels()['subject'],
'value' => $model->subject ?? ''
])
->setLabel(Notification::labels()['subject'])
->render();
$form->field(\itguild\forms\inputs\TextInput::class, 'type', [
'class' => "form-control",
'placeholder' => Notification::labels()['type'],
'value' => $model->type ?? ''
])
->setLabel(Notification::labels()['type'])
->render();
$form->field(\itguild\forms\inputs\TextArea::class, 'message', [
'class' => "form-control",
'placeholder' => Notification::labels()['message'],
'value' => $model->message ?? ''
])
->setLabel(Notification::labels()['message'])
->render();
$form->field(\itguild\forms\inputs\Checkbox::class, 'is_read', [
'class' => "form-check-input",
'placeholder' => Notification::labels()['is_read'],
'value' => $model->is_read ?? ''
])
->setLabel(Notification::labels()['is_read'])
->render();
$form->field(\itguild\forms\inputs\Select::class, 'status', [
'class' => "form-control",
'value' => $model->status ?? ''
])
->setLabel("Статус")
->setOptions(Notification::getStatus())
->render();
?>
<div class="row">
<div class="col-sm-2">
<?php
$form->field(\itguild\forms\inputs\Button::class, name: "btn-submit", params: [
'class' => "btn btn-primary ",
'value' => 'Отправить',
'typeInput' => 'submit'
])
->render();
?>
</div>
<div class="col-sm-2">
<?php
$form->field(\itguild\forms\inputs\Button::class, name: "btn-reset", params: [
'class' => "btn btn-warning",
'value' => 'Сбросить',
'typeInput' => 'reset'
])
->render();
?>
</div>
</div>
<?php
$form->endForm();

View File

@@ -0,0 +1,44 @@
<?php
/**
* @var \Illuminate\Database\Eloquent\Collection $options
* @var int $page_number
*/
use Itguild\EloquentTable\EloquentDataProvider;
use Itguild\EloquentTable\ListEloquentTable;
use kernel\modules\notification\models\Notification;
use kernel\widgets\IconBtn\IconBtnCreateWidget;
use kernel\widgets\IconBtn\IconBtnDeleteWidget;
use kernel\widgets\IconBtn\IconBtnEditWidget;
use kernel\widgets\IconBtn\IconBtnViewWidget;
$table = new ListEloquentTable(new EloquentDataProvider(Notification::class, [
'current_page' => $page_number,
'per_page' => 5,
'params' => ["class" => "table table-bordered", "border" => "2"],
'baseUrl' => "/admin/notification",
]));
$table->beforePrint(function () {
return IconBtnCreateWidget::create(['url' => '/admin/notification/create'])->run();
});
$table->columns([
"status" => [
"value" => function ($cell) {
return Notification::getStatus()[$cell];
}]
]);
$table->addAction(function($row) {
return IconBtnViewWidget::create(['url' => '/admin/notification/view/' . $row['id']])->run();
});
$table->addAction(function($row) {
return IconBtnEditWidget::create(['url' => '/admin/Notification/update/' . $row['id']])->run();
});
$table->addAction(function($row) {
return IconBtnDeleteWidget::create(['url' => '/admin/Notification/delete/' . $row['id']])->run();
});
$table->create();
$table->render();

View File

@@ -0,0 +1,32 @@
<?php
/**
* @var \Illuminate\Database\Eloquent\Collection $option
*/
use Itguild\EloquentTable\ViewEloquentTable;
use Itguild\EloquentTable\ViewJsonTableEloquentModel;
use kernel\IGTabel\btn\DangerBtn;
use kernel\IGTabel\btn\PrimaryBtn;
use kernel\IGTabel\btn\SuccessBtn;
use kernel\widgets\IconBtn\IconBtnDeleteWidget;
use kernel\widgets\IconBtn\IconBtnEditWidget;
use kernel\widgets\IconBtn\IconBtnListWidget;
$table = new ViewEloquentTable(new ViewJsonTableEloquentModel($option, [
'params' => ["class" => "table table-bordered", "border" => "2"],
'baseUrl' => "/admin/user",
]));
$table->beforePrint(function () use ($option) {
$btn = IconBtnListWidget::create(['url' => '/admin/option'])->run();
$btn .= IconBtnEditWidget::create(['url' => '/admin/option/update/' . $option->id])->run();
$btn .= IconBtnDeleteWidget::create(['url' => '/admin/option/delete/' . $option->id])->run();
return $btn;
});
$table->rows([
'status' => (function ($data) {
return \kernel\modules\option\models\Notification::getStatus()[$data];
})
]);
$table->create();
$table->render();

View File

@@ -3,6 +3,7 @@
namespace kernel\modules\option\service; namespace kernel\modules\option\service;
use kernel\FormModel; use kernel\FormModel;
use kernel\helpers\Debug;
use kernel\modules\option\models\Option; use kernel\modules\option\models\Option;
class OptionService class OptionService
@@ -49,6 +50,26 @@ class OptionService
return false; return false;
} }
public static function createOrUpdate(string $key, string $value, string $label = ''): false|Option
{
/** @var Option $option */
$option = self::getItemObject($key);
if (!$option) {
$option = new Option();
$option->key = $key;
}
$option->value = $value;
if (!empty($label)){
$option->label = $label;
}
if ($option->save()) {
return $option;
}
return false;
}
/** /**
* @param $key * @param $key
* @return false|array|string * @return false|array|string
@@ -63,6 +84,20 @@ class OptionService
return false; return false;
} }
/**
* @param $key
* @return false|array|string|Option
*/
public static function getItemObject($key): false|array|string|Option
{
$item = Option::where("key", $key)->first();
if ($item){
return $item;
}
return false;
}
public static function removeOptionByKey(string $key): bool public static function removeOptionByKey(string $key): bool
{ {
$option = Option::where("key", $key)->first(); $option = Option::where("key", $key)->first();

View File

@@ -33,4 +33,9 @@ class PostService
return false; return false;
} }
public static function getListArr()
{
return Post::pluck('title', 'id')->toArray();
}
} }

View File

@@ -8,6 +8,7 @@ use kernel\App;
use kernel\Flash; use kernel\Flash;
use kernel\helpers\Debug; use kernel\helpers\Debug;
use kernel\Mailing; use kernel\Mailing;
use kernel\modules\secure\models\forms\ChangePasswordForm;
use kernel\modules\secure\models\forms\LoginEmailForm; use kernel\modules\secure\models\forms\LoginEmailForm;
use kernel\modules\secure\models\forms\LoginForm; use kernel\modules\secure\models\forms\LoginForm;
use kernel\modules\secure\models\forms\RegisterForm; use kernel\modules\secure\models\forms\RegisterForm;
@@ -40,7 +41,7 @@ class SecureController extends AdminController
// $this->cgView->render('login.php'); // $this->cgView->render('login.php');
} }
#[NoReturn] public function actionAuth(): void #[NoReturn] public function actionAuth($basePath = '/admin'): void
{ {
$loginForm = new LoginForm(); $loginForm = new LoginForm();
$loginForm->load($_REQUEST); $loginForm->load($_REQUEST);
@@ -51,19 +52,36 @@ class SecureController extends AdminController
else { else {
$field = "username"; $field = "username";
} }
$user = $this->userService->getByField($field, $loginForm->getItem("username")); $user = $this->userService->getByField($field, $loginForm->getItem("username"));
if (!$user){ if (!$user){
Flash::setMessage("error", "User not found."); Flash::setMessage("error", "User not found.");
$this->redirect("/admin/login", code: 302); $this->redirect($basePath . "/login", code: 302);
} }
if (password_verify($loginForm->getItem("password"), $user->password_hash)) { if (password_verify($loginForm->getItem("password"), $user->password_hash)) {
setcookie('user_id', $user->id, time()+60*60*24, '/', $_SERVER['SERVER_NAME'], false); setcookie('user_id', $user->id, time()+60*60*24, '/', $_SERVER['SERVER_NAME'], false);
$this->redirect("/admin", code: 302); $this->redirect($basePath . '/', code: 302);
} else { } else {
Flash::setMessage("error", "Username or password incorrect."); Flash::setMessage("error", "Username or password incorrect.");
$this->redirect("/admin/login", code: 302); $this->redirect($basePath . "/login", code: 302);
}
}
#[NoReturn] public function actionChangePassword($basePath = '/admin'): void
{
$changePasswordForm = new ChangePasswordForm();
$changePasswordForm->load($_REQUEST);
$user = UserService::getAuthUser();
if (password_verify($changePasswordForm->getItem("old_password"), $user->password_hash)) {
$user->password_hash = password_hash($changePasswordForm->getItem("new_password"), PASSWORD_DEFAULT);
$user->save();
Flash::setMessage("success", "Пароль успешно изменен.");
$this->redirect($basePath . '', code: 302);
} else {
Flash::setMessage("error", "Username or password incorrect.");
$this->redirect($basePath . "", code: 302);
} }
} }
@@ -148,25 +166,25 @@ class SecureController extends AdminController
$this->cgView->render('register.php'); $this->cgView->render('register.php');
} }
public function actionRegistration(): void public function actionRegistration($basePath = '/admin'): void
{ {
$regForm = new RegisterForm(); $regForm = new RegisterForm();
$regForm->load($_REQUEST); $regForm->load($_REQUEST);
if ($this->userService->getByField('username', $regForm->getItem("username"))) { if ($this->userService->getByField('username', $regForm->getItem("username"))) {
Flash::setMessage("error", "Username already exists."); Flash::setMessage("error", "Username already exists.");
$this->redirect("/admin/register", code: 302); $this->redirect($basePath . "/register", code: 302);
} }
if ($this->userService->getByField('email', $regForm->getItem("email"))) { if ($this->userService->getByField('email', $regForm->getItem("email"))) {
Flash::setMessage("error", "Email already exists."); Flash::setMessage("error", "Email already exists.");
$this->redirect("/admin/register", code: 302); $this->redirect($basePath . "/register", code: 302);
} }
$user = $this->userService->create($regForm); $user = $this->userService->create($regForm);
if ($user){ if ($user){
setcookie('user_id', $user->id, time()+60*60*24, '/', $_SERVER['SERVER_NAME'], false); setcookie('user_id', $user->id, time()+60*60*24, '/', $_SERVER['SERVER_NAME'], false);
$this->redirect("/admin", code: 302); $this->redirect($basePath . "/", code: 302);
} }
} }

View File

@@ -0,0 +1,18 @@
<?php
namespace kernel\modules\secure\models\forms;
use kernel\FormModel;
class ChangePasswordForm extends FormModel
{
public function rules(): array
{
return [
'old_password' => 'required|min-str-len:6|max-str-len:50',
'new_password' => 'required|min-str-len:6|max-str-len:50',
];
}
}

View File

@@ -7,6 +7,7 @@ use JetBrains\PhpStorm\NoReturn;
use kernel\AdminController; use kernel\AdminController;
use kernel\EntityRelation; use kernel\EntityRelation;
use kernel\FileUpload; use kernel\FileUpload;
use kernel\Flash;
use kernel\helpers\Debug; use kernel\helpers\Debug;
use kernel\modules\user\models\forms\CreateUserForm; use kernel\modules\user\models\forms\CreateUserForm;
use kernel\modules\user\models\User; use kernel\modules\user\models\User;
@@ -55,6 +56,7 @@ class UserController extends AdminController
$this->redirect("/admin/user/view/" . $user->id); $this->redirect("/admin/user/view/" . $user->id);
} }
} }
Flash::setMessage("error", $userForm->getErrorsStr());
$this->redirect("/admin/user/create"); $this->redirect("/admin/user/create");
} }

View File

@@ -2,6 +2,7 @@
namespace kernel\modules\user\service; namespace kernel\modules\user\service;
use itguild\forms\ActiveForm;
use kernel\FormModel; use kernel\FormModel;
use kernel\helpers\Debug; use kernel\helpers\Debug;
use kernel\modules\user\models\User; use kernel\modules\user\models\User;
@@ -122,4 +123,12 @@ class UserService
$user->save(); $user->save();
} }
public static function getList(): array
{
return User::select('id', 'username')->get()
->pluck('username', 'id')
->toArray();
}
} }

View File

@@ -47,6 +47,8 @@ if (!isset($model)) {
$model = new User(); $model = new User();
} }
$entityRelations->renderEntityAdditionalPropertyFormBySlug("user", $model); $entityRelations->renderEntityAdditionalPropertyFormBySlug("user", $model);
?> ?>
<div class="row"> <div class="row">
<div class="col-sm-2"> <div class="col-sm-2">

View File

@@ -1,7 +1,7 @@
<?php <?php
/** /**
* @var \Illuminate\Database\Eloquent\Collection $user * @var User $user
*/ */
use kernel\modules\user\models\User; use kernel\modules\user\models\User;
@@ -55,3 +55,5 @@ $table->rows([
]); ]);
$table->create(); $table->create();
$table->render(); $table->render();
\kernel\App::$hook->runHooksByEntity('user_view', ['user' => $user]);

View File

@@ -105,6 +105,11 @@ class AdminThemeService
if ($adminThemePaths) { if ($adminThemePaths) {
$path = json_decode($adminThemePaths->value); $path = json_decode($adminThemePaths->value);
foreach ($path->paths as $p) { foreach ($path->paths as $p) {
if (!is_dir(getConst($p))){
$old_mask = umask(0);
mkdir(getConst($p), permissions: 0775, recursive: true);
umask($old_mask);
}
$dirs[] = getConst($p); $dirs[] = getConst($p);
} }
} }

View File

@@ -44,12 +44,11 @@ class KernelService
$kernel_info = $this->getKernelInfo(); $kernel_info = $this->getKernelInfo();
$kernelVersion = Version::getIntVersionByString($kernel_info['version']);
foreach ($modules_info as $mod) { foreach ($modules_info as $mod) {
$modVersion = Version::getIntVersionByString($mod['version']); if ($mod['slug'] === $kernel_info['slug'] ) {
if ($mod['slug'] === $kernel_info['slug'] && $modVersion <= $kernelVersion) { if (Version::compare($kernel_info['version'], $mod['version']) >= 0){
return true; return true;
}
} }
} }
} }

View File

@@ -185,6 +185,11 @@ class ModuleService
if ($module_paths) { if ($module_paths) {
$path = json_decode($module_paths->value); $path = json_decode($module_paths->value);
foreach ($path->paths as $p) { foreach ($path->paths as $p) {
if (!is_dir(getConst($p))) {
$old_mask = umask(0);
mkdir(getConst($p), permissions: 0775, recursive: true);
umask($old_mask);
}
$dirs[] = getConst($p); $dirs[] = getConst($p);
} }
} }
@@ -273,6 +278,17 @@ class ModuleService
return $routs; return $routs;
} }
public function setModulesHooks(): void
{
$hooks = [];
$modules = $this->getActiveModules();
foreach ($modules as $module) {
if (isset($module['hooks'])) {
include $module['path'] . "/" . $module['hooks'];
}
}
}
/** /**
* @return array * @return array
*/ */
@@ -476,11 +492,13 @@ class ModuleService
$mod_info = $this->getModuleInfoBySlug($slug); $mod_info = $this->getModuleInfoBySlug($slug);
$currentVersion = Version::getIntVersionByString($mod_info['version']); $currentVersion = $mod_info['version'];
foreach ($modules_info as $mod) { foreach ($modules_info as $mod) {
$modVersion = Version::getIntVersionByString($mod['version']); $modVersion = $mod['version'];
if ($mod['slug'] === $mod_info['slug'] && $modVersion <= $currentVersion) { if ($mod['slug'] === $mod_info['slug']) {
return true; if (Version::compare($currentVersion, $modVersion) >= 0) {
return true;
}
} }
} }
} }

View File

@@ -162,6 +162,11 @@ class ThemeService
if ($ThemePaths) { if ($ThemePaths) {
$path = json_decode($ThemePaths->value); $path = json_decode($ThemePaths->value);
foreach ($path->paths as $p) { foreach ($path->paths as $p) {
if (!is_dir(getConst($p))){
$old_mask = umask(0);
mkdir(getConst($p), permissions: 0775, recursive: true);
umask($old_mask);
}
$dirs[] = getConst($p); $dirs[] = getConst($p);
} }
} }
@@ -250,9 +255,9 @@ class ThemeService
} }
if (isset($manifest['theme_kernel_path'])) { if (isset($manifest['theme_kernel_path'])) {
$fileHelper->copy_folder($tmpThemeDirFull . "meta/kernel", $manifest['theme_app_path']); $fileHelper->copy_folder($tmpThemeDirFull . "meta/kernel", $manifest['theme_kernel_path']);
} else { } else {
$fileHelper->copy_folder($tmpThemeDirFull . "meta/kernel", APP_DIR . '/themes/' . $manifest['slug']); $fileHelper->copy_folder($tmpThemeDirFull . "meta/kernel", KERNEL_DIR . '/app_themes/' . $manifest['slug']);
} }
if (isset($manifest['resource_path'])) { if (isset($manifest['resource_path'])) {

View File

@@ -59,7 +59,7 @@ $table->columns([
'value' => $get['title'] ?? '' 'value' => $get['title'] ?? ''
] ]
], ],
] ]);
$table->beforePrint(function () { $table->beforePrint(function () {
return IconBtnCreateWidget::create(['url' => '/admin/{slug}/create'])->run(); return IconBtnCreateWidget::create(['url' => '/admin/{slug}/create'])->run();

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Clean Blog - Start Bootstrap Theme</title>
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css" />
<!-- Core theme CSS (includes Bootstrap)-->
<link href="css/styles.css" rel="stylesheet" />
</head>
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="index.html">Start Bootstrap</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="index.html">Home</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="about.html">About</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="post.html">Sample Post</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
<!-- Page Header-->
<header class="masthead" style="background-image: url('assets/img/about-bg.jpg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="page-heading">
<h1>About Me</h1>
<span class="subheading">This is what I do.</span>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content-->
<main class="mb-4">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe nostrum ullam eveniet pariatur voluptates odit, fuga atque ea nobis sit soluta odio, adipisci quas excepturi maxime quae totam ducimus consectetur?</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius praesentium recusandae illo eaque architecto error, repellendus iusto reprehenderit, doloribus, minus sunt. Numquam at quae voluptatum in officia voluptas voluptatibus, minus!</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut consequuntur magnam, excepturi aliquid ex itaque esse est vero natus quae optio aperiam soluta voluptatibus corporis atque iste neque sit tempora!</p>
</div>
</div>
</div>
</main>
<!-- Footer-->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright &copy; Your Website 2023</div>
</div>
</div>
</div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="js/scripts.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Clean Blog - Start Bootstrap Theme</title>
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css" />
<!-- Core theme CSS (includes Bootstrap)-->
<link href="css/styles.css" rel="stylesheet" />
</head>
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="index.html">Start Bootstrap</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="index.html">Home</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="about.html">About</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="post.html">Sample Post</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
<!-- Page Header-->
<header class="masthead" style="background-image: url('assets/img/contact-bg.jpg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="page-heading">
<h1>Contact Me</h1>
<span class="subheading">Have questions? I have answers.</span>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content-->
<main class="mb-4">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<p>Want to get in touch? Fill out the form below to send me a message and I will get back to you as soon as possible!</p>
<div class="my-5">
<!-- * * * * * * * * * * * * * * *-->
<!-- * * SB Forms Contact Form * *-->
<!-- * * * * * * * * * * * * * * *-->
<!-- This form is pre-integrated with SB Forms.-->
<!-- To make this form functional, sign up at-->
<!-- https://startbootstrap.com/solution/contact-forms-->
<!-- to get an API token!-->
<form id="contactForm" data-sb-form-api-token="API_TOKEN">
<div class="form-floating">
<input class="form-control" id="name" type="text" placeholder="Enter your name..." data-sb-validations="required" />
<label for="name">Name</label>
<div class="invalid-feedback" data-sb-feedback="name:required">A name is required.</div>
</div>
<div class="form-floating">
<input class="form-control" id="email" type="email" placeholder="Enter your email..." data-sb-validations="required,email" />
<label for="email">Email address</label>
<div class="invalid-feedback" data-sb-feedback="email:required">An email is required.</div>
<div class="invalid-feedback" data-sb-feedback="email:email">Email is not valid.</div>
</div>
<div class="form-floating">
<input class="form-control" id="phone" type="tel" placeholder="Enter your phone number..." data-sb-validations="required" />
<label for="phone">Phone Number</label>
<div class="invalid-feedback" data-sb-feedback="phone:required">A phone number is required.</div>
</div>
<div class="form-floating">
<textarea class="form-control" id="message" placeholder="Enter your message here..." style="height: 12rem" data-sb-validations="required"></textarea>
<label for="message">Message</label>
<div class="invalid-feedback" data-sb-feedback="message:required">A message is required.</div>
</div>
<br />
<!-- Submit success message-->
<!---->
<!-- This is what your users will see when the form-->
<!-- has successfully submitted-->
<div class="d-none" id="submitSuccessMessage">
<div class="text-center mb-3">
<div class="fw-bolder">Form submission successful!</div>
To activate this form, sign up at
<br />
<a href="https://startbootstrap.com/solution/contact-forms">https://startbootstrap.com/solution/contact-forms</a>
</div>
</div>
<!-- Submit error message-->
<!---->
<!-- This is what your users will see when there is-->
<!-- an error submitting the form-->
<div class="d-none" id="submitErrorMessage"><div class="text-center text-danger mb-3">Error sending message!</div></div>
<!-- Submit Button-->
<button class="btn btn-primary text-uppercase disabled" id="submitButton" type="submit">Send</button>
</form>
</div>
</div>
</div>
</div>
</main>
<!-- Footer-->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright &copy; Your Website 2023</div>
</div>
</div>
</div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="js/scripts.js"></script>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *-->
<!-- * * SB Forms JS * *-->
<!-- * * Activate your form at https://startbootstrap.com/solution/contact-forms * *-->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *-->
<script src="https://cdn.startbootstrap.com/sb-forms-latest.js"></script>
</body>
</html>

View File

View File

File diff suppressed because it is too large Load Diff

View File

View File

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Clean Blog - Start Bootstrap Theme</title>
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css" />
<!-- Core theme CSS (includes Bootstrap)-->
<link href="css/styles.css" rel="stylesheet" />
</head>
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="index.html">Start Bootstrap</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="index.html">Home</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="about.html">About</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="post.html">Sample Post</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
<!-- Page Header-->
<header class="masthead" style="background-image: url('assets/img/home-bg.jpg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="site-heading">
<h1>Clean Blog</h1>
<span class="subheading">A Blog Theme by Start Bootstrap</span>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content-->
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Man must explore, and this is exploration at its greatest</h2>
<h3 class="post-subtitle">Problems look mighty small from 150 miles up</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 24, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="post.html"><h2 class="post-title">I believe every human has a finite number of heartbeats. I don't intend to waste any of mine.</h2></a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on September 18, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Science has not yet mastered prophecy</h2>
<h3 class="post-subtitle">We predict too much for the next year and yet far too little for the next ten.</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on August 24, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Post preview-->
<div class="post-preview">
<a href="post.html">
<h2 class="post-title">Failure is not an option</h2>
<h3 class="post-subtitle">Many say exploration is part of our destiny, but its actually our duty to future generations.</h3>
</a>
<p class="post-meta">
Posted by
<a href="#!">Start Bootstrap</a>
on July 8, 2023
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
<!-- Pager-->
<div class="d-flex justify-content-end mb-4"><a class="btn btn-primary text-uppercase" href="#!">Older Posts →</a></div>
</div>
</div>
</div>
<!-- Footer-->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright &copy; Your Website 2023</div>
</div>
</div>
</div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="js/scripts.js"></script>
</body>
</html>

View File

View File

View File

@@ -0,0 +1,30 @@
(function ($) {
"use strict";
var fullHeight = function () {
$('.js-fullheight').css('height', $(window).height());
$(window).resize(function () {
$('.js-fullheight').css('height', $(window).height());
});
};
fullHeight();
$('#sidebarCollapse').on('click', function () {
$('#sidebar').toggleClass('active');
});
})(jQuery);
window.addEventListener("load", function () {
let closeBtn = document.querySelectorAll(".closeAlertBtn");
closeBtn.forEach((item) => {
item.addEventListener("click", closeAlert);
});
});
function closeAlert() {
this.parentNode.setAttribute("style", "display: none;")
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
/*!
* Start Bootstrap - Clean Blog v6.0.9 (https://startbootstrap.com/theme/clean-blog)
* Copyright 2013-2023 Start Bootstrap
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-clean-blog/blob/master/LICENSE)
*/
window.addEventListener('DOMContentLoaded', () => {
let scrollPos = 0;
const mainNav = document.getElementById('mainNav');
const headerHeight = mainNav.clientHeight;
window.addEventListener('scroll', function() {
const currentTop = document.body.getBoundingClientRect().top * -1;
if ( currentTop < scrollPos) {
// Scrolling Up
if (currentTop > 0 && mainNav.classList.contains('is-fixed')) {
mainNav.classList.add('is-visible');
} else {
console.log(123);
mainNav.classList.remove('is-visible', 'is-fixed');
}
} else {
// Scrolling Down
mainNav.classList.remove(['is-visible']);
if (currentTop > headerHeight && !mainNav.classList.contains('is-fixed')) {
mainNav.classList.add('is-fixed');
}
}
scrollPos = currentTop;
});
})

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Clean Blog - Start Bootstrap Theme</title>
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
<!-- Font Awesome icons (free version)-->
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
<!-- Google fonts-->
<link href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css" />
<!-- Core theme CSS (includes Bootstrap)-->
<link href="css/styles.css" rel="stylesheet" />
</head>
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="index.html">Start Bootstrap</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="index.html">Home</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="about.html">About</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="post.html">Sample Post</a></li>
<li class="nav-item"><a class="nav-link px-lg-3 py-3 py-lg-4" href="contact.html">Contact</a></li>
</ul>
</div>
</div>
</nav>
<!-- Page Header-->
<header class="masthead" style="background-image: url('assets/img/post-bg.jpg')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="post-heading">
<h1>Man must explore, and this is exploration at its greatest</h1>
<h2 class="subheading">Problems look mighty small from 150 miles up</h2>
<span class="meta">
Posted by
<a href="#!">Start Bootstrap</a>
on August 24, 2023
</span>
</div>
</div>
</div>
</div>
</header>
<!-- Post Content-->
<article class="mb-4">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<p>Never in all their history have men been able truly to conceive of the world as one: a single sphere, a globe, having the qualities of a globe, a round earth in which all the directions eventually meet, in which there is no center because every point, or none, is center — an equal earth which all men occupy as equals. The airman's earth, if free men make it, will be truly round: a globe in practice, not in theory.</p>
<p>Science cuts two ways, of course; its products can be used for both good and evil. But there's no turning back from science. The early warnings about technological dangers also come from science.</p>
<p>What was most significant about the lunar voyage was not that man set foot on the Moon but that they set eye on the earth.</p>
<p>A Chinese tale tells of some men sent to harm a young girl who, upon seeing her beauty, become her protectors rather than her violators. That's how I felt seeing the Earth for the first time. I could not help but love and cherish her.</p>
<p>For those who have seen the Earth from space, and for the hundreds and perhaps thousands more who will, the experience most certainly changes your perspective. The things that we share in our world are far more valuable than those which divide us.</p>
<h2 class="section-heading">The Final Frontier</h2>
<p>There can be no thought of finishing for aiming for the stars. Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.</p>
<p>There can be no thought of finishing for aiming for the stars. Both figuratively and literally, it is a task to occupy the generations. And no matter how much progress one makes, there is always the thrill of just beginning.</p>
<blockquote class="blockquote">The dreams of yesterday are the hopes of today and the reality of tomorrow. Science has not yet mastered prophecy. We predict too much for the next year and yet far too little for the next ten.</blockquote>
<p>Spaceflights cannot be stopped. This is not the work of any one man or even a group of men. It is a historical process which mankind is carrying out in accordance with the natural laws of human development.</p>
<h2 class="section-heading">Reaching for the Stars</h2>
<p>As we got further and further away, it [the Earth] diminished in size. Finally it shrank to the size of a marble, the most beautiful you can imagine. That beautiful, warm, living object looked so fragile, so delicate, that if you touched it with a finger it would crumble and fall apart. Seeing this has to change a man.</p>
<a href="#!"><img class="img-fluid" src="assets/img/post-sample-image.jpg" alt="..." /></a>
<span class="caption text-muted">To go places and do things that have never been done before thats what living is all about.</span>
<p>Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.</p>
<p>As I stand out here in the wonders of the unknown at Hadley, I sort of realize theres a fundamental truth to our nature, Man must explore, and this is exploration at its greatest.</p>
<p>
Placeholder text by
<a href="http://spaceipsum.com/">Space Ipsum</a>
&middot; Images by
<a href="https://www.flickr.com/photos/nasacommons/">NASA on The Commons</a>
</p>
</div>
</div>
</div>
</article>
<!-- Footer-->
<footer class="border-top">
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<ul class="list-inline text-center">
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-twitter fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-facebook-f fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
<li class="list-inline-item">
<a href="#!">
<span class="fa-stack fa-lg">
<i class="fas fa-circle fa-stack-2x"></i>
<i class="fab fa-github fa-stack-1x fa-inverse"></i>
</span>
</a>
</li>
</ul>
<div class="small text-center text-muted fst-italic">Copyright &copy; Your Website 2023</div>
</div>
</div>
</div>
</footer>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="js/scripts.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 KiB

Some files were not shown because too many files have changed in this diff Show More