29 Commits

Author SHA1 Message Date
907a41e057 Merge pull request 'Версия 0.3.0' (#3) from develop into main
Reviewed-on: #3
2024-07-27 02:09:13 +05:00
3902f5d36f my-projects-website:0.3.0. 2024-07-27 01:40:45 +05:00
46da639055 Added the ability to send notifications by email when a new review is added. 2024-07-27 00:29:28 +05:00
c84ed9f12b Configured the removal of old files that were not attached to the model or were marked as deleted. 2024-07-26 22:18:01 +05:00
dde792b97a Made it possible to disable captcha. 2024-07-26 22:16:34 +05:00
618c925dfa Fixed an issue with displaying the translation of site.Menu if the project language did not match the system language. 2024-07-26 22:15:33 +05:00
8bc4c0d8d0 Made it possible to disable captcha. 2024-07-26 22:13:14 +05:00
a5e3c5ed25 Fixed category display error from documentation. Previously, I showed all categories of documentation. 2024-07-26 22:11:54 +05:00
22bb840705 Added prism-yaml. 2024-07-26 22:06:38 +05:00
707762d29b Site. Fixed an error in javascript when the page does not have a tag with the .documentation-version__button class. 2024-07-26 22:06:21 +05:00
b33362a235 Added the ability to upload pictures in the editor. 2024-07-26 22:04:39 +05:00
4d36821ecc Fixed UNIT_SOURCE. 2024-06-12 00:43:36 +05:00
417ce35fc8 Merge pull request 'Версия 0.2.1' (#2) from develop into main
Reviewed-on: #2
2024-06-12 00:03:10 +05:00
da5201dd9a Updated to korelf/my-projects-website:0.2.1. 2024-06-12 00:01:01 +05:00
2910509218 Added environment UNIT_SOURCE and redis volumes. 2024-06-11 23:44:51 +05:00
74f7b88bce Improved docker-entrypoint_*.sh scripts. 2024-06-11 23:43:37 +05:00
6998424b78 Added ignore .env.testing. 2024-06-11 23:42:54 +05:00
05f566c115 Added RoleSyncPermissionsCommandHandlerException. 2024-06-11 23:40:45 +05:00
c5fc106761 Added autocomplete="off" attribute to input password. 2024-06-11 22:39:39 +05:00
d7dc80cca4 Deleted sweetalert2. 2024-06-11 22:35:04 +05:00
6d3891979c Removed extra code. 2024-06-11 22:34:44 +05:00
f986e0396d Merge branch 'develop' 2024-05-19 21:02:48 +05:00
9f6924935d Fixed .dockerignore. 2024-05-19 21:01:04 +05:00
e74456ee2f A documentation section has been added to the site. 2024-05-19 19:47:02 +05:00
42701a24db Fixed an error when saving a project when there is no language data. 2024-05-18 22:58:05 +05:00
78e6a1e396 Added a check for accessibility permission to the project. 2024-05-18 20:18:41 +05:00
b6e1c50486 I fixed this so that prismjs doesn't overlap the menu in the mobile version. 2024-05-17 23:39:26 +05:00
3bc508b8b0 I fixed it so that prismjs would not overlap when selecting another language. 2024-05-17 23:27:18 +05:00
4b6cf902ff Merge pull request 'Версия 0.1.0' (#1) from develop into main
Reviewed-on: #1
2024-04-24 21:56:46 +05:00
107 changed files with 3310 additions and 437 deletions

14
app/.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
**/.env
**/*.env
**/.env.example
**/storage/app/*
**/storage/debugbar
**/storage/framework/cache/*
**/storage/framework/sessions/*
**/storage/framework/views/*
**/storage/framework/testing/*
**/storage/logs/*
**/vendor/
**/node_modules/
**/public/build/
**/public/storage

View File

@@ -1,17 +1,21 @@
APP_NAME=Laravel APP_NAME="My Projects Website"
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://localhost APP_URL=http://localhost
APP_CAPTCHA=false
CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081 CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081
CAPTCHA_PRIVATE_TOKEN= CAPTCHA_PRIVATE_TOKEN=
CAPTCHA_STATIC_PATH=http://your-domain-captcha-or-IP:8081/captcha CAPTCHA_STATIC_PATH=http://your-domain-captcha-or-IP:8081/captcha
CAPTCHA_PUBLIC_TOKEN= CAPTCHA_PUBLIC_TOKEN=
# Don't forget to configure MAIL_MAILER to send mail.
FEEDBACK_MAIL_NOTIFICATIONS=false
FEEDBACK_MAIL_TO=
APP_FORCE_HTTPS=false APP_FORCE_HTTPS=false
#UNIT_SOURCE="\"172.16.0.0/12\""
APP_DEFAULT_LOCALE=ru APP_DEFAULT_LOCALE=ru
APP_FAKER_LOCALE=ru_RU APP_FAKER_LOCALE=ru_RU
@@ -59,7 +63,7 @@ REDIS_HOST=app-redis
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=log MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null

View File

@@ -8,6 +8,7 @@
.env .env
.env.backup .env.backup
.env.production .env.production
.env.testing
.phpunit.result.cache .phpunit.result.cache
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\Console\Commands\Files;
use App\Services\Commands\DeleteOldFilesService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
final class DeleteOldFilesFromStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'files:delete-old-files-from-storage';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove temporary files or files that have been deleted from the storage table';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle(DeleteOldFilesService $deleteOldFilesService): void
{
$temporaryBeforeDate = Carbon::now()->subDays(3);
$deletedBeforeDate = Carbon::now()->subDays(7);
$result = $deleteOldFilesService->fromStorage($temporaryBeforeDate, $deletedBeforeDate);
if ($result->isError()) {
$this->error($result->getMessage());
return;
}
$this->info($result->getMessage());
}
}

View File

@@ -2,14 +2,22 @@
namespace App\Dto\Builder; namespace App\Dto\Builder;
use App\Dto\Builder\DocumentationCategory\Category;
final readonly class Documentation final readonly class Documentation
{ {
public function __construct( public function __construct(
private ?bool $isPublic = null, private ?bool $isPublic = null,
private ?Category $categoryId = null,
) { } ) { }
public function isPublic(): ?bool public function isPublic(): ?bool
{ {
return $this->isPublic; return $this->isPublic;
} }
public function getCategoryId(): ?Category
{
return $this->categoryId;
}
} }

View File

@@ -2,14 +2,22 @@
namespace App\Dto\Builder; namespace App\Dto\Builder;
use App\Dto\Builder\DocumentationCategory\Category;
final readonly class DocumentationCategory final readonly class DocumentationCategory
{ {
public function __construct( public function __construct(
private ?bool $isPublic = null, private ?bool $isPublic = null,
private ?Category $parentId = null,
) { } ) { }
public function isPublic(): ?bool public function isPublic(): ?bool
{ {
return $this->isPublic; return $this->isPublic;
} }
public function getParentId(): ?Category
{
return $this->parentId;
}
} }

View File

@@ -0,0 +1,20 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder\DocumentationCategory;
final readonly class Category
{
public function __construct(
private ?int $categoryId,
) { }
public function getCategoryId(): ?int
{
return $this->categoryId;
}
public function isCategoryNull(): bool
{
return $this->getCategoryId() === null;
}
}

View File

@@ -3,12 +3,14 @@
namespace App\Dto\Service\Admin\Project\About; namespace App\Dto\Service\Admin\Project\About;
use App\Dto\Service\Dto; use App\Dto\Service\Dto;
use App\Dto\Service\Storage\Storages;
final readonly class StoreUpdate extends Dto final readonly class StoreUpdate extends Dto
{ {
public function __construct( public function __construct(
private string $title, private string $title,
private string $description, private string $description,
private Storages $storages,
) { } ) { }
public function getTitle(): string public function getTitle(): string
@@ -20,4 +22,9 @@ final readonly class StoreUpdate extends Dto
{ {
return $this->description; return $this->description;
} }
public function getStorages(): Storages
{
return $this->storages;
}
} }

View File

@@ -2,12 +2,15 @@
namespace App\Dto\Service\Admin\Project\DocumentationContent; namespace App\Dto\Service\Admin\Project\DocumentationContent;
use App\Dto\Service\Storage\Storages;
final readonly class Content final readonly class Content
{ {
public function __construct( public function __construct(
private int $languageId, private int $languageId,
private string $title, private string $title,
private string $content, private string $content,
private Storages $storages,
) { } ) { }
public function getLanguageId(): int public function getLanguageId(): int
@@ -24,4 +27,9 @@ final readonly class Content
{ {
return $this->content; return $this->content;
} }
public function getStorages(): Storages
{
return $this->storages;
}
} }

View File

@@ -2,6 +2,7 @@
namespace App\Dto\Service\Admin\Project\Translation; namespace App\Dto\Service\Admin\Project\Translation;
use App\Enums\DocumentationVersionStatus;
use App\Exceptions\Dto\Admin\Project\Transaction\TranslationsException; use App\Exceptions\Dto\Admin\Project\Transaction\TranslationsException;
final class Translations final class Translations
@@ -23,7 +24,7 @@ final class Translations
public static function getTranslationCodes(): array public static function getTranslationCodes(): array
{ {
return [ $translations = [
'site.Menu', 'site.Menu',
'site.Powered by service', 'site.Powered by service',
'site.About project', 'site.About project',
@@ -38,6 +39,17 @@ final class Translations
'site.attributes.message', 'site.attributes.message',
'site.Message sent successfully', 'site.Message sent successfully',
'Server Error', 'Server Error',
'site.Documentation',
'site.Documentation not created',
'site.Choose version',
'site.alert-status-not-supported',
'site.alert-status-future',
]; ];
foreach (DocumentationVersionStatus::cases() as $status) {
$translations[] = $status->getCodeForTranslation();
}
return $translations;
} }
} }

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\DocumentationContent;
use App\Dto\Service\Storage\Storages;
use App\Models\DocumentationContent;
final class StorageDto
{
/**
* @var array [][DocumentationContent documentationContent, Storages storages]
*/
private array $storages = [];
public function add(DocumentationContent $documentationContent, Storages $storages): void
{
$this->storages[] = [
'documentationContent' => $documentationContent,
'storages' => $storages
];
}
/**
* @return array [][DocumentationContent documentationContent, Storages storages]
*/
public function getStorages(): array
{
return $this->storages;
}
}

View File

@@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Site;
use App\Models\DocumentationVersion;
use App\Models\Project;
use App\Models\User;
use App\Services\WebsiteTranslations;
final readonly class Documentation
{
public function __construct(
private Project $project,
private DocumentationVersion $version,
private WebsiteTranslations $websiteTranslations,
private ?User $user = null,
) { }
public function getProject(): Project
{
return $this->project;
}
public function getVersion(): DocumentationVersion
{
return $this->version;
}
public function getWebsiteTranslations(): WebsiteTranslations
{
return $this->websiteTranslations;
}
public function getUser(): ?User
{
return $this->user;
}
public function toArray(): array
{
return [
'project' => $this->getProject(),
'version' => $this->getVersion(),
'websiteTranslations' => $this->getWebsiteTranslations(),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\View\Volt\Form;
use App\Enums\Morph;
use App\Helpers\Helpers;
final readonly class WysiwygStorageUpload
{
public function __construct(
private string $inputName,
private Morph $morph,
) { }
public function getInputName(): string
{
return $this->inputName;
}
public function getRequestInputName(): string
{
return Helpers::formatAttributeNameToRequestName($this->getInputName());
}
public function getMorph(): Morph
{
return $this->morph;
}
}

View File

@@ -9,6 +9,7 @@ enum CacheTag: string
{ {
case Project = 'project'; case Project = 'project';
case ProjectTranslation = 'project_translation'; case ProjectTranslation = 'project_translation';
case DocumantationVersion = 'documantation_version';
public function getCache(): TaggedCache public function getCache(): TaggedCache
{ {

View File

@@ -2,6 +2,7 @@
namespace App\Enums; namespace App\Enums;
use App\Services\WebsiteTranslations;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
enum DocumentationVersionStatus: int enum DocumentationVersionStatus: int
@@ -11,9 +12,18 @@ enum DocumentationVersionStatus: int
case CurrentVersion = 100; case CurrentVersion = 100;
case FutureVersion = 150; case FutureVersion = 150;
public function getTitle(): string public function getTitle(?WebsiteTranslations $websiteTranslations = null): string
{ {
return __('version-status.' . $this->name); if (\is_null($websiteTranslations)) {
return __($this->getCodeForTranslation());
}
return $websiteTranslations->translate($this->getCodeForTranslation());
}
public function getCodeForTranslation(): string
{
return 'version-status.' . $this->name;
} }
public static function toArray(): array public static function toArray(): array

View File

@@ -2,16 +2,22 @@
namespace App\Enums; namespace App\Enums;
use App\Models\DocumentationContent;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectContent;
enum Morph: int enum Morph: int
{ {
case Project = 1; case Project = 1;
case DocumentationContent = 2;
case ProjectContent = 3;
public function getPathModel(): string public function getPathModel(): string
{ {
return match ($this) { return match ($this) {
self::Project => Project::class, self::Project => Project::class,
self::DocumentationContent => DocumentationContent::class,
self::ProjectContent => ProjectContent::class,
}; };
} }

View File

@@ -5,15 +5,18 @@ namespace App\Enums\Site;
use App\Models\Project; use App\Models\Project;
use App\Models\ProjectLanguage; use App\Models\ProjectLanguage;
enum ProjectSection enum ProjectSection: string
{ {
case Home; case Home = 'home';
case Feedback; case Feedback = 'feedback';
case FeedbackSend; case FeedbackSend = 'feedback.send';
case Documentation = 'documentation';
case DocumentationVersion = 'documentation.version';
case DocumentationCategory = 'documentation.category';
case DocumentationView = 'documentation.view';
public function url(Project $project, ?ProjectLanguage $language = null): string public function url(Project $project, ?ProjectLanguage $language = null, array $parameters = []): string
{ {
$parameters = [];
$prefixProject = ''; $prefixProject = '';
if ($project->http_host === null) { if ($project->http_host === null) {
$prefixProject = 'project.'; $prefixProject = 'project.';
@@ -26,12 +29,7 @@ enum ProjectSection
$prefixLanguage = '-language'; $prefixLanguage = '-language';
} }
$route = match ($this) { $route = \route($prefixProject . $this->value . $prefixLanguage, $parameters, false);
self::Home => \route($prefixProject . 'home' . $prefixLanguage, $parameters, false),
self::Feedback => \route($prefixProject . 'feedback' . $prefixLanguage, $parameters, false),
self::FeedbackSend => \route($prefixProject . 'feedback.send' . $prefixLanguage, $parameters, false),
};
return $project->http_host . $route; return $project->http_host . $route;
} }

View File

@@ -9,39 +9,43 @@ use App\Contracts\StorageType\Video;
enum StorageType: int implements Image, Video, Audio enum StorageType: int implements Image, Video, Audio
{ {
case Logo = 1; case Logo = 1;
case ContentImages = 2;
public function getTitle(): string public function getTitle(): string
{ {
return match ($this) { return match ($this) {
self::Logo => __('validation.attributes.logo'), self::Logo => __('validation.attributes.logo'),
self::ContentImages => __('validation.attributes.content_images'),
}; };
} }
public function getAcceptMimes(): array public function getAcceptMimes(): array
{ {
return match ($this) { return match ($this) {
self::Logo => ['jpeg', 'jpg', 'png'], self::Logo => ['jpeg', 'jpg', 'png'],
self::ContentImages => ['jpeg', 'jpg', 'png'],
}; };
} }
public function isImage(): bool public function isImage(): bool
{ {
return match ($this) { return match ($this) {
self::Logo => true, self::Logo => true,
self::ContentImages => true,
default => false default => false
}; };
} }
public function isVideo(): bool public function isVideo(): bool
{ {
return match ($this->name) { return match ($this) {
default => false default => false
}; };
} }
public function isAudio(): bool public function isAudio(): bool
{ {
return match ($this->name) { return match ($this) {
default => false default => false
}; };
} }

View File

@@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Services\DocumentationContent;
use App\ServiceResults\ServiceResultError;
final class StorageCommandException extends \Exception
{
public function __construct(private readonly ServiceResultError $resultError, string $message = "", int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
public function getResultError(): ServiceResultError
{
return $this->resultError;
}
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Services\Rule;
final class RoleSyncPermissionsCommandHandlerException extends \Exception
{
}

View File

@@ -19,7 +19,9 @@ final class AuthController extends Controller
public function login(): View public function login(): View
{ {
return view('login'); return view('login', [
'captcha' => config('app.captcha', false),
]);
} }
public function authorization(AuthorizationRequest $request): RedirectResponse public function authorization(AuthorizationRequest $request): RedirectResponse

View File

@@ -0,0 +1,96 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use App\Dto\Service\Site\Documentation;
use App\Services\Site\DocumentationService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class DocumentationController extends Controller
{
public function __construct(
private readonly DocumentationService $documentationService,
) { }
public function defaultVersion(Request $request): RedirectResponse | View
{
$project = $request->get('project');
$websiteTranslations = $request->get('websiteTranslations');
$result = $this->documentationService->defaultVersion($project, $request->user());
if ($result->isError()) {
if ($result->getCode() === 404) {
return view('site.projects.documentation.no-default-version', [
'project' => $project,
'websiteTranslations' => $websiteTranslations,
]);
}
$this->errors($result);
}
$url = \App\Enums\Site\ProjectSection::DocumentationVersion->url($project, $websiteTranslations->getLanguage(), ['version' => $result->getVersion()->slug]);
/**
* 302 redirect because the documentation version can change at any time.
*/
return redirect($url, 302);
}
public function index(Request $request): View
{
$documentation = new Documentation(
project: $request->get('project'),
version: $request->get('version'),
websiteTranslations: $request->get('websiteTranslations'),
user: $request->user(),
);
$result = $this->documentationService->index($documentation);
if ($result->isError()) {
$this->errors($result);
}
return view('site.projects.documentation.index', $result->getData());
}
public function category(string $slug, Request $request): View
{
$documentation = new Documentation(
project: $request->get('project'),
version: $request->get('version'),
websiteTranslations: $request->get('websiteTranslations'),
user: $request->user(),
);
$result = $this->documentationService->category($slug, $documentation);
if ($result->isError()) {
$this->errors($result);
}
if ($result->isTranslation()) {
return $this->viewPageWithoutTranslation($result);
}
return view('site.projects.documentation.category', $result->getData());
}
public function view(string $slug, Request $request): View
{
$documentation = new Documentation(
project: $request->get('project'),
version: $request->get('version'),
websiteTranslations: $request->get('websiteTranslations'),
user: $request->user(),
);
$result = $this->documentationService->view($slug, $documentation);
if ($result->isError()) {
$this->errors($result);
}
if ($result->isTranslation()) {
return $this->viewPageWithoutTranslation($result);
}
return view('site.projects.documentation.view', $result->getData());
}
}

View File

@@ -19,6 +19,7 @@ final class FeedbackController extends Controller
return view('site.feedback.index', [ return view('site.feedback.index', [
'project' => $request->get('project'), 'project' => $request->get('project'),
'websiteTranslations' => $request->get('websiteTranslations'), 'websiteTranslations' => $request->get('websiteTranslations'),
'captcha' => config('app.captcha', false),
]); ]);
} }

View File

@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use App\Enums\CacheTag;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final readonly class DocumentationVersion
{
public function handle(Request $request, \Closure $next): Response
{
$project = $request->get('project');
$versionSlug = $request->route()?->parameter('version');
if ($versionSlug === null || $project === null) {
\abort(Response::HTTP_NOT_FOUND);
}
$seconds = 3600;
$version = CacheTag::DocumantationVersion->getCache()->remember(self::class . $project->id . '-' . $versionSlug, $seconds, function () use ($project, $versionSlug) {
return $project->documentationVersions()->where('slug', $versionSlug)->first() ?? false;
});
if ($version === false) {
\abort(Response::HTTP_NOT_FOUND);
}
unset($request->route()->parameters['version']);
$request->attributes->set('version', $version);
if (
$version->is_public === false
&& ( $request->user() === null || $request->user()->cannot('view', $version) )
) {
\abort(Response::HTTP_FORBIDDEN);
}
return $next($request);
}
}

View File

@@ -20,6 +20,13 @@ final class IsProject
\abort(Response::HTTP_NOT_FOUND); \abort(Response::HTTP_NOT_FOUND);
} }
if (
$project->is_public === false
&& ( $request->user() === null || $request->user()->cannot('view', $project) )
) {
\abort(Response::HTTP_FORBIDDEN);
}
return $next($request); return $next($request);
} }
} }

View File

@@ -4,10 +4,19 @@ namespace App\Http\Requests\Admin\Projects\About;
use App\Contracts\FormRequestDto; use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\About\StoreUpdate; use App\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{ {
public function attributes(): array
{
return [
'storage.content_images.*.file' => __('validation.attributes.content_images'),
];
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
*/ */
@@ -16,6 +25,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
return [ return [
'title' => ['required', 'string', 'max:255',], 'title' => ['required', 'string', 'max:255',],
'description' => ['nullable', 'string',], 'description' => ['nullable', 'string',],
'storage.content_images.*.file' => ['numeric', 'min:1'],
]; ];
} }
@@ -24,6 +34,18 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
return new StoreUpdate( return new StoreUpdate(
title: $this->input('title'), title: $this->input('title'),
description: $this->input('description'), description: $this->input('description'),
storages: $this->storages(),
); );
} }
private function storages(): Storages
{
$storages = new Storages();
$content = $this->input('storage', []);
$images = $content['content_images'] ?? [];
$storages->addMany($images, StorageType::ContentImages);
return $storages;
}
} }

View File

@@ -6,10 +6,19 @@ use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\Documentation\StoreUpdate; use App\Dto\Service\Admin\Project\Documentation\StoreUpdate;
use App\Dto\Service\Admin\Project\DocumentationContent\Content; use App\Dto\Service\Admin\Project\DocumentationContent\Content;
use App\Dto\Service\Admin\Project\DocumentationContent\Contents; use App\Dto\Service\Admin\Project\DocumentationContent\Contents;
use App\Dto\Service\Storage\Storages;
use App\Enums\StorageType;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{ {
public function attributes(): array
{
return [
'content.*.content_images.*.file' => __('validation.attributes.content_images'),
];
}
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
*/ */
@@ -22,6 +31,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
'category_id' => ['nullable', 'integer', 'exists:documentation_categories,id'], 'category_id' => ['nullable', 'integer', 'exists:documentation_categories,id'],
'content.*.title' => ['required', 'string', 'max:255'], 'content.*.title' => ['required', 'string', 'max:255'],
'content.*.content' => ['nullable', 'string'], 'content.*.content' => ['nullable', 'string'],
'content.*.content_images.*.file' => ['numeric', 'min:1'],
]; ];
} }
@@ -49,8 +59,19 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto
languageId: (int) $languageId, languageId: (int) $languageId,
title: $content['title'], title: $content['title'],
content: $content['content'] ?? '', content: $content['content'] ?? '',
storages: $this->contentStorages($content),
)); ));
} }
return $contents; return $contents;
} }
private function contentStorages(array $content): Storages
{
$storages = new Storages();
$images = $content['content_images'] ?? [];
$storages->addMany($images, StorageType::ContentImages);
return $storages;
}
} }

View File

@@ -13,12 +13,17 @@ final class AuthorizationRequest extends FormRequest implements FormRequestDto
*/ */
public function rules(): array public function rules(): array
{ {
return [ $rules = [
'email' => ['required', 'email', 'max:255'], 'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'], 'password' => ['required', 'min:3'],
'captcha-verified' => ['captcha'],
'remember' => ['nullable', 'boolean'], 'remember' => ['nullable', 'boolean'],
]; ];
if (config('app.captcha', false)) {
$rules['captcha-verified'] = ['captcha'];
}
return $rules;
} }
public function getDto(): Authorization public function getDto(): Authorization

View File

@@ -32,12 +32,17 @@ final class SendRequest extends FormRequest implements FormRequestDto
*/ */
public function rules(): array public function rules(): array
{ {
return [ $rules = [
'name' => ['nullable', 'string', 'max:255'], 'name' => ['nullable', 'string', 'max:255'],
'email' => ['nullable', 'string', 'max:255', 'email'], 'email' => ['nullable', 'string', 'max:255', 'email'],
'message' => ['required', 'string', 'max:5000'], 'message' => ['required', 'string', 'max:5000'],
'captcha-verified' => ['captcha'],
]; ];
if (config('app.captcha', false)) {
$rules['captcha-verified'] = ['captcha'];
}
return $rules;
} }
public function getDto(): Send public function getDto(): Send

View File

@@ -6,7 +6,9 @@ use App\Contracts\FormRequestDto;
use App\Dto\Service\Storage\Upload; use App\Dto\Service\Storage\Upload;
use App\Enums\Morph; use App\Enums\Morph;
use App\Enums\StorageType; use App\Enums\StorageType;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Enum;
final class ImageRequest extends FormRequest implements FormRequestDto final class ImageRequest extends FormRequest implements FormRequestDto
@@ -37,4 +39,20 @@ final class ImageRequest extends FormRequest implements FormRequestDto
morph: Morph::from((int) $this->input('morph')), morph: Morph::from((int) $this->input('morph')),
); );
} }
/**
* Get the error messages for the defined validation rules.*
* @return array
*/
protected function failedValidation(Validator $validator): array
{
/**
* To always return json
*/
throw new HttpResponseException(response()->json([
'errors' => $validator->errors(),
'status' => true
], 422));
}
} }

View File

@@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use App\Models\Scopes\SortScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -9,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
#[ScopedBy([SortScope::class])]
final class Documentation extends Model final class Documentation extends Model
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;

View File

@@ -2,13 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Contracts\Models\Storage as StorageContract;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
final class DocumentationContent extends Model final class DocumentationContent extends Model implements StorageContract
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes, StorageTrait;
protected $table = 'documentation_content'; protected $table = 'documentation_content';

View File

@@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use App\Enums\DocumentationVersionStatus; use App\Enums\DocumentationVersionStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -37,6 +38,16 @@ final class DocumentationVersion extends Model
'status', 'status',
]; ];
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::addGlobalScope('status', function (Builder $builder) {
$builder->orderBy('status', 'desc');
});
}
/** /**
* Get the attributes that should be cast. * Get the attributes that should be cast.
* *

View File

@@ -2,13 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Contracts\Models\Storage as StorageContract;
use App\Models\Traits\StorageTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
final class ProjectContent extends Model final class ProjectContent extends Model implements StorageContract
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes, StorageTrait;
protected $table = 'project_content'; protected $table = 'project_content';

View File

@@ -0,0 +1,60 @@
<?php declare(strict_types=1);
namespace App\Notifications;
use App\Enums\Site\ProjectSection;
use App\Models\ProjectFeedback;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
final class ReviewAdded extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(
private readonly ProjectFeedback $feedback
) { }
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
$project = $this->feedback->project;
return (new MailMessage)
->subject(__('notification.Review Added: :name', ['name' => $project->name]))
->line(__('notification.Added a new review.'))
->action(__('notification.Project :name', ['name' => $project->name]), \url(ProjectSection::Home->url($project)))
->line( __('site.attributes.name') . ': ' . $this->feedback->name)
->line( __('site.attributes.email') . ': ' . $this->feedback->email)
->line(__('site.attributes.message') . ': ' . $this->feedback->message);
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
final readonly class DocumentationContentPolicy extends Policy
{
public function upload(User $user): bool
{
if ($user->hasPermission('documentation.create') || $user->hasPermission('documentation.update')) {
return true;
}
return false;
}
}

View File

@@ -26,4 +26,13 @@ final readonly class ProjectContentPolicy extends Policy
{ {
return $user->hasPermission('project-content.update'); return $user->hasPermission('project-content.update');
} }
public function upload(User $user): bool
{
if ($user->hasPermission('project-content.create') || $user->hasPermission('project-content.update')) {
return true;
}
return false;
}
} }

View File

@@ -3,8 +3,10 @@
namespace App\Providers; namespace App\Providers;
use App\Enums\Morph; use App\Enums\Morph;
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
use App\Services\Search\CreateSearchInstanceCommand; use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search; use App\Services\Search\Search;
use App\Services\Site\FeedbackService;
use App\Services\Storage\Image\ResizeCommandHandler; use App\Services\Storage\Image\ResizeCommandHandler;
use App\Services\Storage\ImageService; use App\Services\Storage\ImageService;
use App\Services\Storage\StorageCommandHandler; use App\Services\Storage\StorageCommandHandler;
@@ -31,7 +33,15 @@ class AppServiceProvider extends ServiceProvider
}); });
$this->app->bind(StorageCommandHandler::class, function () { $this->app->bind(StorageCommandHandler::class, function () {
return new StorageCommandHandler(disc: (string) config('storage.disk')); return new StorageCommandHandler(disc: (string) \config('storage.disk'));
});
$this->app->bind(FeedbackService::class, function (Application $app) {
return new FeedbackService(
$app->make(ProjectFeedbackCommandHandler::class),
(bool) \config('feedback.mail_notifications', false),
\config('feedback.mail_to', null),
);
}); });
$this->app->bind(ImageService::class, function (Application $app) { $this->app->bind(ImageService::class, function (Application $app) {
@@ -59,6 +69,8 @@ class AppServiceProvider extends ServiceProvider
Route::pattern('language', '[a-z_]+'); Route::pattern('language', '[a-z_]+');
Route::pattern('project', '[a-z0-9_-]+'); Route::pattern('project', '[a-z0-9_-]+');
Route::pattern('slug', '[a-z0-9._-]+');
Route::pattern('version', '[a-z0-9._-]+');
$this->configureRateLimiting(); $this->configureRateLimiting();
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']); Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);

View File

@@ -4,13 +4,13 @@ namespace App\Repositories;
use App\Contracts\Search; use App\Contracts\Search;
use App\Models\DocumentationCategory; use App\Models\DocumentationCategory;
use App\Models\DocumentationVersion;
use App\Models\ProjectLanguage; use App\Models\ProjectLanguage;
use App\Services\DocumentationCategory\BuilderCommand; use App\Services\DocumentationCategory\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand; use App\Services\Search\CreateSearchInstanceCommand;
use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto; use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
final readonly class DocumentationCategoryRepository final readonly class DocumentationCategoryRepository
@@ -35,9 +35,19 @@ final readonly class DocumentationCategoryRepository
return DocumentationCategory::query()->where('id', $id)->first(); return DocumentationCategory::query()->where('id', $id)->first();
} }
public function getCategoryByCode(string $code): ?DocumentationCategory public function getCategoryBySlugWithContent(string $slug, int $versionId, ProjectLanguage $language): ?DocumentationCategory
{ {
return DocumentationCategory::query()->where('code', $code)->first(); $with = [
'content' => function (HasOne $hasOne) use ($language) {
$hasOne->where('language_id', $language->id);
}
];
return DocumentationCategory::query()
->where('version_id', $versionId)
->where('slug', $slug)
->with($with)
->first();
} }
public function isExistsSlug(int $versionId, string $slug, ?int $exceptId = null): bool public function isExistsSlug(int $versionId, string $slug, ?int $exceptId = null): bool
@@ -52,7 +62,7 @@ final readonly class DocumentationCategoryRepository
->exists(); ->exists();
} }
public function getForSelect(?ProjectLanguage $defaultLanguage, ?DocumentationCategory $exceptCategory = null, array $withExcepts = []): array public function getForSelect(DocumentationVersion $version, ?ProjectLanguage $defaultLanguage, ?DocumentationCategory $exceptCategory = null, array $withExcepts = []): array
{ {
$with = [ $with = [
'content' => function (HasOne $hasOne) use ($defaultLanguage) { 'content' => function (HasOne $hasOne) use ($defaultLanguage) {
@@ -62,7 +72,7 @@ final readonly class DocumentationCategoryRepository
} }
]; ];
$categories = DocumentationCategory::query() $categories = $version->categories()
->with($with) ->with($with)
->when($exceptCategory, function (Builder $query, DocumentationCategory $exceptCategory) { ->when($exceptCategory, function (Builder $query, DocumentationCategory $exceptCategory) {
$query->whereNotIn( $query->whereNotIn(

View File

@@ -5,9 +5,11 @@ namespace App\Repositories;
use App\Dto\Builder\Documentation as DocumentationBuilderDto; use App\Dto\Builder\Documentation as DocumentationBuilderDto;
use App\Contracts\Search; use App\Contracts\Search;
use App\Models\Documentation; use App\Models\Documentation;
use App\Models\ProjectLanguage;
use App\Services\Documentation\BuilderCommand; use App\Services\Documentation\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand; use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Str; use Illuminate\Support\Str;
final readonly class DocumentationRepository final readonly class DocumentationRepository
@@ -32,9 +34,19 @@ final readonly class DocumentationRepository
return Documentation::query()->where('id', $id)->first(); return Documentation::query()->where('id', $id)->first();
} }
public function getDocumentationByCode(string $code): ?Documentation public function getDocumentationBySlugWithContent(string $slug, int $versionId, ProjectLanguage $language): ?Documentation
{ {
return Documentation::query()->where('code', $code)->first(); $with = [
'content' => function (HasOne $hasOne) use ($language) {
$hasOne->where('language_id', $language->id);
}
];
return Documentation::query()
->where('version_id', $versionId)
->where('slug', $slug)
->with($with)
->first();
} }
public function isExistsSlug(int $versionId, string $slug, ?int $exceptId = null): bool public function isExistsSlug(int $versionId, string $slug, ?int $exceptId = null): bool

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\ServiceResults\Site\DocumentationService;
use App\Models\DocumentationVersion;
use App\ServiceResults\ServiceResult;
final class DefaultVersion extends ServiceResult
{
public function __construct(
private readonly DocumentationVersion $version,
) { }
public function getVersion(): DocumentationVersion
{
return $this->version;
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Services\Admin\Project;
use App\Contracts\ServiceResultError; use App\Contracts\ServiceResultError;
use App\Dto\Service\Admin\Project\About\StoreUpdate; use App\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Enums\Morph;
use App\Models\ProjectContent; use App\Models\ProjectContent;
use App\Models\User; use App\Models\User;
use App\Repositories\ProjectContentRepository; use App\Repositories\ProjectContentRepository;
@@ -13,6 +14,7 @@ use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\StoreUpdateResult; use App\ServiceResults\StoreUpdateResult;
use App\Services\ProjectContent\ProjectContentCommandHandler; use App\Services\ProjectContent\ProjectContentCommandHandler;
use App\Services\Service; use App\Services\Service;
use App\Services\Storage\StorageService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
final class AboutService extends Service final class AboutService extends Service
@@ -22,6 +24,7 @@ final class AboutService extends Service
private readonly ProjectLanguageRepository $projectLanguageRepository, private readonly ProjectLanguageRepository $projectLanguageRepository,
private readonly ProjectContentRepository $projectContentRepository, private readonly ProjectContentRepository $projectContentRepository,
private readonly ProjectContentCommandHandler $projectContentCommandHandler, private readonly ProjectContentCommandHandler $projectContentCommandHandler,
private readonly StorageService $storageService,
) { } ) { }
public function languages(int $projectId, User $user): ServiceResultError | ServiceResultArray public function languages(int $projectId, User $user): ServiceResultError | ServiceResultArray
@@ -88,11 +91,19 @@ final class AboutService extends Service
if ($user->cannot('create', ProjectContent::class)) { if ($user->cannot('create', ProjectContent::class)) {
return $this->errFobidden(__('Access is denied')); return $this->errFobidden(__('Access is denied'));
} }
$storages = $this->storageService->getStoragesAndValidate($data->getStorages(), Morph::ProjectContent);
if (!$storages->isSuccess()) {
return $storages;
}
try { try {
$aboutProject = DB::transaction(function () use ($data, $projectId, $languageId) { $aboutProject = DB::transaction(function () use ($data, $projectId, $languageId, $storages) {
$dataAboutProject = $this->getDataAboutProject($data); $dataAboutProject = $this->getDataAboutProject($data);
return $this->projectContentCommandHandler->handleStore($projectId, $languageId, $dataAboutProject); $aboutProject = $this->projectContentCommandHandler->handleStore($projectId, $languageId, $dataAboutProject);
$this->storageService->saveAndDelete($aboutProject, $storages);
return $aboutProject;
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
@@ -108,11 +119,19 @@ final class AboutService extends Service
return $this->errFobidden(__('Access is denied')); return $this->errFobidden(__('Access is denied'));
} }
$storages = $this->storageService->getStoragesAndValidate($data->getStorages(), Morph::ProjectContent);
if (!$storages->isSuccess()) {
return $storages;
}
try { try {
$aboutProject = DB::transaction(function () use ($data, $content) { $aboutProject = DB::transaction(function () use ($data, $content, $storages) {
$dataAboutProject = $this->getDataAboutProject($data); $dataAboutProject = $this->getDataAboutProject($data);
return $this->projectContentCommandHandler->handleUpdate($content, $dataAboutProject); $aboutProject = $this->projectContentCommandHandler->handleUpdate($content, $dataAboutProject);
$this->storageService->saveAndDelete($aboutProject, $storages);
return $aboutProject;
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);

View File

@@ -89,7 +89,7 @@ final class DocumentationCategoryService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'category' => new DocumentationCategory(), 'category' => new DocumentationCategory(),
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
]); ]);
} }
@@ -119,7 +119,7 @@ final class DocumentationCategoryService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'category' => $category, 'category' => $category,
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage, $category, $withCategories), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, $category, $withCategories),
]); ]);
} }

View File

@@ -5,13 +5,13 @@ namespace App\Services\Admin\Project;
use App\Dto\Builder\Documentation as DocumentationBuilderDto; use App\Dto\Builder\Documentation as DocumentationBuilderDto;
use App\Dto\QuerySettingsDto; use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Project\Documentation\StoreUpdate; use App\Dto\Service\Admin\Project\Documentation\StoreUpdate;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Models\Documentation; use App\Models\Documentation;
use App\Models\ProjectLanguage; use App\Models\ProjectLanguage;
use App\Models\User; use App\Models\User;
use App\Repositories\DocumentationCategoryRepository; use App\Repositories\DocumentationCategoryRepository;
use App\Repositories\DocumentationRepository; use App\Repositories\DocumentationRepository;
use App\Repositories\DocumentationVersionRepository; use App\Repositories\DocumentationVersionRepository;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray; use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;
@@ -89,7 +89,7 @@ final class DocumentationService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'documentation' => new Documentation(), 'documentation' => new Documentation(),
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage),
]); ]);
} }
@@ -119,7 +119,7 @@ final class DocumentationService extends Service
'version' => $version, 'version' => $version,
'project' => $project, 'project' => $project,
'documentation' => $documentation, 'documentation' => $documentation,
'categories' => $this->documentationCategoryRepository->getForSelect($defaultLanguage, null, $withCategories), 'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, null, $withCategories),
]); ]);
} }
@@ -150,6 +150,8 @@ final class DocumentationService extends Service
return $documentation; return $documentation;
}); });
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -190,6 +192,8 @@ final class DocumentationService extends Service
return $documentation; return $documentation;
}); });
} catch (StorageCommandException $e) {
return $e->getResultError();
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));

View File

@@ -5,6 +5,7 @@ namespace App\Services\Admin\Project;
use App\Dto\QuerySettingsDto; use App\Dto\QuerySettingsDto;
use App\Dto\Builder\DocumentationVersion as DocumentationVersionBuilderDto; use App\Dto\Builder\DocumentationVersion as DocumentationVersionBuilderDto;
use App\Dto\Service\Admin\Project\DocumentationVersion\StoreUpdate; use App\Dto\Service\Admin\Project\DocumentationVersion\StoreUpdate;
use App\Enums\CacheTag;
use App\Enums\DocumentationVersionStatus; use App\Enums\DocumentationVersionStatus;
use App\Models\DocumentationVersion; use App\Models\DocumentationVersion;
use App\Models\User; use App\Models\User;
@@ -14,6 +15,7 @@ use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult; use App\ServiceResults\StoreUpdateResult;
use App\Services\ClearCacheCommandHandler;
use App\Services\DocumentationVersion\DocumentationVersionCommandHandler; use App\Services\DocumentationVersion\DocumentationVersionCommandHandler;
use App\Services\Service; use App\Services\Service;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -24,6 +26,7 @@ final class DocumentationVersionService extends Service
private readonly ProjectRepository $projectRepository, private readonly ProjectRepository $projectRepository,
private readonly DocumentationVersionRepository $documentationVersionRepository, private readonly DocumentationVersionRepository $documentationVersionRepository,
private readonly DocumentationVersionCommandHandler $documentationVersionCommandHandler, private readonly DocumentationVersionCommandHandler $documentationVersionCommandHandler,
private readonly ClearCacheCommandHandler $clearCacheCommandHandler,
) { } ) { }
public function index(int $projectId, DocumentationVersionBuilderDto $documentationVersionBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray public function index(int $projectId, DocumentationVersionBuilderDto $documentationVersionBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
@@ -138,6 +141,7 @@ final class DocumentationVersionService extends Service
$dataVersion = $this->getDataVersion($data); $dataVersion = $this->getDataVersion($data);
return $this->documentationVersionCommandHandler->handleStore($project, $dataVersion); return $this->documentationVersionCommandHandler->handleStore($project, $dataVersion);
}); });
$this->clearCacheCommandHandler->byTag(CacheTag::DocumantationVersion);
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -174,6 +178,7 @@ final class DocumentationVersionService extends Service
$dataVersion = $this->getDataVersion($data); $dataVersion = $this->getDataVersion($data);
return $this->documentationVersionCommandHandler->handleUpdate($version, $dataVersion); return $this->documentationVersionCommandHandler->handleUpdate($version, $dataVersion);
}); });
$this->clearCacheCommandHandler->byTag(CacheTag::DocumantationVersion);
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));
@@ -202,6 +207,7 @@ final class DocumentationVersionService extends Service
DB::transaction(function () use ($version) { DB::transaction(function () use ($version) {
$this->documentationVersionCommandHandler->handleDestroy($version); $this->documentationVersionCommandHandler->handleDestroy($version);
}); });
$this->clearCacheCommandHandler->byTag(CacheTag::DocumantationVersion);
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService(__('Server Error')); return $this->errService(__('Server Error'));

View File

@@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\Services\Commands;
use App\Models\Storage as StorageModel;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\Service;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage as StorageSupport;
final class DeleteOldFilesService extends Service
{
/**
* $temporaryBeforeDate = date of deletion of temporary files
* $deletedBeforeDate = date of deletion files that were marked as deleted
*
* @param Carbon $temporaryBeforeDate
* @param Carbon $deletedBeforeDate
* @return ServiceResultError|ServiceResultSuccess
*/
public function fromStorage(Carbon $temporaryBeforeDate, Carbon $deletedBeforeDate): ServiceResultError|ServiceResultSuccess
{
$disk = config('storage.disk');
StorageModel::withTrashed()
->where(function (Builder $query) use($temporaryBeforeDate) {
$query->whereNull('morph_id')->where('updated_at', '<', $temporaryBeforeDate);
})->orWhere(function (Builder $query) use($deletedBeforeDate) {
$query->whereNotNull('deleted_at')->where('deleted_at', '<', $deletedBeforeDate);
})->chunkById(100, function ($items) use($disk) {
$deleteIds = [];
foreach ($items as $item) {
$deleteIds[] = $item->id;
if (StorageSupport::disk($disk)->exists($item->file)) {
StorageSupport::disk($disk)->delete($item->file);
}
}
StorageModel::withTrashed()->whereIn('id', $deleteIds)->forceDelete();
});
return $this->ok(__('Old Files deleted.'));
}
}

View File

@@ -10,10 +10,12 @@ final readonly class BuilderCommand
{ {
public function execute(Relation | Builder $query, DocumentationBuilderDto $documentationBuilderDto): Relation | Builder public function execute(Relation | Builder $query, DocumentationBuilderDto $documentationBuilderDto): Relation | Builder
{ {
if ($documentationBuilderDto->isPublic() !== null) { return $query
$query->where('is_public', $documentationBuilderDto->isPublic()); ->when($documentationBuilderDto->isPublic(), function (Builder $query) use ($documentationBuilderDto) {
} $query->where('is_public', $documentationBuilderDto->isPublic());
})
return $query; ->when($documentationBuilderDto->getCategoryId(), function (Builder $query) use ($documentationBuilderDto) {
$query->where('category_id', $documentationBuilderDto->getCategoryId()->getCategoryId());
});
} }
} }

View File

@@ -10,10 +10,12 @@ final readonly class BuilderCommand
{ {
public function execute(Relation | Builder $query, DocumentationCategoryBuilderDto $documentationCategoryBuilderDto): Relation | Builder public function execute(Relation | Builder $query, DocumentationCategoryBuilderDto $documentationCategoryBuilderDto): Relation | Builder
{ {
if ($documentationCategoryBuilderDto->isPublic() !== null) { return $query
$query->where('is_public', $documentationCategoryBuilderDto->isPublic()); ->when($documentationCategoryBuilderDto->isPublic(), function (Builder $query) use ($documentationCategoryBuilderDto) {
} $query->where('is_public', $documentationCategoryBuilderDto->isPublic());
})
return $query; ->when($documentationCategoryBuilderDto->getParentId(), function (Builder $query) use ($documentationCategoryBuilderDto) {
$query->where('parent_id', $documentationCategoryBuilderDto->getParentId()->getCategoryId());
});
} }
} }

View File

@@ -4,18 +4,33 @@ namespace App\Services\DocumentationContent;
use App\Dto\Service\Admin\Project\DocumentationContent\Content; use App\Dto\Service\Admin\Project\DocumentationContent\Content;
use App\Dto\Service\Admin\Project\DocumentationContent\Contents; use App\Dto\Service\Admin\Project\DocumentationContent\Contents;
use App\Dto\Service\DocumentationContent\StorageDto;
use App\Exceptions\Services\DocumentationContent\ContentSaveException; use App\Exceptions\Services\DocumentationContent\ContentSaveException;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Models\Documentation; use App\Models\Documentation;
use App\Models\DocumentationContent;
use App\Models\Project; use App\Models\Project;
final readonly class ModelSyncCommand final readonly class ModelSyncCommand
{ {
public function __construct(
private StorageCommand $storageCommand,
) { }
/**
* @throws StorageCommandException
* @throws ContentSaveException
*/
public function execute(Project $project, Documentation $documentation, Contents $contents): void public function execute(Project $project, Documentation $documentation, Contents $contents): void
{ {
$storageDto = new StorageDto();
$languages = $project->languages; $languages = $project->languages;
$documentationContents = $documentation->contents; $documentationContents = $documentation->contents;
$newContents = []; $newContents = [];
$contentsStorageCreated = [];
$contentLanguages = [];
foreach ($contents->getContents() as $content) { foreach ($contents->getContents() as $content) {
/** @var Content $content */ /** @var Content $content */
$language = $languages->firstWhere('id', $content->getLanguageId()); $language = $languages->firstWhere('id', $content->getLanguageId());
@@ -26,15 +41,29 @@ final readonly class ModelSyncCommand
$model = $documentationContents->firstWhere('language_id', $language->id); $model = $documentationContents->firstWhere('language_id', $language->id);
$data = $this->getData($content); $data = $this->getData($content);
if (\is_null($model)) { if (\is_null($model)) {
$contentsStorageCreated[$content->getLanguageId()] = $content->getStorages();
$newContents[] = array_merge(['language_id' => $content->getLanguageId()], $data); $newContents[] = array_merge(['language_id' => $content->getLanguageId()], $data);
$contentLanguages[] = $content->getLanguageId();
continue; continue;
} }
$storageDto->add($model, $content->getStorages());
$model->update($data); $model->update($data);
} }
if (!empty($newContents)) { if (!empty($newContents)) {
$documentation->contents()->createMany($newContents); $documentation->contents()->createMany($newContents);
$contents = $documentation->contents()->whereIn('language_id', $contentLanguages)->get();
foreach ($contents as $content) {
/** @var DocumentationContent $content */
if (!isset($contentsStorageCreated[$content->language_id])) {
continue;
}
$storageDto->add($content, $contentsStorageCreated[$content->language_id]);
}
} }
$this->storageCommand->execute($storageDto);
} }
private function getData(Content $content): array private function getData(Content $content): array

View File

@@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace App\Services\DocumentationContent;
use App\Dto\Service\DocumentationContent\StorageDto;
use App\Enums\Morph;
use App\Exceptions\Services\DocumentationContent\StorageCommandException;
use App\Services\Storage\StorageService;
final readonly class StorageCommand
{
public function __construct(
private StorageService $storageService,
) { }
/**
* @throws StorageCommandException
*/
public function execute(StorageDto $storageDto): void
{
foreach ($storageDto->getStorages() as $storage) {
$storages = $this->storageService->getStoragesAndValidate($storage['storages'], Morph::DocumentationContent, $storage['documentationContent']->id);
if (!$storages->isSuccess()) {
throw new StorageCommandException($storages, 'Error when adding a file to storage: ' . $storages->getMessage());
}
$this->storageService->saveAndDelete($storage['documentationContent'], $storages);
}
}
}

View File

@@ -3,7 +3,7 @@
namespace App\Services\Role; namespace App\Services\Role;
use App\Enums\Permission; use App\Enums\Permission;
use App\Exceptions\Rule\RoleSyncPermissionsCommandHandlerException; use App\Exceptions\Services\Rule\RoleSyncPermissionsCommandHandlerException;
use App\Models\Role; use App\Models\Role;
use App\Models\RolePermission; use App\Models\RolePermission;

View File

@@ -0,0 +1,151 @@
<?php declare(strict_types=1);
namespace App\Services\Site;
use App\Dto\Service\Site\Documentation;
use App\Enums\CacheTag;
use App\Enums\DocumentationVersionStatus;
use App\Models\DocumentationCategory;
use App\Models\Documentation as ModelDocumentation;
use App\Models\DocumentationVersion;
use App\Models\Project;
use App\Models\User;
use App\Repositories\DocumentationCategoryRepository;
use App\Repositories\DocumentationRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\Site\DocumentationService\DefaultVersion;
use app\ServiceResults\Site\PagePossibleWithoutTranslation;
use App\Services\Service;
use App\Dto\Builder\DocumentationCategory as DocumentationCategoryBuilderDto;
use App\Dto\Builder\Documentation as DocumentationBuilderDto;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
final class DocumentationService extends Service
{
public function __construct(
private readonly DocumentationCategoryRepository $documentationCategoryRepository,
private readonly DocumentationRepository $documentationRepository
) { }
public function defaultVersion(Project $project, ?User $user): ServiceResultError | DefaultVersion
{
$seconds = 3600;
$isPublic = null;
if ($user?->cannot('viewAny', DocumentationVersion::class)) {
$isPublic = 1;
}
$version = CacheTag::DocumantationVersion->getCache()
->remember(self::class . $project->id . '_' . $isPublic ?? 0, $seconds, function () use ($project, $isPublic) {
$versions = $project->documentationVersions()
->when($isPublic, function (Builder $query) {
$query->where('is_public', 1);
})
->limit(10)
->get();
return $versions->firstWhere('status', DocumentationVersionStatus::CurrentVersion)
??
$versions->first() ?? false;
});
if ($version === false) {
return $this->errNotFound(__('Not Found'));
}
return new DefaultVersion($version);
}
public function index(Documentation $documentation): ServiceResultError | ServiceResultArray
{
return $this->result(array_merge($documentation->toArray(), [
'categories' => $this->getCategories($documentation, null),
'documentations' => $this->getDocumentations($documentation, null),
]));
}
public function category(string $slug, Documentation $documentation): ServiceResultError | PagePossibleWithoutTranslation
{
$category = $this->documentationCategoryRepository->getCategoryBySlugWithContent($slug, $documentation->getVersion()->id, $documentation->getWebsiteTranslations()->getLanguage());
if (!$category) {
return $this->errNotFound(__('Not Found'));
}
if (
$category->is_public === false &&
($documentation->getUser() === null || $documentation->getUser()->cannot('view', $category))
) {
return $this->errFobidden(__('Access is denied'));
}
$data = array_merge($documentation->toArray(), [
'category' => $category,
'categories' => $this->getCategories($documentation, $category->id),
'documentations' => $this->getDocumentations($documentation, $category->id),
]);
return $this->resultSitePage($documentation->getProject(), $documentation->getWebsiteTranslations(), $data, \is_null($category->content?->title));
}
public function view(string $slug, Documentation $documentation): ServiceResultError | PagePossibleWithoutTranslation
{
$document = $this->documentationRepository->getDocumentationBySlugWithContent($slug, $documentation->getVersion()->id, $documentation->getWebsiteTranslations()->getLanguage());
if (!$document) {
return $this->errNotFound(__('Not Found'));
}
if (
$document->is_public === false &&
($documentation->getUser() === null || $documentation->getUser()->cannot('view', $document))
) {
return $this->errFobidden(__('Access is denied'));
}
$data = array_merge($documentation->toArray(), [
'documentation' => $document,
]);
return $this->resultSitePage($documentation->getProject(), $documentation->getWebsiteTranslations(), $data, \is_null($document->content?->title));
}
private function getCategories(Documentation $documentation, ?int $parentId): Collection
{
$isPublic = null;
if ($documentation->getUser() === null || $documentation->getUser()->cannot('viewAny', DocumentationCategory::class)) {
$isPublic = true;
}
$builderDto = new DocumentationCategoryBuilderDto(
isPublic: $isPublic,
parentId: new DocumentationCategoryBuilderDto\Category($parentId),
);
$with = [
'content' => function (HasOne $hasOne) use ($documentation) {
$hasOne->where('language_id', $documentation->getWebsiteTranslations()->getLanguage()->id);
}
];
return $this->documentationCategoryRepository->getCategories(
$documentation->getVersion()->id,
$builderDto,
$with
)->all();
}
private function getDocumentations(Documentation $documentation, ?int $categoryId): Collection
{
$isPublic = null;
if ($documentation->getUser() === null || $documentation->getUser()->cannot('viewAny', ModelDocumentation::class)) {
$isPublic = true;
}
$builderDto = new DocumentationBuilderDto(
isPublic: $isPublic,
categoryId: new DocumentationCategoryBuilderDto\Category($categoryId),
);
$with = [
'content' => function (HasOne $hasOne) use ($documentation) {
$hasOne->where('language_id', $documentation->getWebsiteTranslations()->getLanguage()->id);
}
];
return $this->documentationRepository->getDocumentations(
$documentation->getVersion()->id,
$builderDto,
$with
)->all();
}
}

View File

@@ -5,32 +5,44 @@ namespace App\Services\Site;
use App\Dto\Service\Site\Feedback\Send; use App\Dto\Service\Site\Feedback\Send;
use App\Models\Project; use App\Models\Project;
use App\Models\User; use App\Models\User;
use App\Notifications\ReviewAdded;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;
use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler; use App\Services\ProjectFeedback\ProjectFeedbackCommandHandler;
use App\Services\Service; use App\Services\Service;
use App\Services\WebsiteTranslations; use App\Services\WebsiteTranslations;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification;
final class FeedbackService extends Service final class FeedbackService extends Service
{ {
public function __construct( public function __construct(
private readonly ProjectFeedbackCommandHandler $feedbackCommandHandler, private readonly ProjectFeedbackCommandHandler $feedbackCommandHandler,
private readonly bool $isNotifications = false,
private readonly ?string $mailNotifications = null,
) { } ) { }
public function send(Send $send, Project $project, WebsiteTranslations $websiteTranslations, ?User $user = null): ServiceResultError | ServiceResultSuccess public function send(Send $send, Project $project, WebsiteTranslations $websiteTranslations, ?User $user = null): ServiceResultError | ServiceResultSuccess
{ {
try { try {
DB::transaction(function () use ($send, $project, $user) { $feedback = DB::transaction(function () use ($send, $project, $user) {
$data = $this->getDataFeedback($send); $data = $this->getDataFeedback($send);
$data['user_id'] = $user?->id; $data['user_id'] = $user?->id;
$this->feedbackCommandHandler->handleStore($project, $data); return $this->feedbackCommandHandler->handleStore($project, $data);
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {
report($e); report($e);
return $this->errService($websiteTranslations->translate('Server Error')); return $this->errService($websiteTranslations->translate('Server Error'));
} }
try {
if ($this->isNotifications) {
Notification::route('mail', $this->mailNotifications)->notify(new ReviewAdded($feedback));
}
} catch (\Throwable $e) {
report($e);
}
return $this->ok($websiteTranslations->translate('site.Message sent successfully')); return $this->ok($websiteTranslations->translate('site.Message sent successfully'));
} }

View File

@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site;
use App\Enums\CacheTag;
use App\Models\DocumentationVersion;
use App\Models\Project;
use App\Models\User;
use App\Services\WebsiteTranslations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Illuminate\View\Component;
use Illuminate\View\View;
final class ChooseVersion extends Component
{
public function __construct(
private readonly DocumentationVersion $version,
private readonly WebsiteTranslations $websiteTranslations,
private readonly Project $project,
private readonly ?User $user,
) { }
public function render(): View
{
$isPublic = null;
if (\is_null($this->user) || $this->user->cannot('viewAny', DocumentationVersion::class)) {
$isPublic = 1;
}
$seconds = 3600 * 12;
$versions = CacheTag::DocumantationVersion->getCache()
->remember(self::class . $this->project->id . '-' . $isPublic ?? 0, $seconds, function () use ($isPublic) {
return $this->project->documentationVersions()
->when($isPublic, function (Builder $query) {
$query->where('is_public', 1);
})
->get();
});
return view('components.site.choose-version', [
'websiteTranslations' => $this->websiteTranslations,
'versions' => $versions,
'version' => $this->version,
'project' => $this->project,
]);
}
}

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site;
use App\Services\WebsiteTranslations;
use Illuminate\View\Component;
use Illuminate\View\View;
final class DocumentationVersion extends Component
{
public function __construct(
private readonly \App\Models\DocumentationVersion $version,
private readonly WebsiteTranslations $websiteTranslations,
) { }
public function render(): View
{
return view('components.site.documentation-version', [
'websiteTranslations' => $this->websiteTranslations,
'version' => $this->version,
]);
}
}

View File

@@ -19,7 +19,7 @@ final class Languages extends Form
private function getValue(): array private function getValue(): array
{ {
$value = old($this->getRequestName(), null); $value = old($this->getRequestName(), null);
if (\is_null($value)) { if (\is_null($value) || !isset($value['items']) || !\is_array($value['items'])) {
return $this->value; return $this->value;
} }

View File

@@ -2,15 +2,17 @@
namespace App\View\Components\Volt\Forms; namespace App\View\Components\Volt\Forms;
use App\Dto\View\Volt\Form\WysiwygStorageUpload;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\View\View; use Illuminate\View\View;
final class TextareaWysiwyg extends Form final class TextareaWysiwyg extends Form
{ {
public function __construct( public function __construct(
private readonly string $title, private readonly string $title,
private readonly string $name, private readonly string $name,
private readonly ?string $value = '', private readonly ?string $value = '',
private readonly ?WysiwygStorageUpload $storageUpload = null,
) { } ) { }
protected function getName(): string protected function getName(): string
@@ -28,6 +30,11 @@ final class TextareaWysiwyg extends Form
return (string) old($this->getRequestName(), $this->value); return (string) old($this->getRequestName(), $this->value);
} }
public function getStorageUpload(): ?WysiwygStorageUpload
{
return $this->storageUpload;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -37,10 +44,11 @@ final class TextareaWysiwyg extends Form
return view('components.volt.forms.textarea-wysiwyg', [ return view('components.volt.forms.textarea-wysiwyg', [
'tinymceLicenseKey' => $tinymceLicenseKey, 'tinymceLicenseKey' => $tinymceLicenseKey,
'title' => $this->getTitle(), 'title' => $this->getTitle(),
'name' => $this->getName(), 'name' => $this->getName(),
'requestName' => $this->getRequestName(), 'requestName' => $this->getRequestName(),
'value' => $this->getValue(), 'value' => $this->getValue(),
'storageUpload' => $this->getStorageUpload(),
]); ]);
} }
} }

View File

@@ -28,6 +28,15 @@ return [
'env' => env('APP_ENV', 'production'), 'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Сaptcha
|--------------------------------------------------------------------------
|
| Enables or disables captcha.
*/
'captcha' => (bool) env('APP_CAPTCHA', false),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Debug Mode | Application Debug Mode

View File

@@ -0,0 +1,9 @@
<?php
return [
/**
* Enable new review alerts.
*/
'mail_notifications' => (bool) env('FEEDBACK_MAIL_NOTIFICATIONS', false),
'mail_to' => env('FEEDBACK_MAIL_TO', null),
];

View File

@@ -0,0 +1,32 @@
<?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
{
Schema::table('storage', function (Blueprint $table) {
$table->dropIndex(['morph_id']);
$table->index(['morph_id', 'updated_at']);
$table->index(['deleted_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('storage', function (Blueprint $table) {
$table->dropIndex(['morph_id', 'updated_at']);
$table->dropIndex(['deleted_at']);
$table->index(['morph_id']);
});
}
};

View File

@@ -267,5 +267,6 @@
"Documentation successfully removed": "Documentation successfully removed", "Documentation successfully removed": "Documentation successfully removed",
"Category successfully created": "Category successfully created", "Category successfully created": "Category successfully created",
"Category updated successfully": "Category updated successfully", "Category updated successfully": "Category updated successfully",
"Category successfully deleted": "Category successfully deleted" "Category successfully deleted": "Category successfully deleted",
"Old Files deleted": "Old Files deleted"
} }

View File

@@ -0,0 +1,6 @@
<?php
return [
'Added a new review.' => 'Added a new review.',
'Review Added: :name' => 'Review Added: :name',
'Project :name' => 'Project :name',
];

View File

@@ -16,4 +16,9 @@ return [
'email' => 'email', 'email' => 'email',
'message' => 'message', 'message' => 'message',
], ],
'Documentation' => 'Documentation',
'Documentation not created' => 'Documentation not created',
'Choose version' => 'Choose version',
'alert-status-not-supported' => 'WARNING! You are viewing the documentation for an older version.',
'alert-status-future' => 'WARNING! You\'re browsing the documentation for an upcoming version. The documentation and features of this release are subject to change.',
]; ];

View File

@@ -307,5 +307,6 @@ return [
'content.*.title' => 'title', 'content.*.title' => 'title',
'content.*.content' => 'content', 'content.*.content' => 'content',
'category_id' => 'category', 'category_id' => 'category',
'content_images' => 'content images',
], ],
]; ];

View File

@@ -267,5 +267,6 @@
"Documentation successfully removed": "Документация успешно удалена", "Documentation successfully removed": "Документация успешно удалена",
"Category successfully created": "Категория успешно создана", "Category successfully created": "Категория успешно создана",
"Category updated successfully": "Категория успешно обновлена", "Category updated successfully": "Категория успешно обновлена",
"Category successfully deleted": "Категория успешно удалена" "Category successfully deleted": "Категория успешно удалена",
"Old Files deleted": "Старые файлы удалены"
} }

View File

@@ -0,0 +1,6 @@
<?php
return [
'Added a new review.' => 'Добавили новый отзыв.',
'Review Added: :name' => 'Отзыв добавлен: :name',
'Project :name' => 'Проект :name',
];

View File

@@ -16,4 +16,9 @@ return [
'email' => 'email', 'email' => 'email',
'message' => 'сообщение', 'message' => 'сообщение',
], ],
'Documentation' => 'Документация',
'Documentation not created' => 'Документация не создана',
'Choose version' => 'Выберите версию',
'alert-status-not-supported' => 'ВНИМАНИЕ! Вы просматриваете документацию для старой версии.',
'alert-status-future' => 'ВНИМАНИЕ! Вы просматриваете документацию к будущей версии. Документация и функции этого выпуска могут быть изменены.',
]; ];

View File

@@ -307,5 +307,6 @@ return [
'content.*.title' => 'заголовок', 'content.*.title' => 'заголовок',
'content.*.content' => 'контент', 'content.*.content' => 'контент',
'category_id' => 'категория', 'category_id' => 'категория',
'content_images' => 'изображения контента',
], ],
]; ];

View File

@@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@fancyapps/ui": "^5.0.36",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"bootstrap": "5.0.2", "bootstrap": "5.0.2",
"chartist": "^0.11.4", "chartist": "^0.11.4",
@@ -17,7 +18,6 @@
"sass": "^1.47.0", "sass": "^1.47.0",
"simplebar": "^5.3.4", "simplebar": "^5.3.4",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"sweetalert2": "^11.0.18",
"tinymce": "^7.0.1", "tinymce": "^7.0.1",
"vanillajs-datepicker": "^1.2.0", "vanillajs-datepicker": "^1.2.0",
"waypoints": "^4.0.1" "waypoints": "^4.0.1"
@@ -398,6 +398,11 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@fancyapps/ui": {
"version": "5.0.36",
"resolved": "https://registry.npmjs.org/@fancyapps/ui/-/ui-5.0.36.tgz",
"integrity": "sha512-GMygQzp1MBTFNTT6AzpbL6pXTD6bTxwjmmpI1fe8Ozmmiseu8/g82Sudl1YhcbZmS4bJgaBOF5THDFGpXQ1fDw=="
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@@ -2065,15 +2070,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/sweetalert2": {
"version": "11.10.7",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.10.7.tgz",
"integrity": "sha512-5Jlzrmaitay6KzU+2+LhYu9q+L4v/dZ8oZyEDH14ep0C/QilCnFLHmqAyD/Lhq/lm5DiwsOs6Tr58iv8k3wyGg==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",

View File

@@ -6,6 +6,7 @@
"build": "vite build" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"@fancyapps/ui": "^5.0.36",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"bootstrap": "5.0.2", "bootstrap": "5.0.2",
"chartist": "^0.11.4", "chartist": "^0.11.4",
@@ -18,7 +19,6 @@
"sass": "^1.47.0", "sass": "^1.47.0",
"simplebar": "^5.3.4", "simplebar": "^5.3.4",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"sweetalert2": "^11.0.18",
"tinymce": "^7.0.1", "tinymce": "^7.0.1",
"vanillajs-datepicker": "^1.2.0", "vanillajs-datepicker": "^1.2.0",
"waypoints": "^4.0.1" "waypoints": "^4.0.1"

View File

@@ -0,0 +1,6 @@
import { Fancybox } from "@fancyapps/ui";
import "@fancyapps/ui/dist/fancybox/fancybox.css";
Fancybox.bind('a.image-open', {
//
});

View File

@@ -18,6 +18,7 @@ import "prismjs/components/prism-nginx.js";
import "prismjs/components/prism-docker.js"; import "prismjs/components/prism-docker.js";
import "prismjs/components/prism-diff.js"; import "prismjs/components/prism-diff.js";
import "prismjs/components/prism-php.js"; import "prismjs/components/prism-php.js";
import "prismjs/components/prism-yaml.js";
import "prismjs/plugins/toolbar/prism-toolbar.css"; import "prismjs/plugins/toolbar/prism-toolbar.css";

View File

@@ -0,0 +1,10 @@
let blockDocumentationVersion = document.querySelector('#documentation-version');
if (blockDocumentationVersion) {
blockDocumentationVersion.querySelector('.documentation-version__button').addEventListener('click', (e) => {
if (blockDocumentationVersion.classList.contains('active')) {
blockDocumentationVersion.classList.remove('active');
} else {
blockDocumentationVersion.classList.add('active');
}
});
}

View File

@@ -1,2 +1,3 @@
import './_menu.js'; import './_menu.js';
import './_choose-language.js'; import './_choose-language.js';
import './_choose-documentation-version.js';

View File

@@ -29,6 +29,39 @@ body {
.content { .content {
flex: 1 0 auto; flex: 1 0 auto;
img {
max-width: 100%;
object-fit: contain;
}
.image-open {
display: block;
img {
max-width: 100%;
height: auto;
}
}
@supports (--custom:property) {
[style*="--aspect-ratio"] {
position: relative;
}
[style*="--aspect-ratio"]::before {
content: "";
display: block;
padding-bottom: calc(100% / (var(--aspect-ratio)));
}
[style*="--aspect-ratio"] img {
position: absolute;
top: 0;
left: 0;
height: 100%;
max-width: 100%;
object-fit: cover;
}
}
} }
.footer { .footer {
@@ -43,7 +76,8 @@ body {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-areas: grid-template-areas:
"logo menu" "logo menu"
"language language"; "language language"
"documentation-version documentation-version";
padding: 8px; padding: 8px;
box-shadow: 0 3px 9px rgba(0,0,0,0.48); box-shadow: 0 3px 9px rgba(0,0,0,0.48);
background: #eee; background: #eee;
@@ -79,7 +113,7 @@ body {
margin-left: auto; margin-left: auto;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
z-index: 10; z-index: 110;
.open { .open {
display: block; display: block;
@@ -138,6 +172,7 @@ body.mobile-menu-open {
overflow: auto; overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 100;
} }
} }
#language { #language {
@@ -176,6 +211,7 @@ body.mobile-menu-open {
padding: 10px 0; padding: 10px 0;
margin: 0; margin: 0;
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
z-index: 100;
a { a {
text-decoration: none; text-decoration: none;
@@ -193,6 +229,77 @@ body.mobile-menu-open {
border-radius: 5px 5px 0 0; border-radius: 5px 5px 0 0;
} }
} }
#documentation-version {
grid-area: documentation-version;
margin-top: 10px;
.status {
font-size: 13px;
color: #444;
display: block;
}
.status.status__100 {
color: #0a6f4d;
}
.status.status__50 {
color: #0a53be;
}
.status.status__0 {
color: #6c0e22;
}
}
.documentation-version__block {
position: relative;
}
.documentation-version__button {
border: 0;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.48);
background: #ccc;
width: 100%;
cursor: pointer;
font-size: 18px;
position: relative;
padding: 10px;
border-radius: 5px;
color: #000;
}
.documentation-version__button__str {
position: absolute;
top: calc(50% - 8px);
right: 10px;
}
.documentation-version__list {
background: #ddd;
display: none;
position: absolute;
top: 100%;
left: 0;
width: 100%;
list-style: none;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.48);
padding: 10px 0;
margin: 0;
border-radius: 0 0 5px 5px;
z-index: 100;
a {
text-decoration: none;
color: #000;
font-size: 16px;
display: block;
padding: 10px 20px;
}
}
#documentation-version.active {
.documentation-version__list {
display: block;
}
.documentation-version__button {
border-radius: 5px 5px 0 0;
}
}
.main-container { .main-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -223,6 +330,15 @@ body.mobile-menu-open {
font-weight: bold; font-weight: bold;
} }
} }
.alert-version {
padding: 20px;
margin-top: 20px;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
font-size: 16px;
}
.alert-version.version-status-not-supported {
color: #6c0e22;
}
@media (min-width: 1000px) { @media (min-width: 1000px) {
body { body {
@@ -230,8 +346,8 @@ body.mobile-menu-open {
margin: 0 auto; margin: 0 auto;
} }
.header { .header {
grid-template-areas: "logo language"; grid-template-areas: "logo documentation-version language";
grid-template-columns: 1fr 200px; grid-template-columns: 1fr 200px 200px;
} }
.header_logo { .header_logo {
width: 150px; width: 150px;
@@ -248,6 +364,7 @@ body.mobile-menu-open {
.main-container { .main-container {
flex-direction: row; flex-direction: row;
} }
#language { #language {
margin-right: 10px; margin-right: 10px;
} }
@@ -257,6 +374,17 @@ body.mobile-menu-open {
.language__button { .language__button {
padding: 7px 10px 7px 0; padding: 7px 10px 7px 0;
} }
#documentation-version {
margin-right: 10px;
}
.documentation-version__block {
margin-top: 0px;
}
.documentation-version__button {
padding: 7px 10px 7px 0;
}
#menu { #menu {
flex: none; flex: none;
background: #eee; background: #eee;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
@vite('resources/fancybox/app.js')

View File

@@ -0,0 +1 @@
<script src="{{ asset('/build/tinymce/tinymce.min.js') }}" referrerpolicy="origin"></script>

View File

@@ -0,0 +1,4 @@
@pushOnce('scripts')
@include('_tinymce')
@include('_prism')
@endpushonce

View File

@@ -1,6 +1,11 @@
@csrf @csrf
<x-volt.forms.input :title="__('validation.attributes.title')" name="title" type="text" :value="$content->title" required autofocus /> <x-volt.forms.input :title="__('validation.attributes.title')" name="title" type="text" :value="$content->title" required autofocus />
<x-volt.forms.textarea-wysiwyg :title="__('validation.attributes.description')" name="description" :value="$content->description" /> <x-volt.forms.textarea-wysiwyg
:title="__('validation.attributes.description')"
name="description"
:value="$content->description"
:storageUpload="new \App\Dto\View\Volt\Form\WysiwygStorageUpload(inputName: 'storage', morph: \App\Enums\Morph::ProjectContent)"
/>
@canany(['create', 'update'], $content) @canany(['create', 'update'], $content)
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button> <button class="btn btn-primary" type="submit">{{ __('Save') }}</button>

View File

@@ -1,15 +1,22 @@
@csrf @csrf
<x-volt.forms.checkbox :title="__('validation.attributes.is_public')" name="is_public" checkboxValue="1" notCheckedValue="0" :userValue="(string) $documentation->is_public" /> <x-volt.forms.checkbox :title="__('validation.attributes.is_public')" name="is_public" checkboxValue="1"
<x-volt.forms.input :title="__('validation.attributes.slug')" allowed-characters="a-z0-9.-_" name="slug" type="text" :value="$documentation->slug" required autofocus /> notCheckedValue="0" :userValue="(string) $documentation->is_public"/>
<x-volt.forms.input :title="__('validation.attributes.sort')" name="sort" type="number" :value="$documentation->sort" required /> <x-volt.forms.input :title="__('validation.attributes.slug')" allowed-characters="a-z0-9.-_" name="slug" type="text"
<x-volt.forms.select :title="__('validation.attributes.category_id')" name="category_id" :list="$categories" :value="(string) $documentation->category?->id"> :value="$documentation->slug" required autofocus/>
<x-volt.forms.input :title="__('validation.attributes.sort')" name="sort" type="number" :value="$documentation->sort"
required/>
<x-volt.forms.select :title="__('validation.attributes.category_id')" name="category_id" :list="$categories"
:value="(string) $documentation->category?->id">
<option value=""></option> <option value=""></option>
</x-volt.forms.select> </x-volt.forms.select>
<nav> <nav>
<div class="nav nav-tabs mb-4" id="nav-language-tab" role="tablist"> <div class="nav nav-tabs mb-4" id="nav-language-tab" role="tablist">
@foreach($project->languages as $index => $language) @foreach($project->languages as $index => $language)
<a class="nav-item nav-link @if($index === 0) active @endif" id="language-{{ $language->id }}-tab" data-bs-toggle="tab" href="#language-{{ $language->id }}" role="tab" aria-controls="language-{{ $language->id }}" aria-selected="{{ $index ? 'false' : 'true' }}">{{ $language->title }}</a> <a class="nav-item nav-link @if($index === 0) active @endif" id="language-{{ $language->id }}-tab"
data-bs-toggle="tab" href="#language-{{ $language->id }}" role="tab"
aria-controls="language-{{ $language->id }}"
aria-selected="{{ $index ? 'false' : 'true' }}">{{ $language->title }}</a>
@endforeach @endforeach
</div> </div>
</nav> </nav>
@@ -18,10 +25,22 @@
@php @php
$content = $documentation->contents->firstWhere('language_id', $language->id); $content = $documentation->contents->firstWhere('language_id', $language->id);
@endphp @endphp
<div class="tab-pane fade @if($index === 0) show active @endif" id="language-{{ $language->id }}" role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab"> <div class="tab-pane fade @if($index === 0) show active @endif" id="language-{{ $language->id }}"
<x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id" :user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1" notCheckedValue="0"/> role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab">
<x-volt.forms.input :title="__('validation.attributes.title')" :name="'content[' . $language->id . '][title]'" type="text" class="language-content" :disabled="$index !== 0" :value="$content?->title ?? ''" required /> <x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id"
<x-volt.forms.textarea-wysiwyg :title="__('validation.attributes.content')" :name="'content[' . $language->id . '][content]'" class="language-content" :value="$content?->content" :disabled="$index !== 0" /> :user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1"
notCheckedValue="0"/>
<x-volt.forms.input :title="__('validation.attributes.title')"
:name="'content[' . $language->id . '][title]'" type="text" class="language-content"
:disabled="$index !== 0" :value="$content?->title ?? ''" required/>
<x-volt.forms.textarea-wysiwyg
:title="__('validation.attributes.content')"
:storageUpload="new \App\Dto\View\Volt\Form\WysiwygStorageUpload(inputName: 'content[' . $language->id . ']', morph: \App\Enums\Morph::DocumentationContent)"
:name="'content[' . $language->id . '][content]'"
class="language-content"
:value="$content?->content"
:disabled="$index !== 0"
/>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@@ -0,0 +1,18 @@
<div id="documentation-version">
<div class="documentation-version__block">
<button class="documentation-version__button" type="button" aria-label="{{ $websiteTranslations->translate('site.Choose version') }}">
<svg width="16" height="16" data-slot="icon" fill="currentColor" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path clip-rule="evenodd" fill-rule="evenodd" d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm1 5.75A.75.75 0 0 1 5.75 7h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 5 7.75Zm0 3a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"></path>
</svg>
{{ $version->title }} <span class="status status__{{ $version->status->value }}">({{ $version->status->getTitle($websiteTranslations) }})</span>
<svg width="16" height="16" class="documentation-version__button__str" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"></path>
</svg>
</button>
<ul class="documentation-version__list">
@foreach($versions as $v)
<li><a href="{{ \App\Enums\Site\ProjectSection::DocumentationVersion->url($project, $websiteTranslations->getLanguage(), ['version' => $v->slug]) }}">{{ $v->title }} <span class="status status__{{ $v->status->value }}">({{ $v->status->getTitle($websiteTranslations) }})</span></a></li>
@endforeach
</ul>
</div>
</div>

View File

@@ -0,0 +1,10 @@
@if($version->status === \App\Enums\DocumentationVersionStatus::NotSupported)
<div class="alert-version version-status-not-supported">
{{ $websiteTranslations->translate('site.alert-status-not-supported') }}
</div>
@endif
@if($version->status === \App\Enums\DocumentationVersionStatus::FutureVersion)
<div class="alert-version version-status-future">
{{ $websiteTranslations->translate('site.alert-status-future') }}
</div>
@endif

View File

@@ -1,23 +1,114 @@
@php
$tinyId = \Illuminate\Support\Str::random(7);
$tinyId = 'form-textarea-wysiwyg-' . $tinyId;
$images = [];
$files = [];
/** @var \App\Dto\View\Volt\Form\WysiwygStorageUpload | null $storageUpload */
if ($storageUpload !== null) {
$storages = old($storageUpload->getRequestInputName(), []);
$images = $storages['content_images'] ?? [];
}
@endphp
<div class="mb-3"> <div class="mb-3">
<label for="form-textarea-wysiwyg-{{ $requestName }}">{{ $title }}</label> <label for="{{ $tinyId }}">{{ $title }}</label>
<textarea class="form-control {{ $attributes->get('class') }} textarea-tinymce @error($requestName) is-invalid @enderror" name="{{ $name }}" id="form-textarea-wysiwyg-{{ $requestName }}" rows="3">{{ $value }}</textarea> <textarea class="form-control {{ $attributes->get('class') }} textarea-tinymce @error($requestName) is-invalid @enderror" name="{{ $name }}" id="{{ $tinyId }}" rows="3">{{ $value }}</textarea>
@foreach($images as $image)
@continue( empty($image['file']) )
<input type="hidden" value="{{ $image['file'] }}" name="{{ $storageUpload->getInputName() }}[content_images][][file]';">
@endforeach
@error($requestName) @error($requestName)
<span class="invalid-feedback">{{ $message }}</span> <span class="invalid-feedback">{{ $message }}</span>
@enderror @enderror
</div> </div>
@pushOnce('scripts') @include('admin._scripts._tinymce')
<script src="{{ asset('/build/tinymce/tinymce.min.js') }}" referrerpolicy="origin"></script> @push('scripts')
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@if($storageUpload !== null)
const textarea = document.querySelector('#{{ $tinyId }}');
if (!textarea) {
return;
}
const storageImageType = '{{ \App\Enums\StorageType::ContentImages->value }}';
const morph = '{{ $storageUpload->getMorph()->value }}';
const storageInputName = '{{ $storageUpload->getInputName() }}';
const imageUpload = (blobInfo, progress) => new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.open('POST', '{{ route('storage.image_upload_and_resize') }}');
xhr.upload.onprogress = (e) => {
progress(e.loaded / e.total * 100);
};
xhr.onload = () => {
if (xhr.status === 422) {
const json = JSON.parse(xhr.responseText);
let error = 'Error:<br><br>';
for (let key of Object.keys(json.errors)) {
error += json.errors[key] + '<br><br>';
}
reject(error);
return;
}
if (xhr.status === 403) {
reject({ message: 'HTTP Error: ' + xhr.status, remove: true });
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
reject('HTTP Error: ' + xhr.status);
return;
}
const json = JSON.parse(xhr.responseText);
if (!json || typeof json.url != 'string') {
reject('Invalid JSON: ' + xhr.responseText);
return;
}
let input = document.createElement('input');
input.type = 'hidden';
input.name = storageInputName + '[content_images][][file]';
input.value = json.id;
textarea.after(input);
resolve(json.url);
};
xhr.onerror = () => {
reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
};
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
formData.append('_token', document.querySelector('meta[name="csrf-token"]').content);
formData.append('storage_type', storageImageType);
formData.append('morph', morph);
xhr.send(formData);
});
@endif
tinymce.init({ tinymce.init({
selector: '.textarea-tinymce', selector: '#{{ $tinyId }}',
@if(in_array(app()->getLocale(), ['ru'], true)) @if(in_array(app()->getLocale(), ['ru'], true))
language: '{{ app()->getLocale() }}', language: '{{ app()->getLocale() }}',
@endif @endif
license_key: '{{ $tinymceLicenseKey }}', license_key: '{{ $tinymceLicenseKey }}',
plugins: 'advlist code emoticons link lists table codesample', plugins: 'advlist code emoticons link lists table codesample media my-image',
toolbar: 'bold italic | bullist numlist | link emoticons codesample', toolbar: 'bold italic | bullist numlist | link image emoticons media codesample',
referrer_policy: 'origin', referrer_policy: 'origin',
@if($storageUpload !== null)
images_upload_handler: imageUpload,
@endif
relative_urls: false,
convert_urls: false,
codesample_global_prismjs: true, codesample_global_prismjs: true,
codesample_languages: [ codesample_languages: [
{text: 'HTML/XML', value: 'markup'}, {text: 'HTML/XML', value: 'markup'},
@@ -33,11 +124,11 @@
{text: 'Go', value: 'go'}, {text: 'Go', value: 'go'},
{text: 'Nginx', value: 'nginx'}, {text: 'Nginx', value: 'nginx'},
{text: 'Docker', value: 'docker'}, {text: 'Docker', value: 'docker'},
{text: 'Yaml', value: 'yaml'},
{text: "Treeview", value: "treeview"}, {text: "Treeview", value: "treeview"},
{text: "Diff", value: "diff"}, {text: "Diff", value: "diff"},
], ],
}); });
}); });
</script> </script>
@include('_prism') @endpush
@endpushonce

View File

@@ -30,18 +30,25 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
</svg> </svg>
</button> </button>
@if($attributes->has('documentationVersion'))
<x-site.choose-version :version="$attributes->get('documentationVersion')" :websiteTranslations="$websiteTranslations" :project="$project" :user="auth()->user()" />
@endif
<x-site.choose-language :websiteTranslations="$websiteTranslations" :project="$project" /> <x-site.choose-language :websiteTranslations="$websiteTranslations" :project="$project" />
</header> </header>
<div class="main-container"> <div class="main-container">
<nav id="menu"> <nav id="menu">
<div class="menu__title">{{ __('site.Menu') }}</div> <div class="menu__title">{{ $websiteTranslations->translate('site.Menu') }}</div>
<ul> <ul>
<li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['home', 'language.home', 'project.home', 'project.language.home'])])>{{ $websiteTranslations->translate('site.About project') }}</a></li> <li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['home', 'language.home', 'project.home', 'project.language.home'])])>{{ $websiteTranslations->translate('site.About project') }}</a></li>
<li><a href="{{ \App\Enums\Site\ProjectSection::Documentation->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['documentation', 'documentation.home'])])>{{ $websiteTranslations->translate('site.Documentation') }}</a></li>
<li><a href="{{ \App\Enums\Site\ProjectSection::Feedback->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['feedback', 'language.feedback', 'project.feedback', 'project.language.feedback'])])>{{ $websiteTranslations->translate('site.Feedback') }}</a></li> <li><a href="{{ \App\Enums\Site\ProjectSection::Feedback->url($project, $websiteTranslations->getLanguage()) }}" @class(['active' => request()->route()->named(['feedback', 'language.feedback', 'project.feedback', 'project.language.feedback'])])>{{ $websiteTranslations->translate('site.Feedback') }}</a></li>
</ul> </ul>
</nav> </nav>
<div class="section-container"> <div class="section-container">
<div class="content"> <div class="content">
@if($attributes->has('documentationVersion'))
<x-site.documentation-version :websiteTranslations="$websiteTranslations" :version="$attributes->get('documentationVersion')" />
@endif
<h1>@yield('h1', '')</h1> <h1>@yield('h1', '')</h1>
@includeWhen($errors->any(), 'layout.site._errors', ['errors' => $errors->all()]) @includeWhen($errors->any(), 'layout.site._errors', ['errors' => $errors->all()])

View File

@@ -36,7 +36,7 @@
<span class="input-group-text" id="basic-addon2"> <span class="input-group-text" id="basic-addon2">
<svg class="icon icon-xs text-gray-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg> <svg class="icon icon-xs text-gray-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>
</span> </span>
<input type="password" name="password" placeholder="Password" class="form-control" id="password" required> <input type="password" name="password" placeholder="Password" class="form-control" id="password" autocomplete="off" required>
</div> </div>
</div> </div>
<!-- End of Form --> <!-- End of Form -->
@@ -49,9 +49,11 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group mb-4"> @if($captcha)
@captcha <div class="form-group mb-4">
</div> @captcha
</div>
@endif
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-gray-800">{{ __('Sign in') }}</button> <button type="submit" class="btn btn-gray-800">{{ __('Sign in') }}</button>

View File

@@ -23,8 +23,8 @@
<form method="post" action="{{ route('profile.update-password') }}"> <form method="post" action="{{ route('profile.update-password') }}">
@csrf @csrf
@method('PUT') @method('PUT')
<x-volt.forms.input :title="__('validation.attributes.password')" name="password" type="password" required /> <x-volt.forms.input :title="__('validation.attributes.password')" name="password" type="password" autocomplete="off" required />
<x-volt.forms.input :title="__('validation.attributes.password_confirmation')" name="password_confirmation" type="password" required /> <x-volt.forms.input :title="__('validation.attributes.password_confirmation')" name="password_confirmation" type="password" autocomplete="off" required />
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button> <button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
</form> </form>
</div> </div>

View File

@@ -8,12 +8,14 @@
<x-site.forms.input :title="$websiteTranslations->translate('site.attributes.name')" :websiteTranslations="$websiteTranslations" name="name" type="text" value="" autofocus /> <x-site.forms.input :title="$websiteTranslations->translate('site.attributes.name')" :websiteTranslations="$websiteTranslations" name="name" type="text" value="" autofocus />
<x-site.forms.input :title="$websiteTranslations->translate('site.attributes.email')" :websiteTranslations="$websiteTranslations" name="email" type="text" value="" /> <x-site.forms.input :title="$websiteTranslations->translate('site.attributes.email')" :websiteTranslations="$websiteTranslations" name="email" type="text" value="" />
<x-site.forms.textarea-wysiwyg :title="$websiteTranslations->translate('site.attributes.message')" :websiteTranslations="$websiteTranslations" required name="message" value="" /> <x-site.forms.textarea-wysiwyg :title="$websiteTranslations->translate('site.attributes.message')" :websiteTranslations="$websiteTranslations" required name="message" value="" />
<div class="form-block"> @if($captcha)
@captcha <div class="form-block">
@error('captcha-verified') @captcha
<span class="invalid-feedback">{{ $message }}</span> @error('captcha-verified')
@enderror <span class="invalid-feedback">{{ $message }}</span>
</div> @enderror
</div>
@endif
<div class="form-block"> <div class="form-block">
<button class="button" type="submit">{{ $websiteTranslations->translate('site.Feedback-send') }}</button> <button class="button" type="submit">{{ $websiteTranslations->translate('site.Feedback-send') }}</button>
</div> </div>

View File

@@ -11,5 +11,6 @@
@push('scripts') @push('scripts')
@include('_prism') @include('_prism')
@include('_fancybox')
@endpush @endpush
</x-site.layout> </x-site.layout>

View File

@@ -0,0 +1,13 @@
@section('meta_title', $category->content?->title . ' - ' . $project->name . ' ' . $version->title)
@section('h1', $category->content?->title)
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations" :documentationVersion="$version">
@foreach($categories as $category)
@continue(! $category->content?->title)
<p><a href="{{ \App\Enums\Site\ProjectSection::DocumentationCategory->url($project, $websiteTranslations->getLanguage(), ['version' => $version->slug, 'slug' => $category->slug]) }}">{{ $category->content->title }}</a></p>
@endforeach
@foreach($documentations as $documentation)
@continue(! $documentation->content?->title)
<p><a href="{{ \App\Enums\Site\ProjectSection::DocumentationView->url($project, $websiteTranslations->getLanguage(), ['version' => $version->slug, 'slug' => $documentation->slug]) }}">{{ $documentation->content->title }}</a></p>
@endforeach
</x-site.layout>

View File

@@ -0,0 +1,13 @@
@section('meta_title', $websiteTranslations->translate('site.Documentation') . ' - ' . $project->name . ' ' . $version->title)
@section('h1', $websiteTranslations->translate('site.Documentation'))
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations" :documentationVersion="$version">
@foreach($categories as $category)
@continue(! $category->content?->title)
<p><a href="{{ \App\Enums\Site\ProjectSection::DocumentationCategory->url($project, $websiteTranslations->getLanguage(), ['version' => $version->slug, 'slug' => $category->slug]) }}">{{ $category->content->title }}</a></p>
@endforeach
@foreach($documentations as $documentation)
@continue(! $documentation->content?->title)
<p><a href="{{ \App\Enums\Site\ProjectSection::DocumentationView->url($project, $websiteTranslations->getLanguage(), ['version' => $version->slug, 'slug' => $documentation->slug]) }}">{{ $documentation->content->title }}</a></p>
@endforeach
</x-site.layout>

View File

@@ -0,0 +1,6 @@
@section('meta_title', $websiteTranslations->translate('site.Documentation not created'))
@section('h1', $websiteTranslations->translate('site.Documentation not created'))
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations">
</x-site.layout>

View File

@@ -0,0 +1,10 @@
@section('meta_title', $documentation->content?->title . ' - ' . $project->name . ' ' . $version->title)
@section('h1', $documentation->content?->title)
<x-site.layout :project="$project" :websiteTranslations="$websiteTranslations" :documentationVersion="$version">
<div class="line-numbers">{!! $documentation->content->content !!}</div>
@push('scripts')
@include('_prism')
@include('_fancybox')
@endpush
</x-site.layout>

View File

@@ -19,53 +19,10 @@
const d = document; const d = document;
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import Swal from 'sweetalert2';
import SmoothScroll from 'smooth-scroll'; import SmoothScroll from 'smooth-scroll';
import Chartist from 'chartist';
import 'chartist-plugin-tooltips';
d.addEventListener("DOMContentLoaded", function(event) { d.addEventListener("DOMContentLoaded", function(event) {
const swalWithBootstrapButtons = Swal.mixin({
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-gray'
},
buttonsStyling: false
});
var themeSettingsEl = document.getElementById('theme-settings');
var themeSettingsExpandEl = document.getElementById('theme-settings-expand');
if(themeSettingsEl) {
var themeSettingsCollapse = new bootstrap.Collapse(themeSettingsEl, {
show: true,
toggle: false
});
if (window.localStorage.getItem('settings_expanded') === 'true') {
themeSettingsCollapse.show();
themeSettingsExpandEl.classList.remove('show');
} else {
themeSettingsCollapse.hide();
themeSettingsExpandEl.classList.add('show');
}
themeSettingsEl.addEventListener('hidden.bs.collapse', function () {
themeSettingsExpandEl.classList.add('show');
window.localStorage.setItem('settings_expanded', false);
});
themeSettingsExpandEl.addEventListener('click', function () {
themeSettingsExpandEl.classList.remove('show');
window.localStorage.setItem('settings_expanded', true);
setTimeout(function() {
themeSettingsCollapse.show();
}, 300);
});
}
// options // options
const breakpoints = { const breakpoints = {
sm: 540, sm: 540,
@@ -84,13 +41,6 @@ d.addEventListener("DOMContentLoaded", function(event) {
}); });
} }
var iconNotifications = d.querySelector('.notification-bell');
if (iconNotifications) {
iconNotifications.addEventListener('shown.bs.dropdown', function () {
iconNotifications.classList.remove('unread');
});
}
[].slice.call(d.querySelectorAll('[data-background]')).map(function(el) { [].slice.call(d.querySelectorAll('[data-background]')).map(function(el) {
el.style.background = 'url(' + el.getAttribute('data-background') + ')'; el.style.background = 'url(' + el.getAttribute('data-background') + ')';
}); });
@@ -112,17 +62,15 @@ d.addEventListener("DOMContentLoaded", function(event) {
//Tooltips //Tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl) return new bootstrap.Tooltip(tooltipTriggerEl)
}) })
// Popovers // Popovers
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl) return new bootstrap.Popover(popoverTriggerEl)
}) })
// Datepicker // Datepicker
var datepickers = [].slice.call(d.querySelectorAll('[data-datepicker]')) var datepickers = [].slice.call(d.querySelectorAll('[data-datepicker]'))
var datepickersList = datepickers.map(function (el) { var datepickersList = datepickers.map(function (el) {
@@ -131,164 +79,6 @@ d.addEventListener("DOMContentLoaded", function(event) {
}); });
}) })
if(d.querySelector('.input-slider-container')) {
[].slice.call(d.querySelectorAll('.input-slider-container')).map(function(el) {
var slider = el.querySelector(':scope .input-slider');
var sliderId = slider.getAttribute('id');
var minValue = slider.getAttribute('data-range-value-min');
var maxValue = slider.getAttribute('data-range-value-max');
var sliderValue = el.querySelector(':scope .range-slider-value');
var sliderValueId = sliderValue.getAttribute('id');
var startValue = sliderValue.getAttribute('data-range-value-low');
var c = d.getElementById(sliderId),
id = d.getElementById(sliderValueId);
noUiSlider.create(c, {
start: [parseInt(startValue)],
connect: [true, false],
//step: 1000,
range: {
'min': [parseInt(minValue)],
'max': [parseInt(maxValue)]
}
});
});
}
if (d.getElementById('input-slider-range')) {
var c = d.getElementById("input-slider-range"),
low = d.getElementById("input-slider-range-value-low"),
e = d.getElementById("input-slider-range-value-high"),
f = [d, e];
noUiSlider.create(c, {
start: [parseInt(low.getAttribute('data-range-value-low')), parseInt(e.getAttribute('data-range-value-high'))],
connect: !0,
tooltips: true,
range: {
min: parseInt(c.getAttribute('data-range-value-min')),
max: parseInt(c.getAttribute('data-range-value-max'))
}
}), c.noUiSlider.on("update", function (a, b) {
f[b].textContent = a[b]
});
}
//Chartist
if(d.querySelector('.ct-chart-sales-value')) {
//Chart 5
new Chartist.Line('.ct-chart-sales-value', {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
series: [
[0, 10, 30, 40, 80, 60, 100]
]
}, {
low: 0,
showArea: true,
fullWidth: true,
plugins: [
Chartist.plugins.tooltip()
],
axisX: {
// On the x-axis start means top and end means bottom
position: 'end',
showGrid: true
},
axisY: {
// On the y-axis start means left and end means right
showGrid: false,
showLabel: false,
labelInterpolationFnc: function(value) {
return '$' + (value / 1) + 'k';
}
}
});
}
if(d.querySelector('.ct-chart-ranking')) {
var chart = new Chartist.Bar('.ct-chart-ranking', {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
series: [
[1, 5, 2, 5, 4, 3],
[2, 3, 4, 8, 1, 2],
]
}, {
low: 0,
showArea: true,
plugins: [
Chartist.plugins.tooltip()
],
axisX: {
// On the x-axis start means top and end means bottom
position: 'end'
},
axisY: {
// On the y-axis start means left and end means right
showGrid: false,
showLabel: false,
offset: 0
}
});
chart.on('draw', function(data) {
if(data.type === 'line' || data.type === 'area') {
data.element.animate({
d: {
begin: 2000 * data.index,
dur: 2000,
from: data.path.clone().scale(1, 0).translate(0, data.chartRect.height()).stringify(),
to: data.path.clone().stringify(),
easing: Chartist.Svg.Easing.easeOutQuint
}
});
}
});
}
if(d.querySelector('.ct-chart-traffic-share')) {
var data = {
series: [70, 20, 10]
};
var sum = function(a, b) { return a + b };
new Chartist.Pie('.ct-chart-traffic-share', data, {
labelInterpolationFnc: function(value) {
return Math.round(value / data.series.reduce(sum) * 100) + '%';
},
low: 0,
high: 8,
donut: true,
donutWidth: 20,
donutSolid: true,
fullWidth: false,
showLabel: false,
plugins: [
Chartist.plugins.tooltip()
],
});
}
if (d.getElementById('loadOnClick')) {
d.getElementById('loadOnClick').addEventListener('click', function () {
var button = this;
var loadContent = d.getElementById('extraContent');
var allLoaded = d.getElementById('allLoadedText');
button.classList.add('btn-loading');
button.setAttribute('disabled', 'true');
setTimeout(function () {
loadContent.style.display = 'block';
button.style.display = 'none';
allLoaded.style.display = 'block';
}, 1500);
});
}
var scroll = new SmoothScroll('a[href*="#"]', { var scroll = new SmoothScroll('a[href*="#"]', {
speed: 500, speed: 500,
speedAsDuration: true speedAsDuration: true
@@ -297,68 +87,4 @@ d.addEventListener("DOMContentLoaded", function(event) {
if(d.querySelector('.current-year')){ if(d.querySelector('.current-year')){
d.querySelector('.current-year').textContent = new Date().getFullYear(); d.querySelector('.current-year').textContent = new Date().getFullYear();
} }
// Glide JS
if (d.querySelector('.glide')) {
new Glide('.glide', {
type: 'carousel',
startAt: 0,
perView: 3
}).mount();
}
if (d.querySelector('.glide-testimonials')) {
new Glide('.glide-testimonials', {
type: 'carousel',
startAt: 0,
perView: 1,
autoplay: 2000
}).mount();
}
if (d.querySelector('.glide-clients')) {
new Glide('.glide-clients', {
type: 'carousel',
startAt: 0,
perView: 5,
autoplay: 2000
}).mount();
}
if (d.querySelector('.glide-news-widget')) {
new Glide('.glide-news-widget', {
type: 'carousel',
startAt: 0,
perView: 1,
autoplay: 2000
}).mount();
}
if (d.querySelector('.glide-autoplay')) {
new Glide('.glide-autoplay', {
type: 'carousel',
startAt: 0,
perView: 3,
autoplay: 2000
}).mount();
}
// Pricing countup
var billingSwitchEl = d.getElementById('billingSwitch');
if(billingSwitchEl) {
const countUpStandard = new countUp.CountUp('priceStandard', 99, { startVal: 199 });
const countUpPremium = new countUp.CountUp('pricePremium', 199, { startVal: 299 });
billingSwitchEl.addEventListener('change', function() {
if(billingSwitch.checked) {
countUpStandard.start();
countUpPremium.start();
} else {
countUpStandard.reset();
countUpPremium.reset();
}
});
}
}); });

View File

@@ -1,2 +1,6 @@
<?php <?php
use Illuminate\Support\Facades\Schedule;
$timezone = config('app.user_timezone');
Schedule::command(\App\Console\Commands\Files\DeleteOldFilesFromStorage::class)->timezone($timezone)->dailyAt('3:30');

View File

@@ -6,6 +6,22 @@ Route::get('/', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])
Route::middleware([\App\Http\Middleware\IsProject::class, \App\Http\Middleware\IsWebsiteTranslations::class])->group(function () { Route::middleware([\App\Http\Middleware\IsProject::class, \App\Http\Middleware\IsWebsiteTranslations::class])->group(function () {
Route::get('/language/{language}', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home-language'); Route::get('/language/{language}', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home-language');
Route::prefix('docs')->group(function () {
Route::get('/', [\App\Http\Controllers\Site\DocumentationController::class, 'defaultVersion'])->name('documentation');
Route::get('language/{language}', [\App\Http\Controllers\Site\DocumentationController::class, 'defaultVersion'])->name('documentation-language');
Route::middleware([\App\Http\Middleware\DocumentationVersion::class])->prefix('{version}')->group(function () {
Route::get('/', [\App\Http\Controllers\Site\DocumentationController::class, 'index'])->name('documentation.version');
Route::get('language/{language}', [\App\Http\Controllers\Site\DocumentationController::class, 'index'])->name('documentation.version-language');
Route::get('category/{slug}', [\App\Http\Controllers\Site\DocumentationController::class, 'category'])->name('documentation.category');
Route::get('category/{slug}/language/{language}', [\App\Http\Controllers\Site\DocumentationController::class, 'category'])->name('documentation.category-language');
Route::get('{slug}', [\App\Http\Controllers\Site\DocumentationController::class, 'view'])->name('documentation.view');
Route::get('{slug}/language/{language}', [\App\Http\Controllers\Site\DocumentationController::class, 'view'])->name('documentation.view-language');
});
});
Route::prefix('feedback')->group(function () { Route::prefix('feedback')->group(function () {
Route::get('/', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback'); Route::get('/', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback');
Route::get('language/{language}', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback-language'); Route::get('language/{language}', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback-language');

View File

@@ -15,6 +15,8 @@ export default defineConfig({
'resources/site/js/app.js', 'resources/site/js/app.js',
'resources/prism/app.js', 'resources/prism/app.js',
'resources/fancybox/app.js',
], ],
refresh: true, refresh: true,
}), }),

View File

@@ -1,8 +0,0 @@
**/*.env
**/*.env.example
application/bootstrap/cache/*
application/storage/**
application/vendor/**
application/node_modules/**
application/public/build/**
application/public/storage

View File

@@ -64,10 +64,12 @@ FROM BUILD AS PRODUCTION
COPY --from=APP_BUILD_FOR_PRODUCTION /home/app /var/www/html COPY --from=APP_BUILD_FOR_PRODUCTION /home/app /var/www/html
COPY docker/docker-entrypoint_prod.sh /home/unit/docker-entrypoint.sh COPY docker/docker-entrypoint_prod.sh /home/unit/docker-entrypoint.sh
COPY docker/start.sh /usr/local/bin/start
WORKDIR /var/www/html WORKDIR /var/www/html
RUN chmod 755 /home/unit/docker-entrypoint.sh RUN chmod 755 /home/unit/docker-entrypoint.sh \
&& chmod 755 /usr/local/bin/start
STOPSIGNAL SIGTERM STOPSIGNAL SIGTERM
@@ -81,17 +83,19 @@ FROM BUILD AS DEVELOP
WORKDIR /var/www/html WORKDIR /var/www/html
COPY docker/docker-entrypoint_dev.sh /home/unit/docker-entrypoint.sh COPY docker/docker-entrypoint_dev.sh /home/unit/docker-entrypoint.sh
COPY docker/start.sh /usr/local/bin/start
STOPSIGNAL SIGTERM STOPSIGNAL SIGTERM
RUN chmod 755 /home/unit/docker-entrypoint.sh \ RUN chmod 755 /home/unit/docker-entrypoint.sh \
&& chmod 755 /usr/local/bin/start \
&& apk --no-cache add git nodejs npm \ && apk --no-cache add git nodejs npm \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
ENTRYPOINT ["/home/unit/docker-entrypoint.sh"] ENTRYPOINT ["/home/unit/docker-entrypoint.sh"]
EXPOSE 9000 EXPOSE 9000
CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock", "--user", "unit", "--group", "unit"] CMD ["/usr/local/bin/start"]

View File

@@ -5,6 +5,7 @@ set -euo pipefail
WAITLOOPS=5 WAITLOOPS=5
SLEEPSEC=1 SLEEPSEC=1
unitd="unitd" unitd="unitd"
role=${CONTAINER_ROLE:-app}
curl_put() curl_put()
{ {
@@ -22,7 +23,7 @@ curl_put()
return 0 return 0
} }
if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then if [ "$role" = "app" ]; then
echo "$0: Launching Unit daemon to perform initial configuration..." echo "$0: Launching Unit daemon to perform initial configuration..."
/usr/sbin/$unitd --control unix:/var/run/control.unit.sock /usr/sbin/$unitd --control unix:/var/run/control.unit.sock
for i in $(/usr/bin/seq $WAITLOOPS); do for i in $(/usr/bin/seq $WAITLOOPS); do
@@ -61,7 +62,7 @@ if [ "$unitd" = "unitd" ] || [ "$unitd" = "unitd-debug" ]; then
if [ ! -z ${UNIT_SOURCE+x} ] if [ ! -z ${UNIT_SOURCE+x} ]
then then
echo "[${UNIT_SOURCE}]" > /docker-entrypoint.d/unit_source.json echo $UNIT_SOURCE > /docker-entrypoint.d/unit_source.json
curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source" curl_put "/docker-entrypoint.d/unit_source.json" "config/listeners/*:9000/forwarded/source"
fi fi

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