first init

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2022-11-27 23:47:38 +10:00
commit 32307e238d
63 changed files with 7904 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
!.gitignore
composer.phar
vendor

11
LICENSE Normal file
View File

@ -0,0 +1,11 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# site-mirzaev
My personal site

58
composer.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "mirzaev/site-mirzaev",
"description": "My personal site",
"readme": "README.md",
"keywords": [
"personal site",
"blog",
"articles",
"videos",
"news"
],
"type": "site",
"homepage": "https://git.mirzaev.sexy/mirzaev/site-mirzaev",
"license": "WTFPL",
"authors": [
{
"name": "Arsen Mirzaev Tatyano-Muradovich",
"email": "arsen@mirzaev.sexy",
"homepage": "https://mirzaev.sexy",
"role": "Programmer"
}
],
"support": {
"email": "arsen@mirzaev.sexy",
"wiki": "https://git.mirzaev.sexy/mirzaev/site-mirzaev/wiki",
"issues": "https://git.mirzaev.sexy/mirzaev/site-mirzaev/issues"
},
"funding": [
{
"type": "funding",
"url": "https://fund.mirzaev.sexy"
}
],
"require": {
"php": "~8.1",
"ext-sodium": "~8.1",
"mirzaev/minimal": "^2.0.x-dev",
"mirzaev/accounts": "~1.2.x-dev",
"mirzaev/arangodb": "^1.0.0",
"mirzaev/vk": "^4.0",
"triagens/arangodb": "~3.9.x-dev",
"twig/twig": "^3.4",
"guzzlehttp/guzzle": "^7.5"
},
"require-dev": {
"phpunit/phpunit": "~9.5"
},
"autoload": {
"psr-4": {
"mirzaev\\site\\mirzaev\\": "mirzaev/site/mirzaev/system"
}
},
"autoload-dev": {
"psr-4": {
"mirzaev\\site\\mirzaev\\tests\\": "mirzaev/site/mirzaev/tests"
}
}
}

3051
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\controllers;
// Файлы проекта
use mirzaev\site\mirzaev\controllers\core;
use mirzaev\site\mirzaev\models\account_model as account;
use mirzaev\site\mirzaev\models\session_model as session;
use mirzaev\site\mirzaev\models\vk_model as vk;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
use stdClass;
// Фреймворк для ВКонтакте
use mirzaev\vk\core as api;
/**
* Контроллер аккаунтов
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account_controller extends core
{
/**
* Страница профиля
*
* @param array $parameters Параметры запроса
*/
public function index(array $parameters = []): ?string
{
return null;
}
/**
* Инициализация
*
* @param array $parameters Параметры запроса
*/
public function initialization(array $parameters = []): ?string
{
if ($this->variables['account'] instanceof _document) {
// Найден аккаунт
if ($this->variables['vk'] instanceof _document) {
// Найден аккаунт ВКонтакте
// Инициализация данных аккаунта ВКонтакте
vk::parse($this->variables['vk'], $this->variables['errors']['vk']);
}
// Запись кода ответа
http_response_code(200);
return null;
} else {
// Не найден аккаунт
// Запись кода ответа
http_response_code(401);
// Запись заголовка ответа с ключом аккаунта
header('session: ' . $this->variables['session']->hash);
return null;
}
// Запись кода ответа
http_response_code(500);
return null;
}
/**
* Связь аккаунта с аккаунтом ВКонтакте
*
* @param array $parameters Параметры запроса
*/
public function connect(array $parameters = []): ?string
{
if ($this->variables['session']->hash === $parameters['state']) {
// Совпадает хеш сессии с полученным хешем из ответа ВКонтакте
if (!empty($response = vk::key($parameters['code'], $this->variables['errors']['vk']))) {
// Получены данные аккаунта ВКонтакте
if (($this->variables['vk'] = vk::initialization($response, $this->variables['errors']['vk'])) instanceof _document) {
// Инициализирован аккаунт ВКонтакте
if (($this->variables['account'] = vk::account($this->variables['vk'])) instanceof _document) {
// Найден аккаунт (существующий)
if (session::connect($this->variables['session'], $this->variables['account'], $this->variables['errors']['session'])) {
// Связана сессия с аккаунтом
}
} else if (($this->variables['account'] = account::create($this->variables['errors']['account'])) instanceof _document) {
// Найден аккаунт (создан новый)
if (session::connect($this->variables['session'], $this->variables['account'], $this->variables['errors']['session'])) {
// Связана сессия с аккаунтом
if (account::connect($this->variables['account'], $this->variables['vk'], $this->variables['errors']['account'])) {
// Связан аккаунт с аккаунтом ВКонтакте
}
}
}
// Инициализация робота для аккаунта ВКонтакте
$this->vk = api::init()->user(key: $this->variables['vk']->access['key']);
if ($this->variables['vk'] instanceof _document) {
// Инициализирован робот для аккаунта ВКонтакте
// Инициализация данных аккаунта ВКонтакте
$data = vk::parse($this->vk, $this->variables['errors']['vk']);
var_dump($data); die;
if ($data instanceof stdClass) {
// Получены данные ВКонтакте
// Запись в базу данных
vk::update($this->variables['vk'], $data, $this->variables['errors']['vk']);
}
}
}
}
}
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'account' . DIRECTORY_SEPARATOR . 'vk.html', $this->variables);
}
/**
* Генерация панели аккаунта
*
* @param array $parameters Параметры запроса
*/
public function panel(array $parameters = []): ?string
{
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'account' . DIRECTORY_SEPARATOR . 'panel.html', $this->variables);
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\controllers;
// Файлы проекта
use mirzaev\site\mirzaev\views\manager;
use mirzaev\site\mirzaev\models\core as models;
use mirzaev\site\mirzaev\models\account_model as account;
use mirzaev\site\mirzaev\models\session_model as session;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Фреймворк PHP
use mirzaev\minimal\controller;
// Фреймворк ВКонтакте
use mirzaev\vk\core as vk;
use mirzaev\vk\robots\user as robot;
/**
* Ядро контроллеров
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class core extends controller
{
/**
* Переменные окружения
*/
protected robot $vk;
/**
* Переменные окружения
*/
protected array $variables = [];
/**
* Конструктор
*
* @return void
*/
public function __construct()
{
parent::__construct();
// Инициализация ядра моделей (соединение с базой данных...)
new models();
// Инициализация журнала ошибок
$this->variables['errors'] = [
'session' => [],
'account' => [],
'vk' => []
];
// Инициализация даты до которой будет активна сессия
$expires = time() + 604800;
// Инициализация сессии (без журналирования)
$this->variables['session'] = session::initialization($_COOKIE["session"] ?? null, $expires) ?? header('Location: https://mirzaev.sexy/error?code=500&text=Не+удалось+инициализировать+сессию');
if ($_COOKIE["session"] ?? null !== $this->variables['session']->hash) {
// Изменился хеш сессии (подразумевается, что сессия устарела)
// Запись хеша новой сессии
setcookie('session', $this->variables['session']->hash, [
'expires' => $expires,
'domain' => 'mirzaev.sexy',
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'strict'
]);
}
// Инициализация аккаунта (без журналирования)
$this->variables['account'] = session::account($this->variables['session']);
if ($this->variables['account'] instanceof _document) {
// Инициализирован аккаунт
// Инициализация аккаунта ВКонтакте (без журналирования)
$this->variables['vk'] = account::vk($this->variables['account']);
if ($this->variables['vk'] instanceof _document) {
// Инициализирован аккаунт ВКонтакте
// Инициализация робота для аккаунта ВКонтакте
$this->vk = vk::init()->user(key: $this->variables['vk']->access['key']);
} else unset($this->variables['account'], $this->variables['vk']);
}
// Инициализация препроцессора представления
$this->view = new manager;
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\controllers;
// Файлы проекта
use mirzaev\site\mirzaev\controllers\core;
/**
* Контроллер ошибок
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class error_controller extends core
{
/**
* Страница с ошибкой
*
* @param array $parameters
*/
public function index(array $parameters = []): ?string
{
// Запись текста ошибки в переменную окружения
$this->variables['text'] = $parameters['text'] ?? null;
if (isset($parameters['code'])) {
// Получен код ошибки
// Запись кода ошибки в переменную окружения
$this->variables['code'] = $parameters['code'];
// Запись кода ответа
http_response_code($parameters['code']);
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . 'index.html', $this->variables);
}
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR . ($parameters['code'] ?? 'index') . '.html', $this->variables);
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\controllers;
// Файлы проекта
use mirzaev\site\mirzaev\controllers\core;
/**
* Контроллер графика
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class graph_controller extends core
{
/**
* Страница с графиком
*
* Можно использовать совместно с элементом <iframe> для изоляции
* содержимого бегущей строки от поисковых роботов
*
* @param array $parameters
*/
public function index(array $parameters = []): ?string
{
// Инициализация элементов для генерации в головном элементе
$this->variables['head'] = [
'title' => 'Бегущая строка',
'metas' => [
[
'attributes' => [
'name' => 'robots',
'content' => 'nofollow'
]
]
]
];
// Инициализация бегущей строки
$this->variables['graph'] = [
'id' => $this->variables['request']['id'] ?? 'graph'
];
// Инициализация аттрибутов бегущей строки
$this->variables['graph']['attributes'] = [
];
// Инициализация элементов бегущей строки
$this->variables['graph']['elements'] = [
];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'graph' . DIRECTORY_SEPARATOR . 'index.html', $this->variables);
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\controllers;
// Файлы проекта
use mirzaev\site\mirzaev\controllers\core;
/**
* Контроллер бегущей строки
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class hotline_controller extends core
{
/**
* Страница с бегущей строкой
*
* Можно использовать совместно с элементом <iframe> для изоляции
* содержимого бегущей строки от поисковых роботов
*
* @param array $parameters
*/
public function index(array $parameters = []): ?string
{
// Инициализация элементов для генерации в головном элементе
$this->variables['head'] = [
'title' => 'Бегущая строка',
'metas' => [
[
'attributes' => [
'name' => 'robots',
'content' => 'nofollow'
]
]
]
];
// Инициализация бегущей строки
$this->variables['hotline'] = [
'id' => $this->variables['request']['id'] ?? 'hotline'
];
// Инициализация параметров бегущей строки
$this->variables['hotline']['parameters'] = [
// 'step' => 2
];
// Инициализация аттрибутов бегущей строки
$this->variables['hotline']['attributes'] = [
];
// Инициализация элементов бегущей строки
$this->variables['hotline']['elements'] = [
['content' => '1'],
[
'tag' => 'article',
'content' => '2'
],
['content' => '3'],
['content' => '4'],
['content' => '5'],
['content' => '6'],
['content' => '7'],
['content' => '8'],
['content' => '9'],
['content' => '10'],
['content' => '11'],
['content' => '12'],
['content' => '13'],
['content' => '14'],
['content' => '15']
];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'hotline' . DIRECTORY_SEPARATOR . 'index.html', $this->variables);
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\controllers;
// Файлы проекта
use mirzaev\site\mirzaev\controllers\core;
/**
* Контроллер основной страницы
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class index_controller extends core
{
/**
* Главная страница
*
* @param array $parameters Параметры запроса
*/
public function index(array $parameters = []): ?string
{
// Инициализация загружаемых категорий
$this->variables['include'] = [
'head' => ['self'],
'body' => ['self']
];
// Инициализация бегущей строки
$this->variables['hotline'] = [
'id' => $this->variables['request']['id'] ?? 'hotline'
];
// Инициализация параметров бегущей строки
$this->variables['hotline']['parameters'] = [
// 'step' => 2
];
// Инициализация аттрибутов бегущей строки
$this->variables['hotline']['attributes'] = [
];
// Инициализация элементов бегущей строки
$this->variables['hotline']['elements'] = [
['content' => '1'],
[
'tag' => 'article',
'content' => '2'
],
['content' => '3'],
['content' => '4'],
['content' => '5'],
['content' => '6'],
['content' => '7'],
['content' => '8'],
['content' => '9'],
['content' => '10'],
['content' => '11'],
['content' => '12'],
['content' => '13'],
['content' => '14'],
['content' => '15']
];
// Инициализация бегущей строки
$this->variables['graph'] = [
'id' => $this->variables['request']['id'] ?? 'graph'
];
// Инициализация аттрибутов бегущей строки
$this->variables['graph']['attributes'] = [
];
// Инициализация элементов бегущей строки
$this->variables['graph']['elements'] = [
];
// Генерация представления
return $this->view->render(DIRECTORY_SEPARATOR . 'index.html', $this->variables);
}
}

View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\models;
// Файлы проекта
use mirzaev\site\mirzaev\models\vk_model as vk;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
/**
* Модель регистрации, аутентификации и авторизации
*
* @package mirzaev\site\mirzaev\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class account_model extends core
{
/**
* Коллекция
*/
public const COLLECTION = 'account';
/**
* Создать
*
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта, если удалось создать
*/
public static function create(array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
// Запись аккаунта в базу данных
$_id = document::write(static::$db->session, self::COLLECTION);
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '$_id'
RETURN d
AQL,
self::COLLECTION
))) {
// Найден созданный аккаунт
return $account;
} else throw new exception('Не удалось создать аккаунт');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Связь аккаунта с аккаунтом ВКонтакте
*
* @param _document $account Инстанция аккаунта
* @param _document $vk Инстанция аккаунта ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return bool Статус выполнения
*/
public static function connect(_document $account, _document $vk, array &$errors = []): bool
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, vk::COLLECTION)
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, true)
) {
// Инициализированы коллекции
if (document::write(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, [
'_from' => $account->getId(),
'_to' => $vk->getId()
])) {
// Создано ребро: account -> vk
return true;
} else throw new exception('Не удалось создать ребро: account -> vk');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return false;
}
/**
* Поиск связанного аккаунта ВКонтакте
*
* @param _document $account Инстанция аккаунта
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта, если удалось найти
*/
public static function vk(_document $account, array &$errors = []): ?_document
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, vk::COLLECTION)
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . vk::COLLECTION, true)
) {
// Инициализирована коллекция
if ($vk = collection::search(static::$db->session, sprintf(
<<<AQL
FOR document IN %s
LET edge = (
FOR edge IN %s
FILTER edge._from == '%s'
SORT edge._key DESC
LIMIT 1
RETURN edge
)
FILTER document._id == edge[0]._to
LIMIT 1
RETURN document
AQL,
vk::COLLECTION,
self::COLLECTION . '_edge_' . vk::COLLECTION,
$account->getId()
))) {
// Найден аккаунт ВКонтакте
return $vk;
} else throw new exception('Не удалось найти аккаунт ВКонтакте');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\models;
use mirzaev\minimal\model;
use mirzaev\arangodb\connection;
use exception;
/**
* Ядро моделей
*
* @package mirzaev\site\mirzaev\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class core extends model
{
/**
* Коллекция в которой хранятся аккаунты
*/
public const SETTINGS = '../settings/arangodb.php';
/**
* Соединение с базой данных
*/
protected static connection $db;
public function __construct(connection $db = null)
{
if (isset($db)) {
// Получена инстанция соединения с базой данных
// Запись и инициализация соединения с базой данных
$this->__set('db', $db);
} else {
// Не получена инстанция соединения с базой данных
// Инициализация соединения с базой данных по умолчанию
$this->__get('db');
}
}
/**
* Записать свойство
*
* @param string $name Название
* @param mixed $value Значение
*/
public function __set(string $name, mixed $value = null): void
{
match ($name) {
'db' => (function () use ($value) {
if ($this->__isset('db')) {
// Свойство уже было инициализировано
// Выброс исключения (неудача)
throw new exception('Запрещено реинициализировать соединение с базой данных ($this->db)', 500);
} else {
// Свойство ещё не было инициализировано
if ($value instanceof connection) {
// Передано подходящее значение
// Запись свойства (успех)
self::$db = $value;
} else {
// Передано неподходящее значение
// Выброс исключения (неудача)
throw new exception('Соединение с базой данных ($this->db) должен быть инстанцией mirzaev\arangodb\connection', 500);
}
}
})(),
default => parent::__set($name, $value)
};
}
/**
* Прочитать свойство
*
* @param string $name Название
*
* @return mixed Содержимое
*/
public function __get(string $name): mixed
{
return match ($name) {
'db' => (function () {
if (!$this->__isset('db')) {
// Свойство не инициализировано
// Инициализация значения по умолчанию исходя из настроек
$this->__set('db', new connection(require static::SETTINGS));
}
return self::$db;
})(),
default => parent::__get($name)
};
}
/**
* Проверить свойство на инициализированность
*
* @param string $name Название
*/
public function __isset(string $name): bool
{
return match ($name) {
default => parent::__isset($name)
};
}
/**
* Удалить свойство
*
* @param string $name Название
*/
public function __unset(string $name): void
{
match ($name) {
default => parent::__isset($name)
};
}
/**
* Статический вызов
*
* @param string $name Название
* @param array $arguments Параметры
*/
public static function __callStatic(string $name, array $arguments): mixed
{
match ($name) {
'db' => (new static)->__get('db'),
default => throw new exception("Не найдено свойство или функция: $name", 500)
};
}
}

View File

@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\models;
// Файлы проекта
use mirzaev\site\mirzaev\models\account_model as account;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Встроенные библиотеки
use exception;
/**
* Модель сессий
*
* @package mirzaev\site\mirzaev\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class session_model extends core
{
/**
* Коллекция
*/
public const COLLECTION = 'session';
/**
* Инициализация
*
* @param ?string $hash Хеш сессии в базе данных
* @param ?int $expires Дата окончания работы сессии (используется при создании новой сессии)
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция сессии, если удалось найти или создать
*/
public static function initialization(?string $hash = null, ?int $expires = null, array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
if (isset($hash) && $session = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.hash == '$hash' && d.expires > %d
RETURN d
AQL,
self::COLLECTION,
time()
))) {
// Найдена сессия по хешу
// Возврат сессии
return $session;
} else if ($session = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.ip == '%s' && d.expires > %d
RETURN d
AQL,
self::COLLECTION,
$_SERVER['REMOTE_ADDR'],
time()
))) {
// Найдена сессия по данным пользователя
// Возврат сессии
return $session;
} else {
// Не найдена сессия
// Запись сессии в базу данных
$_id = document::write(static::$db->session, self::COLLECTION, [
'ip' => $_SERVER['REMOTE_ADDR'],
'expires' => $expires ?? time() + 604800
]);
if ($session = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '$_id' && d.expires > %d
RETURN d
AQL,
self::COLLECTION,
time()
))) {
// Найдена созданная сессия
// Запись хеша
$session->hash = sodium_bin2hex(sodium_crypto_generichash($_id));
if (document::update(static::$db->session, $session)) {
// Записано обновление
return $session;
} else throw new exception('Не удалось записать данные сессии');
} else throw new exception('Не удалось создать или найти созданную сессию');
}
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Связь сессии с аккаунтом
*
* @param _document $session Инстанция сессии
* @param _document $account Инстанция аккаунта
* @param array &$errors Журнал ошибок
*
* @return bool Статус выполнения
*/
public static function connect(_document $session, _document $account, array &$errors = []): bool
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, account::COLLECTION)
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
) {
// Инициализирована коллекция
if (document::write(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, [
'_from' => $session->getId(),
'_to' => $account->getId()
])) {
// Создано ребро: session -> account
return true;
} else throw new exception('Не удалось создать ребро: session -> account');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return false;
}
/**
* Поиск связанного аккаунта
*
* @param _document $session Инстанция сессии
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта, если удалось найти
*/
public static function account(_document $session, array &$errors = []): ?_document
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, account::COLLECTION)
&& collection::init(static::$db->session, self::COLLECTION . '_edge_' . account::COLLECTION, true)
) {
// Инициализированы коллекции
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR document IN %s
LET edge = (
FOR edge IN %s
FILTER edge._from == '%s'
SORT edge._key DESC
LIMIT 1
RETURN edge
)
FILTER document._id == edge[0]._to
LIMIT 1
RETURN document
AQL,
account::COLLECTION,
self::COLLECTION . '_edge_' . account::COLLECTION,
$session->getId()
))) {
// Найден аккаунт
return $account;
} else throw new exception('Не удалось найти аккаунт');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
}

View File

@ -0,0 +1,555 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\models;
// Файлы проекта
use mirzaev\site\mirzaev\models\account_model as account;
// Фреймворк ArangoDB
use mirzaev\arangodb\collection,
mirzaev\arangodb\document;
// Фреймворк ВКонтакте
use mirzaev\vk\robots\user as robot;
// Библиотека для ArangoDB
use ArangoDBClient\Document as _document;
// Библиотека браузера
use GuzzleHttp\Client as browser;
// Встроенные библиотеки
use exception;
use stdClass;
/**
* Модель аккаунта ВКонтакте
*
* @package mirzaev\site\mirzaev\models
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class vk_model extends core
{
/**
* Коллекция
*/
public const COLLECTION = 'vk';
/**
* Инициализация
*
* @param string $response Ответ сервера ВКонтакте с данными аккаунта
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта ВКонтакте, если удалось создать
*/
public static function initialization(string $response = '', array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
// Инициализация данных аккаунта ВКонтакте
$data = json_decode($response);
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d.id == $data->user_id
RETURN d
AQL,
self::COLLECTION
))) {
// Найден аккаунт ВКонтакте
return $account;
} else {
// Не найден аккаунт ВКонтакте
return self::create($response, $errors);
}
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Создание
*
* @param string $response Ответ сервера ВКонтакте с данными аккаунта
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта ВКонтакте, если удалось создать
*/
public static function create(string $response = '', array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
// Запись аккаунта в базу данных
$_id = document::write(static::$db->session, self::COLLECTION);
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR d IN %s
FILTER d._id == '$_id'
RETURN d
AQL,
self::COLLECTION
))) {
// Найден созданный аккаунт ВКонтакте
if (document::update(static::$db->session, $account)) {
// Записано обновление
// Запись данных об аккаунте ВКонтакте и возврат (bool)
return self::update($account, json_decode($response), $errors);
}
}
throw new exception('Не удалось создать аккаунт ВКонтакте');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Запросить ключ
*
* @param string $code Код полученный от ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return ?string Тело ответа, если получен код ответа 200
*/
public static function key(string $code = '', array &$errors = []): ?string
{
try {
// Инициализация браузера
$browser = new browser();
// Запрос
$response = $browser->request('GET', "https://oauth.vk.com/access_token?client_id=51447080&client_secret=KYlk0nGELW0A9ds7NQi6&redirect_uri=https://mirzaev.sexy/account/vk/connect&code=$code");
if ($response->getStatusCode() === 200) {
// Ответ сервера: 200
return (string) $response->getBody();
} else throw new exception('Не удалось получить ключ ВКонтакте (' . $response->getStatusCode() . ')');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Поиск связанного аккаунта
*
* @param _document $vk Инстанция аккаунта ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта, если удалось найти
*/
public static function account(_document $vk, array &$errors = []): ?_document
{
try {
if (
collection::init(static::$db->session, self::COLLECTION)
&& collection::init(static::$db->session, account::COLLECTION)
&& collection::init(static::$db->session, account::COLLECTION . '_edge_' . self::COLLECTION, true)
) {
// Инициализированы коллекции
if ($account = collection::search(static::$db->session, sprintf(
<<<AQL
FOR document IN %s
LET edge = (
FOR edge IN %s
FILTER edge._to == '%s'
SORT edge._key DESC
LIMIT 1
RETURN edge
)
FILTER document._id == edge[0]._from
LIMIT 1
RETURN document
AQL,
account::COLLECTION,
account::COLLECTION . '_edge_' . self::COLLECTION,
$vk->getId()
))) {
// Найден аккаунт
return $account;
} else throw new exception('Не удалось найти аккаунт');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Запрос данных аккаунта ВКонтакте с серверов ВКонтакте
*
* @param robot $vk Инстанция аккаунта ВКонтакте
* @param array &$errors Журнал ошибок
*
* @return ?stdClass Данные аккаунта ВКонтакте, если получены
*/
public static function parse(robot $vk, array &$errors = []): ?stdClass
{
try {
// Запрос к API-серверу ВКонтакте
$response = $vk->user->get(fields: [
'activities',
'about',
// 'blacklisted',
// 'blacklisted_by_me',
'books',
'bdate',
'can_be_invited_group',
'can_post',
'can_see_all_posts',
'can_see_audio',
'can_send_friend_request',
'can_write_private_message',
'career',
'common_count',
'connections',
'contacts',
'city',
'country',
'crop_photo',
'domain',
'education',
'exports',
'followers_count',
'friend_status',
'has_photo',
'has_mobile',
'home_town',
'photo_50',
'photo_100',
'photo_200',
'photo_200_orig',
'photo_400_orig',
'photo_max',
'photo_max_orig',
'sex',
'site',
'schools',
'screen_name',
'status',
'verified',
'games',
'interests',
'is_favorite',
'is_friend',
'is_hidden_from_feed',
'last_seen',
'maiden_name',
'military',
'movies',
'music',
'nickname',
'occupation',
'online',
'personal',
'photo_id',
'quotes',
'relation',
'relatives',
'timezone',
'tv',
'universities'
])[0];
if (!empty($response)) {
// Получен ответ
return $response;
}
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
/**
* Обновление данных аккаунта ВКонтакте
*
* Все файлы (аватар, например) будут скачаны на сервер
*
* @param _document $vk Инстанция аккаунта ВКонтакте
* @param stdClass $data Информация об аккаунте (self::parse() или json_decode())
* @param array &$errors Журнал ошибок
*
* @return ?_document Инстанция аккаунта ВКонтакте, если удалось обновить
*/
public static function update(_document $vk, stdClass $data, array &$errors = []): ?_document
{
try {
if (collection::init(static::$db->session, self::COLLECTION)) {
// Инициализирована коллекция
if (empty($vk->id) and isset($data->user_id) || isset($data->id)) {
// Получен идентификатор
// Запись
$vk->id = $data->user_id ?? $data->id;
// Удаление из списка необработанных
unset($data->user_id, $data->id);
} else if (empty($vk->id)) throw new exception('Не удалось найти идентификатор аккаунта ВКонтакте');
if (isset($data->access_token, $data->expires_in)) {
// Получен ключ
// Запись
$vk->access = [
'key' => $data->access_token,
'expires' => $data->expires_in
];
// Удаление из списка необработанных
unset($data->access_token, $data->expires_in);
}
// Инициализация браузера
$browser = new browser();
// Инициализация директории с обложкой
if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR))
mkdir($path, 0775, true);
if (isset($data->photo_50)) {
// Получено изображение 50x50
if ($browser->get($data->photo_50, ['sink' => $file = "$path/50x50.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'50x50' => ($vk->cover['50x50'] ?? []) +
[
'source' => $data->photo_50,
'public' => "/storage/$vk->id/cover/50x50.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 50x50 с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_50);
}
// Инициализация директории с обложкой
if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR))
mkdir($path, 0775, true);
if (isset($data->photo_100)) {
// Получено изображение 100x100
if ($browser->get($data->photo_100, ['sink' => $file = "$path/100x100.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'100x100' => ($vk->cover['100x100'] ?? []) +
[
'source' => $data->photo_100,
'public' => "/storage/$vk->id/cover/100x100.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 100x100 с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_100);
}
// Инициализация директории с обложкой
if (!file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . $vk->id . DIRECTORY_SEPARATOR . 'cover' . DIRECTORY_SEPARATOR))
mkdir($path, 0775, true);
if (isset($data->photo_200)) {
// Получено изображение 200x200
if ($browser->get($data->photo_200, ['sink' => $file = "$path/200x200.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'200x200' => ($vk->cover['200x200'] ?? []) +
[
'source' => $data->photo_200,
'public' => "/storage/$vk->id/cover/200x200.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 200x200 с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_200);
}
if (isset($data->photo_200_orig)) {
// Получено изображение 200x
if ($browser->get($data->photo_200_orig, ['sink' => $file = "$path/200x.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'200x' => ($vk->cover['200x'] ?? []) +
[
'source' => $data->photo_200_orig,
'public' => "/storage/$vk->id/cover/200x.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 200x с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_200_orig);
}
if (isset($data->photo_400_orig)) {
// Получено изображение 400x
if ($browser->get($data->photo_400_orig, ['sink' => $file = "$path/400x.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'400x' => ($vk->cover['400x'] ?? []) +
[
'source' => $data->photo_400_orig,
'public' => "/storage/$vk->id/cover/400x.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение 400x с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_400_orig);
}
if (isset($data->photo_max)) {
// Получено изображение MAXxMAX
if ($browser->get($data->photo_max, ['sink' => $file = "$path/MAXxMAX.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'MAXxMAX' => ($vk->cover['MAXxMAX'] ?? []) +
[
'source' => $data->photo_max,
'public' => "/storage/$vk->id/cover/MAXxMAX.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение MAXxMAX с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_max);
}
if (isset($data->photo_max_orig)) {
// Получено изображение MAXx
if ($browser->get($data->photo_max_orig, ['sink' => $file = "$path/MAXx.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'MAXx' => ($vk->cover['MAXx'] ?? []) +
[
'source' => $data->photo_max_orig,
'public' => "/storage/$vk->id/cover/MAXx.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение MAXx с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_max_orig);
}
if (isset($data->crop_photo)) {
// Получено изображение MAXx
if ($browser->get($data->photo_max_orig, ['sink' => $file = "$path/MAXx.jpg"])->getStatusCode() === 200)
$vk->cover =
($vk->cover ?? []) +
[
'MAXx' => ($vk->cover['MAXx'] ?? []) +
[
'source' => $data->photo_max_orig,
'public' => "/storage/$vk->id/cover/MAXx.jpg",
'local' => $file,
]
];
else throw new exception('Не удалось получить изображение MAXx с серверов ВКонтакте');
// Удаление из списка необработанных
unset($data->photo_max_orig);
}
// Перебор оставшихся параметров
foreach ($data as $key => $value) $vk->{$key} = $value;
if (document::update(static::$db->session, $vk)) {
// Записано обновление
return $vk;
} else throw new exception('Не удалось записать данные аккаунта ВКонтакте');
} else throw new exception('Не удалось инициализировать коллекцию');
} catch (exception $e) {
// Запись в журнал ошибок
$errors[] = [
'text' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'stack' => $e->getTrace()
];
}
return null;
}
}

View File

@ -0,0 +1,33 @@
section#account {
z-index : 10000;
right : 40px;
bottom : 40px;
position: absolute;
display : flex;
}
section#account>button#login {
width : 80px;
height : 80px;
display : flex;
border-radius : 100%;
cursor : pointer;
overflow : hidden;
background-color: var(--background-inverse);
}
section#account>button#login>i.icon.authentication {
width : 12px;
margin : auto;
color : var(--text-inverse);
background-color: unset;
}
section#account>button#login:hover>i.icon.authentication {
background-color: var(--grey-dark);
}
section#account>button#login:active>i.icon.authentication {
background-color: unset;
}

View File

@ -0,0 +1,52 @@
main>section#books {
display: flex;
flex-flow: row wrap;
}
main>section#books>* {
margin-bottom: 20px;
}
main>section#books>form.upload {
width: calc(100% / 3 - 20px - 9px * 2);
height: calc(220px - 9px * 2);
margin: 5px;
margin-right: 20px;
}
main>section#books>form.upload>p {
font-size: 3rem;
height: 0.3rem;
line-height: 0;
}
main>section#books>article.book {
width: calc(100% / 3 - 20px);
margin-right: 20px;
display: flex;
flex-direction: column;
}
main>section#books>article.book:nth-child(3) {
width: calc(100% / 3);
margin-right: unset;
}
main>section#books>article.book>img {
height: 220px;
object-fit: cover;
object-position: right;
overflow: hidden;
clip-path: polygon(5px calc(100% - 5px), calc(100% - 5px) calc(100% - 5px), calc(100% - 5px) 5px, 5px 5px);
}
main>section#books>article.book>h4 {
margin-top: 5px;
margin-bottom: 10px;
height: 50px;
text-align: center;
}
main>section#books>article.book>p {
margin: unset;
}

View File

@ -0,0 +1,244 @@
@keyframes node-select {
from {
outline-offset: 0px;
}
to {
outline : 2px solid var(--grey-light);
outline-offset: 10px;
}
}
@keyframes node-select-revert {
from {
outline : 2px solid var(--grey-light);
outline-offset: 10px;
}
to {
outline-offset: 0px;
}
}
@keyframes description-select {
from {
outline-offset: 0px;
}
to {
outline : 2px solid var(--grey);
outline-offset: 10px;
}
}
@keyframes description-select-revert {
from {
outline : 2px solid var(--grey);
outline-offset: 10px;
}
to {
outline-offset: 0px;
}
}
section.graph {
width : 100vw;
height : 100vh;
position : absolute;
transition: unset;
}
section.graph * {
transition: unset;
}
section.graph:active {
cursor: move;
}
section.graph article.node {
z-index : 500;
position : absolute;
display : flex;
cursor : grab;
border-radius : 100%;
background-color : var(--node-background);
}
section.graph article.node.animated {
animation-duration : 0.1s;
animation-name : node-select-revert;
animation-fill-mode : forwards;
}
section.graph article.node.animated:hover {
animation-duration : 0.1s;
animation-name : node-select;
animation-fill-mode : forwards;
}
section.graph article.node:active {
cursor : grabbing;
background-color: var(--node-background-important);
}
section.graph article.node>h4.title {
margin : auto;
text-align: center;
cursor : pointer;
}
section.graph article.node>div.description {
z-index : 1000;
position : absolute;
text-align : justify;
text-align-last : center;
border-radius : 100%;
overflow : hidden;
background-color: var(--node-background-completed);
}
/* section.graph article.node>div.description.animated {
animation-duration : 0.1s;
animation-name : description-select-revert;
animation-fill-mode : forwards;
}
section.graph article.node>div.description.animated:hover {
animation-duration : 0.1s;
animation-name : description-select;
animation-fill-mode : forwards;
} */
section.graph article.node * {
transition: 0.1s ease-in;
}
section.graph article.node>div.description>span.wrapper {
width : 50%;
height : 100%;
shape-margin: 15px;
}
section.graph article.node>div.description>span.left.wrapper {
float : left;
shape-outside: polygon(100% 0%, 0% 0%, 0% 100%, 100% 100%, 68% 98%, 38% 90%, 10% 72%, 0% 50%, 10% 28%, 20% 20%, 100% 20%);
}
section.graph article.node>div.description>span.right.wrapper {
float : right;
shape-outside: polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%, 0% 20%, 82% 20%, 90% 28%, 100% 50%, 90% 72%, 60% 90%, 32% 98%);
}
section.graph article.node>div.description>p {
z-index : 1500;
position : relative;
margin : 0;
opacity : 0;
word-break: break-all;
color : var(--text-inverse);
}
section.graph article.node>div.description:hover>p {
opacity: 1;
}
section.graph article.node>div.description>a.source {
z-index : 2000;
top : calc(20% - 1.3em + (1em - 1.3ex));
left : 0;
position : absolute;
font-weight: bold;
font-size : 1.3em;
opacity : 0;
}
section.graph article.node.white>div.description>a.source {
color: var(--text-inverse);
}
section.graph article.node.white>div.description>a.source:active {
color: var(--text-inverse-active);
}
section.graph article.node.white>div.description>a.source:hover {
color: var(--text-inverse-hover);
}
section.graph article.node.red>div.description>a.source {
color: var(--text-red);
}
section.graph article.node.red>div.description>a.source:active {
color: var(--text-red-active);
}
section.graph article.node.red>div.description>a.source:hover {
color: var(--text-red-hover);
}
section.graph article.node>div.description>a.source.red:active {
color: var(--text-red-active);
}
section.graph article.node>div.description:hover>a.source {
opacity: 1;
}
section.graph article.node>div.description>a.source:visited ::after {
content : '';
width : 100%;
height : 100%;
background-color: var(--node-background-completed);
}
section.graph article.node>div.description>img.cover {
left : 0;
top : 0;
position : absolute;
width : 100%;
height : 100%;
object-fit : cover;
pointer-events: none;
filter : unset;
}
section.graph article.node>div.description:hover>img.cover {
filter: blur(5px) brightness(0.5);
}
section.graph article.node>i.close {
z-index : -2000;
top : -10%;
right : -10%;
position : absolute;
scale : 0;
opacity : 0;
cursor : pointer;
color : var(--text);
transition: 0.2s ease-out;
}
section.graph article.node>i.close:hover {
scale : 1.4 !important;
color : var(--text-hover);
transition: 0.1s ease-in;
}
section.graph article.node>i.close:active {
scale : 1.2 !important;
color : var(--text-active);
transition: unset;
}
section.graph svg.connection {
z-index : -500;
position: absolute;
width : 100%;
height : 100%;
}
section.graph svg.connection>line {
stroke: var(--connection);
}

View File

@ -0,0 +1,31 @@
section.hotline {
display: inline-flex;
height : 100%;
}
section.hotline * {
transition: unset;
}
section.hotline:last-child {
margin-bottom: unset;
}
section.hotline>article {
margin-right : 18px;
width : 140px;
height : 190px;
display : flex;
align-self : flex-end;
border-radius : 3px;
background-color: var(--background-light-1);
box-shadow : 0px -6px 6px rgba(0, 0, 0, 0.3);
}
section.hotline>article:last-child {
margin-right: unset;
}
section.hotline>article>* {
margin: auto;
}

View File

@ -0,0 +1,40 @@
.icon.authentication {
box-sizing : border-box;
position : relative;
display : block;
transform : scale(var(--ggs, 1));
width : 6px;
height : 16px;
border : 2px solid;
border-left : 0;
border-top-right-radius : 2px;
border-bottom-right-radius: 2px;
margin-right : -10px
}
.icon.authentication::after,
.icon.authentication::before {
content : "";
display : block;
box-sizing: border-box;
position : absolute
}
.icon.authentication::after {
border-top : 2px solid;
border-right: 2px solid;
transform : rotate(45deg);
width : 8px;
height : 8px;
left : -8px;
bottom : 2px
}
.icon.authentication::before {
border-radius: 3px;
width : 10px;
height : 2px;
background : currentColor;
left : -11px;
bottom : 5px
}

View File

@ -0,0 +1,29 @@
.icon.close {
box-sizing : border-box;
position : relative;
display : block;
transform : scale(var(--ggs, 1));
width : 22px;
height : 22px;
border : 2px solid transparent;
border-radius: 40px
}
.icon.close::after,
.icon.close::before {
content : "";
display : block;
box-sizing : border-box;
position : absolute;
width : 16px;
height : 2px;
background : currentColor;
transform : rotate(45deg);
border-radius: 5px;
top : 8px;
left : 1px
}
.icon.close::after {
transform: rotate(-45deg)
}

View File

@ -0,0 +1,185 @@
@import url('/fonts/comissioner.ttf');
@media (prefers-color-scheme: light) {
:root {
--background : #eee;
--background-inverse : #221e1e;
--background-inverse-dark : #120f0f;
--node-background-important: #c3eac3;
--node-background-completed: #b0c0b0;
--node-background : #bdb;
--connection : #b2b7b2;
--connection-completed : #d1d1d1;
--text : #131313;
--text-hover : #292929;
--text-active : #0c0c0c;
--text-inverse : #e6e6e6;
--text-inverse-hover : #fff;
--text-inverse-active : #d0d0d0;
--text-red : #f8a2a2;
--text-red-hover : #ffbcbc;
--text-red-active : #e69191;
--link : #3c76ff;
--link-hover : #6594ff;
--link-active : #3064dd;
}
}
@media (prefers-color-scheme: dark) {
:root {
--background-light-3: #403939;
--background-light-2: #322d2d;
--background-light-1: #2b2525;
--background-light : #252020;
--background : #221e1e;
--block-background : ;
--node-background : #221e1e;
--text : #e6e6e6;
--text-hover : #fff;
--text-active : #d0d0d0;
--text-inverse : 'dark';
--red-light-1 : #dc4343;
--red-light : #bf3737;
--red : #a43333;
--red-dark : #8d2a2a;
}
}
:root {
--grey-light: #c0c0c0;
--grey : #858585;
--grey-dark : #565656;
}
* {
text-decoration: none;
outline : none;
border : none;
color : var(--text);
font-family : 'Commissioner', sans-serif;
transition : 0.1s ease-out;
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select : none;
-khtml-user-select : none;
-moz-user-select : none;
-ms-user-select : none;
user-select : none;
}
a {
color: var(--link);
}
a:hover {
color: var(--link-hover);
}
a:active {
color: var(--link-active);
}
body {
height : 100vh;
margin : 0;
overflow : hidden;
background-color: var(--background);
}
aside {
z-index : 500;
grid-column: 1/ 4;
grid-row : 2;
overflow : hidden;
}
header {
z-index : 5000;
position : absolute;
display : flex;
flex-direction: column;
box-shadow : 2px 0 5px rgba(0, 0, 0, 0.3);
}
header>menu {
margin : unset;
padding : 20px;
display : flex;
flex-direction : column;
flex-grow : 1;
background-color: var(--background-light-1);
}
header>#account>button#login {
z-index: 1500;
}
header>menu a {
margin-bottom: 8px;
display : flex;
align-items : center;
}
header>menu a:last-child {
margin-bottom: unset;
}
header>menu a svg {
margin-right: 8px;
height : 1.2rem;
position : relative;
}
header>menu a:hover svg {
margin-left : -5px;
margin-right: 13px;
}
header>menu a svg path {
fill: var(--text);
}
header>section {
background-color: var(--background-light-1);
}
header :is(button, a[type="button"]) {
width : 100%;
height : 40px;
display : flex;
justify-content : center;
align-items : center;
cursor : pointer;
background-color: var(--red);
transition : unset;
}
header button {
font-weight : bold;
text-transform: uppercase;
}
header :is(button, a[type="button"]):hover {
background-color: var(--red-light);
}
header :is(button, a[type="button"]):active {
background-color: var(--red-dark);
}
header>nav {
margin-top : auto;
display : flex;
flex-direction: column;
}
main {
z-index: 1000;
}
footer {
z-index : 3000;
position: absolute;
}

View File

@ -0,0 +1,33 @@
form.upload {
width: 100%;
height: 100px;
position: relative;
display: flex;
border: 4px dashed #e5ddd1;
}
form.upload:hover {
background-color: #ccc6bd;
border: 4px dashed #fff7ea;
}
form.upload>p {
margin: auto;
font-weight: bold;
color: #eee6d9;
}
form.upload:hover>p {
color: #fff7ea;
}
form.upload>input {
width: 100%;
height: 100%;
position: absolute;
opacity: 0;
}
form.upload:hover>input {
cursor: pointer;
}

View File

@ -0,0 +1 @@
<svg height="32" width="32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h32v32H0z"/><path d="M30 17.349V12h-2v-2h2V2H2v8h2v2H2v8h2v2H2v8h15.349A8.97 8.97 0 0 0 23 32a9.002 9.002 0 0 0 9-9 8.968 8.968 0 0 0-2-5.651zM14.059 22H6v-2h8.522a8.932 8.932 0 0 0-.463 2zM26 12H6v-2h20v2zM4 6h4v2H4V6zm0 10h4v2H4v-2zm4 12H4v-2h4v2zm15 1.883A6.898 6.898 0 0 1 16.115 23 6.898 6.898 0 0 1 23 16.115 6.898 6.898 0 0 1 29.883 23 6.898 6.898 0 0 1 23 29.883z"/><path d="m19 25 2 2 2-2 2 2 2-2-2-2 2-2-2-2-2 2-2-2-2 2 2 2z"/></svg>

After

Width:  |  Height:  |  Size: 552 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M0 6c0-1.1.9-2 2-2h16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6zm2 0v2h2V6H2zm1 3v2h2V9H3zm-1 3v2h2v-2H2zm3 0v2h10v-2H5zm11 0v2h2v-2h-2zM6 9v2h2V9H6zm3 0v2h2V9H9zm3 0v2h2V9h-2zm3 0v2h2V9h-2zM5 6v2h2V6H5zm3 0v2h2V6H8zm3 0v2h2V6h-2zm3 0v2h4V6h-4z"/></svg>

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1 @@
<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h256v256H0z"/><path d="M224 177.3V78.7a8.1 8.1 0 0 0-4.1-7l-88-49.5a7.8 7.8 0 0 0-7.8 0l-88 49.5a8.1 8.1 0 0 0-4.1 7v98.6a8.1 8.1 0 0 0 4.1 7l88 49.5a7.8 7.8 0 0 0 7.8 0l88-49.5a8.1 8.1 0 0 0 4.1-7Z" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m222.9 74.6-94 53.4-95.8-53.4M128.9 128l-.9 106.8"/></svg>

After

Width:  |  Height:  |  Size: 537 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m11.136 12.117-.596 2.415c.736.185 3.004.921 3.34-.441.35-1.421-2.009-1.789-2.744-1.974Zm.813-3.297-.54 2.191c.612.154 2.5.784 2.806-.455.318-1.293-1.654-1.581-2.266-1.736ZM12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm4.358 8.575a1.743 1.743 0 0 1-1.385 1.611 1.933 1.933 0 0 1 .997 2.661c-.586 1.692-1.977 1.835-3.827 1.481l-.449 1.82-1.085-.274.443-1.795c-.28-.07-.568-.145-.864-.227l-.445 1.804-1.084-.273.45-1.824c-.254-.065-.511-.135-.774-.201l-1.412-.356.539-1.256s.8.215.788.199a.394.394 0 0 0 .498-.26l1.217-4.939a.583.583 0 0 0-.505-.638c.016-.011-.789-.198-.789-.198l.29-1.172 1.495.378-.001.006c.225.056.457.11.693.164l.444-1.802 1.085.274-.436 1.766c.291.068.584.135.87.207l.432-1.755 1.085.274-.445 1.802c1.37.477 2.372 1.193 2.175 2.523Z"/></svg>

After

Width:  |  Height:  |  Size: 825 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace mirzaev\site;
use mirzaev\minimal\core;
use mirzaev\minimal\router;
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
define('VIEWS', realpath('..' . DIRECTORY_SEPARATOR . 'views'));
define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage'));
define('INDEX', __DIR__);
// Автозагрузка
require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
// Инициализация маршрутазитора
$router = new router;
// Запись маршрутов
$router->write('/', 'index', 'index');
$router->write('/system/hotline', 'hotline', 'index');
$router->write('/system/graph', 'graph', 'index');
$router->write('/account/initialization', 'account', 'initialization', 'PUT');
$router->write('/account/vk/connect', 'account', 'connect');
$router->write('/account/panel', 'account', 'panel');
// Инициализация ядра
$core = new core(namespace: __NAMESPACE__, router: $router);
// Обработка запроса
echo $core->start();

View File

@ -0,0 +1,26 @@
"use strict";
class account {
static async initialization() {
// Запрос
return fetch('https://auth.mirzaev.sexy/account/initialization', {
method: 'GET'
});
}
static authentication() {
// Инициализация аккаунта
alert(1);
this.initialization()
.then(
(response) => {
alert(2);
}
);
return true;
}
static deauthentication() {
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,668 @@
'use strict';
/**
* Бегущая строка
*
* @description
* Простой, но мощный класс для создания бегущих строк. Поддерживает
* перемещение мышью и прокрутку колесом, полностью настраивается очень гибок
* для настроек в CSS и подразумевается, что отлично индексируется поисковыми роботами.
* Имеет свой препроцессор, благодаря которому можно создавать бегущие строки
* без программирования - с помощью HTML-аттрибутов, а так же возможность
* изменять параметры (data-hotline-* аттрибуты) на лету. Есть возможность вызывать
* события при выбранных действиях для того, чтобы пользователь имел возможность
* дорабатывать функционал без изучения и изменения моего кода
*
* @example
* сonst hotline = new hotline();
* hotline.step = '-5';
* hotline.start();
*
* @todo
* 1. Бесконечный режим - элементы не удаляются если видны на экране (будут дубликаты)
*
* @copyright WTFPL
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class hotline {
// Идентификатор
#id = 0;
// Оболочка (instanceof HTMLElement)
#shell = document.getElementById("hotline");
// Инстанция горячей строки
#instance = null;
// Перемещение
#transfer = true;
// Движение
#move = true;
// Наблюдатель
#observer = null;
// Наблюдатель
#block = new Set(["events"]);
// Настраиваемые параметры
transfer = null;
move = null;
delay = 10;
step = 1;
hover = true;
movable = true;
sticky = false;
wheel = false;
delta = null;
vertical = false;
observe = false;
events = new Map([
["start", false],
["stop", false],
["move", false],
["move.block", false],
["move.unblock", false],
["offset", false],
["transfer.start", true],
["transfer.end", true],
["onmousemove", false]
]);
constructor(id, shell) {
// Запись идентификатора
if (typeof id === "string" || typeof id === "number") this.#id = id;
// Запись оболочки
if (shell instanceof HTMLElement) this.#shell = shell;
}
start() {
if (this.#instance === null) {
// Нет запущенной инстанции бегущей строки
// Инициализация ссылки на ядро
const _this = this;
// Запуск движения
this.#instance = setInterval(function () {
if (_this.#shell.childElementCount > 1) {
// Найдено содержимое бегущей строки (2 и более)
// Инициализация буфера для временных данных
let buffer;
// Инициализация данных первого элемента в строке
const first = {
element: (buffer = _this.#shell.firstElementChild),
coords: buffer.getBoundingClientRect()
};
if (_this.vertical) {
// Вертикальная бегущая строка
// Инициализация сдвига у первого элемента (движение)
first.offset = isNaN(
(buffer = parseFloat(first.element.style.marginTop))
)
? 0
: buffer;
// Инициализация отступа до второго элемента у первого элемента (разделение)
first.separator = isNaN(
(buffer = parseFloat(
getComputedStyle(first.element).marginBottom
))
)
? 0
: buffer;
// Инициализация крайнего с конца ребра первого элемента в строке
first.end = first.coords.y + first.coords.height + first.separator;
} else {
// Горизонтальная бегущая строка
// Инициализация отступа у первого элемента (движение)
first.offset = isNaN(
(buffer = parseFloat(first.element.style.marginLeft))
)
? 0
: buffer;
// Инициализация отступа до второго элемента у первого элемента (разделение)
first.separator = isNaN(
(buffer = parseFloat(getComputedStyle(first.element).marginRight))
)
? 0
: buffer;
// Инициализация крайнего с конца ребра первого элемента в строке
first.end = first.coords.x + first.coords.width + first.separator;
}
if (
(_this.vertical &&
Math.round(first.end) < _this.#shell.offsetTop) ||
(!_this.vertical && Math.round(first.end) < _this.#shell.offsetLeft)
) {
// Элемент (вместе с отступом до второго элемента) вышел из области видимости (строки)
if (
(_this.transfer === null && _this.#transfer) ||
_this.transfer === true
) {
// Перенос разрешен
if (_this.vertical) {
// Вертикальная бегущая строка
// Удаление отступов (движения)
first.element.style.marginTop = null;
} else {
// Горизонтальная бегущая строка
// Удаление отступов (движения)
first.element.style.marginLeft = null;
}
// Копирование первого элемента в конец строки
_this.#shell.appendChild(first.element);
if (_this.events.get("transfer.end")) {
// Запрошен вызов события: "перемещение в конец"
// Вызов события: "перемещение в конец"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.transfer.end`, {
detail: {
element: first.element,
offset: -(
(_this.vertical
? first.coords.height
: first.coords.width) + first.separator
)
}
})
);
}
}
} else if (
(_this.vertical &&
Math.round(first.coords.y) > _this.#shell.offsetTop) ||
(!_this.vertical &&
Math.round(first.coords.x) > _this.#shell.offsetLeft)
) {
// Передняя (движущая) граница первого элемента вышла из области видимости
if (
(_this.transfer === null && _this.#transfer) ||
_this.transfer === true
) {
// Перенос разрешен
// Инициализация отступа у последнего элемента (разделение)
const separator =
(buffer = isNaN(
(buffer = parseFloat(
getComputedStyle(_this.#shell.lastElementChild)[
_this.vertical ? "marginBottom" : "marginRight"
]
))
)
? 0
: buffer) === 0
? first.separator
: buffer;
// Инициализация координат первого элемента в строке
const coords = _this.#shell.lastElementChild.getBoundingClientRect();
if (_this.vertical) {
// Вертикальная бегущая строка
// Удаление отступов (движения)
_this.#shell.lastElementChild.style.marginTop =
-coords.height - separator + "px";
} else {
// Горизонтальная бегущая строка
// Удаление отступов (движения)
_this.#shell.lastElementChild.style.marginLeft =
-coords.width - separator + "px";
}
// Копирование последнего элемента в начало строки
_this.#shell.insertBefore(
_this.#shell.lastElementChild,
first.element
);
// Удаление отступов у второго элемента в строке (движения)
_this.#shell.children[1].style[
_this.vertical ? "marginTop" : "marginLeft"
] = null;
if (_this.events.get("transfer.start")) {
// Запрошен вызов события: "перемещение в начало"
// Вызов события: "перемещение в начало"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.transfer.start`, {
detail: {
element: _this.#shell.lastElementChild,
offset:
(_this.vertical ? coords.height : coords.width) +
separator
}
})
);
}
}
} else {
// Элемент в области видимости
if ((_this.move === null && _this.#move) || _this.move === true) {
// Движение разрешено
// Запись новых координат сдвига
const offset = first.offset + _this.step;
// Запись сдвига (движение)
_this.offset(offset);
if (_this.events.get("move")) {
// Запрошен вызов события: "движение"
// Вызов события: "движение"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move`, {
detail: {
from: first.offset,
to: offset
}
})
);
}
}
}
}
}, _this.delay);
if (this.hover) {
// Запрошена возможность останавливать бегущую строку
// Инициализация сдвига
let offset = 0;
// Инициализация слушателя события при перемещении элемента в бегущей строке
const listener = function (e) {
// Увеличение сдвига
offset += e.detail.offset ?? 0;
};
// Инициализация обработчика наведения курсора (остановка движения)
this.#shell.onmouseover = function (e) {
// Курсор наведён на бегущую строку
// Блокировка движения
_this.#move = false;
if (_this.events.get("move.block")) {
// Запрошен вызов события: "блокировка движения"
// Вызов события: "блокировка движения"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move.block`)
);
}
if (_this.movable) {
// Запрошена возможность двигать бегущую строку
_this.#shell.onmousedown = function (onmousedown) {
// Курсор активирован
// Инициализация слушателей события перемещения элемента в бегущей строке
document.addEventListener(
`hotline.${_this.#id}.transfer.start`,
listener
);
document.addEventListener(
`hotline.${_this.#id}.transfer.end`,
listener
);
// Инициализация буфера для временных данных
let buffer;
// Инициализация данных первого элемента в строке
const first = {
offset: isNaN(
(buffer = parseFloat(
_this.vertical
? _this.#shell.firstElementChild.style.marginTop
: _this.#shell.firstElementChild.style.marginLeft
))
)
? 0
: buffer
};
document.onmousemove = function (onmousemove) {
// Курсор движется
if (_this.vertical) {
// Вертикальная бегущая строка
// Инициализация буфера местоположения
const from = _this.#shell.firstElementChild.style.marginTop;
const to = onmousemove.pageY - (onmousedown.pageY + offset - first.offset);
// Движение
_this.#shell.firstElementChild.style.marginTop = to +
"px";
if (_this.events.get("onmousemove")) {
// Запрошен вызов события: "перемещение мышью"
// Вызов события: "перемещение мышью"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.onmousemove`, {
detail: { from, to }
})
);
}
} else {
// Горизонтальная бегущая строка
// Инициализация буфера местоположения
const from = _this.#shell.firstElementChild.style.marginLeft;
const to = onmousemove.pageX - (onmousedown.pageX + offset - first.offset);
// Движение
_this.#shell.firstElementChild.style.marginLeft = to + "px";
if (_this.events.get("onmousemove")) {
// Запрошен вызов события: "перемещение мышью"
// Вызов события: "перемещение мышью"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.onmousemove`, {
detail: { from, to }
})
);
}
}
// Запись курсора
_this.#shell.style.cursor = "grabbing";
};
};
// Перещапись событий браузера (чтобы не дёргалось)
_this.#shell.ondragstart = null;
_this.#shell.onmouseup = function () {
// Курсор деактивирован
// Остановка обработки движения
document.onmousemove = null;
// Сброс сдвига
offset = 0;
document.removeEventListener(
`hotline.${_this.#id}.transfer.start`,
listener
);
document.removeEventListener(
`hotline.${_this.#id}.transfer.end`,
listener
);
// Восстановление курсора
_this.#shell.style.cursor = null;
};
}
};
// Инициализация обработчика отведения курсора (остановка движения)
this.#shell.onmouseleave = function (onmouseleave) {
// Курсор отведён от бегущей строки
if (!_this.sticky) {
// Отключено прилипание
// Остановка обработки движения
document.onmousemove = null;
document.removeEventListener(
`hotline.${_this.#id}.transfer.start`,
listener
);
document.removeEventListener(
`hotline.${_this.#id}.transfer.end`,
listener
);
// Восстановление курсора
_this.#shell.style.cursor = null;
}
// Сброс сдвига
offset = 0;
// Разблокировка движения
_this.#move = true;
if (_this.events.get("move.unblock")) {
// Запрошен вызов события: "разблокировка движения"
// Вызов события: "разблокировка движения"
document.dispatchEvent(
new CustomEvent(`hotline.${_this.#id}.move.unblock`)
);
}
};
}
if (this.wheel) {
// Запрошена возможность прокручивать колесом мыши
// Инициализация обработчика наведения курсора (остановка движения)
this.#shell.onwheel = function (e) {
// Курсор наведён на бегущую
// Инициализация буфера для временных данных
let buffer;
// Перемещение
_this.offset(
(isNaN(
(buffer = parseFloat(
_this.#shell.firstElementChild.style[
_this.vertical ? "marginTop" : "marginLeft"
]
))
)
? 0
: buffer) +
(_this.delta === null
? e.wheelDelta
: e.wheelDelta > 0
? _this.delta
: -_this.delta)
);
};
}
}
if (this.observe) {
// Запрошено наблюдение за изменениями аттрибутов элемента бегущей строки
if (this.#observer === null) {
// Отсутствует наблюдатель
// Инициализация ссылки на ядро
const _this = this;
// Инициализация наблюдателя
this.#observer = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
if (mutation.type === "attributes") {
// Запись параметра в инстанцию бегущей строки
_this.write(mutation.attributeName);
}
}
// Перезапуск бегущей строки
_this.restart();
});
// Активация наблюдения
this.#observer.observe(this.#shell, {
attributes: true
});
}
} else if (this.#observer instanceof MutationObserver) {
// Запрошено отключение наблюдения
// Деактивация наблюдения
this.#observer.disconnect();
// Удаление наблюдателя
this.#observer = null;
}
if (this.events.get("start")) {
// Запрошен вызов события: "запуск"
// Вызов события: "запуск"
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.start`));
}
}
stop() {
// Остановка бегущей строки
clearInterval(this.#instance);
// Удаление инстанции интервала
this.#instance = null;
if (this.events.get("stop")) {
// Запрошен вызов события: "остановка"
// Вызов события: "остановка"
document.dispatchEvent(new CustomEvent(`hotline.${this.#id}.stop`));
}
}
restart() {
// Остановка бегущей строки
this.stop();
// Запуск бегущей строки
this.start();
}
write(attribute) {
// Инициализация названия параметра
const parameter = (/^data-hotline-(\w+)$/.exec(attribute) ?? [, null])[1];
if (typeof parameter === "string") {
// Параметр найден
// Проверка на разрешение изменения
if (this.#block.has(parameter)) return;
// Инициализация значения параметра
const value = this.#shell.getAttribute(attribute);
// Инициализация буфера для временных данных
let buffer;
// Запись параметра
this[parameter] = isNaN((buffer = parseFloat(value)))
? value === "true"
? true
: value === "false"
? false
: value
: buffer;
}
}
offset(value) {
// Запись отступа
this.#shell.firstElementChild.style[
this.vertical ? "marginTop" : "marginLeft"
] = value + "px";
if (this.events.get("offset")) {
// Запрошен вызов события: "сдвиг"
// Вызов события: "сдвиг"
document.dispatchEvent(
new CustomEvent(`hotline.${this.#id}.offset`, {
detail: {
to: value
}
})
);
}
}
static preprocessing(event = false) {
// Инициализация счётчиков инстанций горячей строки
const success = new Set();
let error = 0;
for (const element of document.querySelectorAll('*[data-hotline="true"]')) {
// Перебор бегущих строк
if (typeof element.id === "string") {
// Найден идентификатор
// Инициализация инстанции бегущей строки
const hotline = new this(element.id, element);
for (const attribute of element.getAttributeNames()) {
// Перебор аттрибутов
// Запись параметра в инстанцию бегущей строки
hotline.write(attribute);
}
// Запуск бегущей строки
hotline.start();
// Запись инстанции бегущей строки в элемент
element.hotline = hotline;
// Запись в счётчик успешных инициализаций
success.add(hotline);
} else ++error;
}
if (event) {
// Запрошен вызов события: "предварительная подготовка"
// Вызов события: "предварительная подготовка"
document.dispatchEvent(
new CustomEvent(`hotline.preprocessed`, {
detail: {
success,
error
}
})
);
}
}
}
document.dispatchEvent(
new CustomEvent("hotline.loaded", {
detail: { hotline }
})
);

View File

@ -0,0 +1,2 @@
/*! js-cookie v3.0.1 | MIT */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})}));

View File

@ -0,0 +1,127 @@
"use strict";
class troller {
static what = {
enable() {
document.body.onmouseleave = function () {
// if (Math.random() > 0.90) {
// 10%
troller.what.start();
// }
};
document.body.onmouseenter = function () {
troller.what.end();
};
},
disable() {
document.body.onmouseleave = document.body.onmouseenter = undefined;
},
start() {
// Отображение изображения
document.getElementById('what_image').classList.add('active');
// Инициализация элемента со звуком
const what_sound = document.getElementById('what_sound');
// Воспроизведение звука
what_sound.currentTime = 0;
what_sound.play();
},
end() {
// Сокрытие изображения
document.getElementById('what_image').classList.remove('active');
// Остановка звука
document.getElementById('what_sound').pause();
},
single(event = 'onmouseleave') {
if (typeof event === 'string') {
// Получены обязательные входные параметры
// Отображение изображения
document.getElementById('what_image').classList.add('active');
// Инициализация элемента со звуком
const what_sound = document.getElementById('what_sound');
// Воспроизведение звука
what_sound.currentTime = 0;
what_sound.play();
document.body[event] = function () {
troller.what.end();
document.body[event] = undefined;
};
}
}
}
static vk() {
setInterval(function () {
const sound = document.getElementById('sound_vk');
if (Math.random() > 0.95) {
// 5%
// Воспроизведение звука
sound.currentTime = 0;
sound.play();
}
}, 85000);
}
static whatsapp() {
setInterval(function () {
const sound = document.getElementById('sound_whatsup');
if (Math.random() > 0.97) {
// 3%
// Воспроизведение звука
sound.currentTime = 0;
sound.play();
}
}, 125000);
}
static iphone() {
setInterval(function () {
const sound = document.getElementById('sound_iphone');
if (Math.random() > 0.98) {
// 2%
// Воспроизведение звука
sound.currentTime = 0;
sound.play();
}
}, 265000);
}
}
if (Math.random() > 0.90) {
// 10%
troller.what.enable();
}
if (Math.random() > 0.90) {
// 10%
troller.vk();
}
if (Math.random() > 0.90) {
// 10%
troller.whatsapp();
}
if (Math.random() > 0.90) {
// 10%
troller.iphone();
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1 @@
arangodb.php

View File

@ -0,0 +1,8 @@
<?php
return [
'endpoint' => 'unix:///var/run/arangodb3/arango.sock',
'database' => '',
'name' => '',
'password' => ''
];

View File

@ -0,0 +1,21 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/account.css">
<link type="text/css" rel="stylesheet" href="/css/icon_authentication.css">
{% endblock %}
{% block body %}
<section id="account">
{% if account %}
{{ account.getKey() }}
{% if vk %}
{{ vk.mail }}
{% endif %}
{% else %}
<button id="login" title="Войти в аккаунт" onclick="return account.authentication()"><i class='icon authentication'></i></button>
{% endif %}
</section>
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/account.js"></script>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% use 'account/element.html' with css as account_css, body as account_body, js as account_js %}
{% block css %}
{{ block('account_css') }}
{% endblock %}
{% block body %}
<aside>
{{ block('account_body') }}
</aside>
{% endblock %}
{% block js %}
{{ block('account_js') }}
{% endblock %}
{% block js_init %}
{% endblock %}

View File

@ -0,0 +1,33 @@
<!doctype html>
<html lang="ru">
<head>
{% use 'head.html' with title as head_title, meta as head_meta, css as head_css %}
{% block title %}
{{ block('head_title') }}
{% endblock %}
{% block meta %}
{{ block('head_meta') }}
{% endblock %}
{% block css %}
{{ block('head_css') }}
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
{% block js %}
{% include 'js.html' %}
{% endblock %}
{% block js_init %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,4 @@
<footer>
<!-- <p><a href="http://www.anybrowser.org/campaign/"><img src="/img/logos/any_browser.gif" width="278" height="44" alt="Доступно на любом браузере" /></a></p> -->
<!-- <p><a href="/browsers"><img src="/img/logos/any_browser.gif" width="278" height="44" alt="Доступно на любом браузере" /></a></p> -->
</footer>

View File

@ -0,0 +1,86 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/graph.css">
<link type="text/css" rel="stylesheet" href="/css/icon_close.css" />
{% endblock %}
{% block main %}
{% if graph.id != empty %}
<section id="{{ graph.id }}" class="graph unselectable" {% for name, value in graph.attributes %} {{ name }}="{{value}}"
{% endfor %}>
{% for element in graph.elements %}
<{{element.tag??'article'}}>{{ element.content }}</{{element.tag??'article'}}>
{% endfor %}
</section>
{% endif %}
{% endblock %}
{% block js %}
<script type="module" src="/js/victor.js" defer></script>
<script type="module" src="/js/graph.js" defer></script>
{% endblock %}
{% block js_init %}
<script>
document.addEventListener('graph.loaded', function (e) {
// Инициализация графика
{% if graph.id != empty %}
const core = new e.detail.graph(document.getElementById('{{ graph.id }}'));
core.write({
title: 'бебра'
});
const mirzaev = core.write({
title: 'Арсен Мирзаев',
description: ' абабабаба абабабабаабабабабаабабабабаабабабаба абабабаба абабабаба абабабабаабабабаба абабабаба абабабаба абабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабабабабаба абабабабаабабаба',
cover: 'https://sun9-east.userapi.com/sun9-27/s/v1/ig2/qOBvWvsDwPmMjOTqQbl0TuGHaoMwWQhhd81nxD847v32dT-pyYa9kxw2MY7moQBVBoN4iVLnUZx6WmE4x4HnIwAu.jpg?size=810x1080&quality=95&type=album',
link: {
name: 'Арсен Мирзаев',
title: 'Читать статью полностью',
href: 'https://google.com',
class: ['source']
},
popup: 'Для подробной информации читайте статью полностью',
color: 'red'
});
const berbi = core.write({
title: 'берби'
});
const anarchy = core.write({
title: 'анархия'
});
core.connect(
berbi,
mirzaev);
core.connect(
anarchy,
mirzaev);
core.connect(
core.write({
title: 'бабы'
}),
mirzaev);
core.connect(
core.write({
title: 'Ксения Велькович',
description: 'А меня вписать в кружочек?',
cover: 'https://storage.mirzaev.sexy/2022/mirzaev.sexy/nodes/ksenia_velkovich.jpg',
link: {
name: 'Ксения Велькович',
title: 'Страница ВКонтакте',
href: 'https://vk.com/id720261644',
class: ['source']
}
}),
mirzaev);
core.connect(
core.write({
title: 'чокопайки'
}),
mirzaev);
core.connect(
anarchy,
berbi);
{% endif %}
});
</script>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% block title %}
<title>{% if head.title != empty %}{{head.title}}{% else %}Мирзаев{% endif %}</title>
{% endblock %}
{% block meta %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% for meta in head.metas %}
<meta {% for name, value in meta.attributes %}{{name}}="{{value}}" {% endfor %}>
{% endfor %}
{% endblock %}
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/main.css" />
{% endblock %}

View File

@ -0,0 +1,13 @@
{% block css %}
{% endblock %}
{% block body %}
<header>
</header>
{% endblock %}
{% block js %}
{% endblock %}
{% block js_init %}
{% endblock %}

View File

@ -0,0 +1,28 @@
{% block css %}
<link type="text/css" rel="stylesheet" href="/css/hotline.css">
{% endblock %}
{% block body %}
{% if hotline.id != empty %}
<section id="{{ hotline.id }}" class="hotline unselectable" data-hotline="true" {% for name, value in hotline.parameters
%} data-hotline-{{ name }}="{{value}}" {% endfor %} {% for name, value in hotline.attributes %} {{ name
}}="{{value}}" {% endfor %}>
{% for element in hotline.elements %}
<{{element.tag??'article'}}>{{ element.content }}</{{element.tag??'article'}}>
{% endfor %}
</section>
{% endif %}
{% endblock %}
{% block js %}
<script type="text/javascript" src="/js/hotline.js" defer></script>
{% endblock %}
{% block js_init %}
<script>
document.addEventListener('hotline.loaded', function (e) {
// Запуск препроцессора бегущих строк
e.detail.hotline.preprocessing();
});
</script>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends "core.html" %}
{% use "core.html" with css as core_css, body as core_body, js as core_js, js_init as core_js_init %}
{% use "header.html" with css as header_css, body as header_body, js as header_js, js_init as header_js_init %}
{% use "aside.html" with css as aside_css, body as aside_body, js as aside_js, js_init as aside_js_init %}
{% use 'graph/index.html' with css as graph_css, main as graph_main, js as graph_js, js_init as graph_js_init %}
{% block css %}
{{ block('core_css') }}
{{ block('header_css') }}
{{ block('aside_css') }}
{{ block('graph_css') }}
{% endblock %}
{% block body %}
{{ block('core_body') }}
{{ block('aside_body') }}
{{ block('header_body') }}
<main>
<noscript>К сожалению мой сайт ещё пока не готов для работы без javascript</noscript>
{% block main %}
{{ block('graph_main') }}
{% endblock %}
</main>
{# {% include 'footer.html' %} #}
{% endblock %}
{% block js %}
{{ block('core_js') }}
{{ block('header_js') }}
{{ block('aside_js') }}
{{ block('graph_js') }}
{% endblock %}
{% block js_init %}
{{ block('core_js_init') }}
{{ block('header_js_init') }}
{{ block('aside_js_init') }}
{{ block('graph_js_init') }}
{% endblock %}

View File

@ -0,0 +1,3 @@
{% block js %}
<script type="text/javascript" src="/js/js.cookie.min.js" defer></script>
{% endblock %}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace mirzaev\site\mirzaev\views;
use mirzaev\minimal\controller;
use Twig\Loader\FilesystemLoader;
use Twig\Environment as view;
/**
* Менеджер представлений
*
* @package mirzaev\site\mirzaev\controllers
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
final class manager extends controller
{
public function render(string $file, array $vars = []): ?string
{
// Генерация представления
return (new view(new FilesystemLoader(VIEWS)))->render($file, $vars);
}
}

File diff suppressed because one or more lines are too long