From e74456ee2fdd393f3d6c20ad9ce9ef4d4a18cb34 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 May 2024 19:47:02 +0500 Subject: [PATCH] A documentation section has been added to the site. --- .../app/Dto/Builder/Documentation.php | 10 +- .../app/Dto/Builder/DocumentationCategory.php | 10 +- .../DocumentationCategory/Category.php | 20 +++ .../Project/Translation/Translations.php | 14 +- .../app/Dto/Service/Site/Documentation.php | 47 ++++++ app/application/app/Enums/CacheTag.php | 1 + .../app/Enums/DocumentationVersionStatus.php | 14 +- .../app/Enums/Site/ProjectSection.php | 22 ++- .../Site/DocumentationController.php | 96 +++++++++++ .../Http/Middleware/DocumentationVersion.php | 40 +++++ app/application/app/Models/Documentation.php | 3 + .../app/Models/DocumentationVersion.php | 11 ++ .../app/Providers/AppServiceProvider.php | 2 + .../DocumentationCategoryRepository.php | 15 +- .../Repositories/DocumentationRepository.php | 16 +- .../DocumentationService/DefaultVersion.php | 17 ++ .../Project/DocumentationVersionService.php | 6 + .../Services/Documentation/BuilderCommand.php | 12 +- .../DocumentationCategory/BuilderCommand.php | 12 +- .../Services/Site/DocumentationService.php | 151 ++++++++++++++++++ .../View/Components/Site/ChooseVersion.php | 48 ++++++ .../Components/Site/DocumentationVersion.php | 23 +++ app/application/lang/en/site.php | 5 + app/application/lang/ru/site.php | 5 + .../site/js/_choose-documentation-version.js | 8 + app/application/resources/site/js/app.js | 1 + app/application/resources/site/scss/app.scss | 99 +++++++++++- .../components/site/choose-version.blade.php | 18 +++ .../site/documentation-version.blade.php | 10 ++ .../resources/views/layout/site.blade.php | 7 + .../projects/documentation/category.blade.php | 13 ++ .../projects/documentation/index.blade.php | 13 ++ .../no-default-version.blade.php | 6 + .../projects/documentation/view.blade.php | 9 ++ app/application/routes/web-project.php | 16 ++ 35 files changed, 766 insertions(+), 34 deletions(-) create mode 100644 app/application/app/Dto/Builder/DocumentationCategory/Category.php create mode 100644 app/application/app/Dto/Service/Site/Documentation.php create mode 100644 app/application/app/Http/Controllers/Site/DocumentationController.php create mode 100644 app/application/app/Http/Middleware/DocumentationVersion.php create mode 100644 app/application/app/ServiceResults/Site/DocumentationService/DefaultVersion.php create mode 100644 app/application/app/Services/Site/DocumentationService.php create mode 100644 app/application/app/View/Components/Site/ChooseVersion.php create mode 100644 app/application/app/View/Components/Site/DocumentationVersion.php create mode 100644 app/application/resources/site/js/_choose-documentation-version.js create mode 100644 app/application/resources/views/components/site/choose-version.blade.php create mode 100644 app/application/resources/views/components/site/documentation-version.blade.php create mode 100644 app/application/resources/views/site/projects/documentation/category.blade.php create mode 100644 app/application/resources/views/site/projects/documentation/index.blade.php create mode 100644 app/application/resources/views/site/projects/documentation/no-default-version.blade.php create mode 100644 app/application/resources/views/site/projects/documentation/view.blade.php diff --git a/app/application/app/Dto/Builder/Documentation.php b/app/application/app/Dto/Builder/Documentation.php index fdec08d..c0202de 100644 --- a/app/application/app/Dto/Builder/Documentation.php +++ b/app/application/app/Dto/Builder/Documentation.php @@ -2,14 +2,22 @@ namespace App\Dto\Builder; +use App\Dto\Builder\DocumentationCategory\Category; + final readonly class Documentation { public function __construct( - private ?bool $isPublic = null, + private ?bool $isPublic = null, + private ?Category $categoryId = null, ) { } public function isPublic(): ?bool { return $this->isPublic; } + + public function getCategoryId(): ?Category + { + return $this->categoryId; + } } diff --git a/app/application/app/Dto/Builder/DocumentationCategory.php b/app/application/app/Dto/Builder/DocumentationCategory.php index 242a20c..7bdc68c 100644 --- a/app/application/app/Dto/Builder/DocumentationCategory.php +++ b/app/application/app/Dto/Builder/DocumentationCategory.php @@ -2,14 +2,22 @@ namespace App\Dto\Builder; +use App\Dto\Builder\DocumentationCategory\Category; + final readonly class DocumentationCategory { public function __construct( - private ?bool $isPublic = null, + private ?bool $isPublic = null, + private ?Category $parentId = null, ) { } public function isPublic(): ?bool { return $this->isPublic; } + + public function getParentId(): ?Category + { + return $this->parentId; + } } diff --git a/app/application/app/Dto/Builder/DocumentationCategory/Category.php b/app/application/app/Dto/Builder/DocumentationCategory/Category.php new file mode 100644 index 0000000..73e0e3a --- /dev/null +++ b/app/application/app/Dto/Builder/DocumentationCategory/Category.php @@ -0,0 +1,20 @@ +categoryId; + } + + public function isCategoryNull(): bool + { + return $this->getCategoryId() === null; + } +} diff --git a/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php b/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php index ed702cb..f7831bb 100644 --- a/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php +++ b/app/application/app/Dto/Service/Admin/Project/Translation/Translations.php @@ -2,6 +2,7 @@ namespace App\Dto\Service\Admin\Project\Translation; +use App\Enums\DocumentationVersionStatus; use App\Exceptions\Dto\Admin\Project\Transaction\TranslationsException; final class Translations @@ -23,7 +24,7 @@ public function getTranslations(): array public static function getTranslationCodes(): array { - return [ + $translations = [ 'site.Menu', 'site.Powered by service', 'site.About project', @@ -38,6 +39,17 @@ public static function getTranslationCodes(): array 'site.attributes.message', 'site.Message sent successfully', '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; } } diff --git a/app/application/app/Dto/Service/Site/Documentation.php b/app/application/app/Dto/Service/Site/Documentation.php new file mode 100644 index 0000000..a8d141c --- /dev/null +++ b/app/application/app/Dto/Service/Site/Documentation.php @@ -0,0 +1,47 @@ +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(), + ]; + } +} diff --git a/app/application/app/Enums/CacheTag.php b/app/application/app/Enums/CacheTag.php index 316900c..a9e7b6f 100644 --- a/app/application/app/Enums/CacheTag.php +++ b/app/application/app/Enums/CacheTag.php @@ -9,6 +9,7 @@ enum CacheTag: string { case Project = 'project'; case ProjectTranslation = 'project_translation'; + case DocumantationVersion = 'documantation_version'; public function getCache(): TaggedCache { diff --git a/app/application/app/Enums/DocumentationVersionStatus.php b/app/application/app/Enums/DocumentationVersionStatus.php index 14ab233..7cb4c9d 100644 --- a/app/application/app/Enums/DocumentationVersionStatus.php +++ b/app/application/app/Enums/DocumentationVersionStatus.php @@ -2,6 +2,7 @@ namespace App\Enums; +use App\Services\WebsiteTranslations; use Illuminate\Support\Collection; enum DocumentationVersionStatus: int @@ -11,9 +12,18 @@ enum DocumentationVersionStatus: int case CurrentVersion = 100; 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 diff --git a/app/application/app/Enums/Site/ProjectSection.php b/app/application/app/Enums/Site/ProjectSection.php index 740c7d8..b61c8ec 100644 --- a/app/application/app/Enums/Site/ProjectSection.php +++ b/app/application/app/Enums/Site/ProjectSection.php @@ -5,15 +5,18 @@ use App\Models\Project; use App\Models\ProjectLanguage; -enum ProjectSection +enum ProjectSection: string { - case Home; - case Feedback; - case FeedbackSend; + case Home = 'home'; + case Feedback = 'feedback'; + 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 = ''; if ($project->http_host === null) { $prefixProject = 'project.'; @@ -26,12 +29,7 @@ public function url(Project $project, ?ProjectLanguage $language = null): string $prefixLanguage = '-language'; } - $route = match ($this) { - 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), - }; + $route = \route($prefixProject . $this->value . $prefixLanguage, $parameters, false); return $project->http_host . $route; } diff --git a/app/application/app/Http/Controllers/Site/DocumentationController.php b/app/application/app/Http/Controllers/Site/DocumentationController.php new file mode 100644 index 0000000..feb935e --- /dev/null +++ b/app/application/app/Http/Controllers/Site/DocumentationController.php @@ -0,0 +1,96 @@ +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()); + } +} diff --git a/app/application/app/Http/Middleware/DocumentationVersion.php b/app/application/app/Http/Middleware/DocumentationVersion.php new file mode 100644 index 0000000..e71fb39 --- /dev/null +++ b/app/application/app/Http/Middleware/DocumentationVersion.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/app/application/app/Models/Documentation.php b/app/application/app/Models/Documentation.php index e237937..dd11136 100644 --- a/app/application/app/Models/Documentation.php +++ b/app/application/app/Models/Documentation.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\Scopes\SortScope; +use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -9,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; +#[ScopedBy([SortScope::class])] final class Documentation extends Model { use HasFactory, SoftDeletes; diff --git a/app/application/app/Models/DocumentationVersion.php b/app/application/app/Models/DocumentationVersion.php index 384b08d..4025555 100644 --- a/app/application/app/Models/DocumentationVersion.php +++ b/app/application/app/Models/DocumentationVersion.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Enums\DocumentationVersionStatus; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -37,6 +38,16 @@ final class DocumentationVersion extends Model '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. * diff --git a/app/application/app/Providers/AppServiceProvider.php b/app/application/app/Providers/AppServiceProvider.php index 20abed2..670578a 100644 --- a/app/application/app/Providers/AppServiceProvider.php +++ b/app/application/app/Providers/AppServiceProvider.php @@ -59,6 +59,8 @@ public function boot(): void Route::pattern('language', '[a-z_]+'); Route::pattern('project', '[a-z0-9_-]+'); + Route::pattern('slug', '[a-z0-9._-]+'); + Route::pattern('version', '[a-z0-9._-]+'); $this->configureRateLimiting(); Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']); diff --git a/app/application/app/Repositories/DocumentationCategoryRepository.php b/app/application/app/Repositories/DocumentationCategoryRepository.php index 2f9f47f..81e2d65 100644 --- a/app/application/app/Repositories/DocumentationCategoryRepository.php +++ b/app/application/app/Repositories/DocumentationCategoryRepository.php @@ -4,6 +4,7 @@ use App\Contracts\Search; use App\Models\DocumentationCategory; +use App\Models\DocumentationVersion; use App\Models\ProjectLanguage; use App\Services\DocumentationCategory\BuilderCommand; use App\Services\Search\CreateSearchInstanceCommand; @@ -35,9 +36,19 @@ public function getCategoryById(int $id): ?DocumentationCategory 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 diff --git a/app/application/app/Repositories/DocumentationRepository.php b/app/application/app/Repositories/DocumentationRepository.php index 2a2072f..d6d22e8 100644 --- a/app/application/app/Repositories/DocumentationRepository.php +++ b/app/application/app/Repositories/DocumentationRepository.php @@ -5,9 +5,11 @@ use App\Dto\Builder\Documentation as DocumentationBuilderDto; use App\Contracts\Search; use App\Models\Documentation; +use App\Models\ProjectLanguage; use App\Services\Documentation\BuilderCommand; use App\Services\Search\CreateSearchInstanceCommand; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Support\Str; final readonly class DocumentationRepository @@ -32,9 +34,19 @@ public function getDocumentationById(int $id): ?Documentation 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 diff --git a/app/application/app/ServiceResults/Site/DocumentationService/DefaultVersion.php b/app/application/app/ServiceResults/Site/DocumentationService/DefaultVersion.php new file mode 100644 index 0000000..f329d70 --- /dev/null +++ b/app/application/app/ServiceResults/Site/DocumentationService/DefaultVersion.php @@ -0,0 +1,17 @@ +version; + } +} diff --git a/app/application/app/Services/Admin/Project/DocumentationVersionService.php b/app/application/app/Services/Admin/Project/DocumentationVersionService.php index ae9d904..22ef657 100644 --- a/app/application/app/Services/Admin/Project/DocumentationVersionService.php +++ b/app/application/app/Services/Admin/Project/DocumentationVersionService.php @@ -5,6 +5,7 @@ use App\Dto\QuerySettingsDto; use App\Dto\Builder\DocumentationVersion as DocumentationVersionBuilderDto; use App\Dto\Service\Admin\Project\DocumentationVersion\StoreUpdate; +use App\Enums\CacheTag; use App\Enums\DocumentationVersionStatus; use App\Models\DocumentationVersion; use App\Models\User; @@ -14,6 +15,7 @@ use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\StoreUpdateResult; +use App\Services\ClearCacheCommandHandler; use App\Services\DocumentationVersion\DocumentationVersionCommandHandler; use App\Services\Service; use Illuminate\Support\Facades\DB; @@ -24,6 +26,7 @@ public function __construct( private readonly ProjectRepository $projectRepository, private readonly DocumentationVersionRepository $documentationVersionRepository, private readonly DocumentationVersionCommandHandler $documentationVersionCommandHandler, + private readonly ClearCacheCommandHandler $clearCacheCommandHandler, ) { } public function index(int $projectId, DocumentationVersionBuilderDto $documentationVersionBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray @@ -138,6 +141,7 @@ public function store(int $projectId, StoreUpdate $data, User $user): ServiceRes $dataVersion = $this->getDataVersion($data); return $this->documentationVersionCommandHandler->handleStore($project, $dataVersion); }); + $this->clearCacheCommandHandler->byTag(CacheTag::DocumantationVersion); } catch (\Throwable $e) { report($e); return $this->errService(__('Server Error')); @@ -174,6 +178,7 @@ public function update(int $projectId, int $versionId, StoreUpdate $data, User $ $dataVersion = $this->getDataVersion($data); return $this->documentationVersionCommandHandler->handleUpdate($version, $dataVersion); }); + $this->clearCacheCommandHandler->byTag(CacheTag::DocumantationVersion); } catch (\Throwable $e) { report($e); return $this->errService(__('Server Error')); @@ -202,6 +207,7 @@ public function destroy(int $projectId, int $versionId, User $user): ServiceResu DB::transaction(function () use ($version) { $this->documentationVersionCommandHandler->handleDestroy($version); }); + $this->clearCacheCommandHandler->byTag(CacheTag::DocumantationVersion); } catch (\Throwable $e) { report($e); return $this->errService(__('Server Error')); diff --git a/app/application/app/Services/Documentation/BuilderCommand.php b/app/application/app/Services/Documentation/BuilderCommand.php index 24485cc..65c14bf 100644 --- a/app/application/app/Services/Documentation/BuilderCommand.php +++ b/app/application/app/Services/Documentation/BuilderCommand.php @@ -10,10 +10,12 @@ { public function execute(Relation | Builder $query, DocumentationBuilderDto $documentationBuilderDto): Relation | Builder { - if ($documentationBuilderDto->isPublic() !== null) { - $query->where('is_public', $documentationBuilderDto->isPublic()); - } - - return $query; + return $query + ->when($documentationBuilderDto->isPublic(), function (Builder $query) use ($documentationBuilderDto) { + $query->where('is_public', $documentationBuilderDto->isPublic()); + }) + ->when($documentationBuilderDto->getCategoryId(), function (Builder $query) use ($documentationBuilderDto) { + $query->where('category_id', $documentationBuilderDto->getCategoryId()->getCategoryId()); + }); } } diff --git a/app/application/app/Services/DocumentationCategory/BuilderCommand.php b/app/application/app/Services/DocumentationCategory/BuilderCommand.php index c7e2b8c..2eaf1d6 100644 --- a/app/application/app/Services/DocumentationCategory/BuilderCommand.php +++ b/app/application/app/Services/DocumentationCategory/BuilderCommand.php @@ -10,10 +10,12 @@ { public function execute(Relation | Builder $query, DocumentationCategoryBuilderDto $documentationCategoryBuilderDto): Relation | Builder { - if ($documentationCategoryBuilderDto->isPublic() !== null) { - $query->where('is_public', $documentationCategoryBuilderDto->isPublic()); - } - - return $query; + return $query + ->when($documentationCategoryBuilderDto->isPublic(), function (Builder $query) use ($documentationCategoryBuilderDto) { + $query->where('is_public', $documentationCategoryBuilderDto->isPublic()); + }) + ->when($documentationCategoryBuilderDto->getParentId(), function (Builder $query) use ($documentationCategoryBuilderDto) { + $query->where('parent_id', $documentationCategoryBuilderDto->getParentId()->getCategoryId()); + }); } } diff --git a/app/application/app/Services/Site/DocumentationService.php b/app/application/app/Services/Site/DocumentationService.php new file mode 100644 index 0000000..1c9bfaf --- /dev/null +++ b/app/application/app/Services/Site/DocumentationService.php @@ -0,0 +1,151 @@ +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(); + } +} diff --git a/app/application/app/View/Components/Site/ChooseVersion.php b/app/application/app/View/Components/Site/ChooseVersion.php new file mode 100644 index 0000000..e2f63fb --- /dev/null +++ b/app/application/app/View/Components/Site/ChooseVersion.php @@ -0,0 +1,48 @@ +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, + ]); + } +} diff --git a/app/application/app/View/Components/Site/DocumentationVersion.php b/app/application/app/View/Components/Site/DocumentationVersion.php new file mode 100644 index 0000000..dfd0dc0 --- /dev/null +++ b/app/application/app/View/Components/Site/DocumentationVersion.php @@ -0,0 +1,23 @@ + $this->websiteTranslations, + 'version' => $this->version, + ]); + } +} diff --git a/app/application/lang/en/site.php b/app/application/lang/en/site.php index edb4844..21b7500 100644 --- a/app/application/lang/en/site.php +++ b/app/application/lang/en/site.php @@ -16,4 +16,9 @@ 'email' => 'email', '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.', ]; diff --git a/app/application/lang/ru/site.php b/app/application/lang/ru/site.php index c7fa6e4..188f154 100644 --- a/app/application/lang/ru/site.php +++ b/app/application/lang/ru/site.php @@ -16,4 +16,9 @@ 'email' => 'email', 'message' => 'сообщение', ], + 'Documentation' => 'Документация', + 'Documentation not created' => 'Документация не создана', + 'Choose version' => 'Выберите версию', + 'alert-status-not-supported' => 'ВНИМАНИЕ! Вы просматриваете документацию для старой версии.', + 'alert-status-future' => 'ВНИМАНИЕ! Вы просматриваете документацию к будущей версии. Документация и функции этого выпуска могут быть изменены.', ]; diff --git a/app/application/resources/site/js/_choose-documentation-version.js b/app/application/resources/site/js/_choose-documentation-version.js new file mode 100644 index 0000000..ba5ac52 --- /dev/null +++ b/app/application/resources/site/js/_choose-documentation-version.js @@ -0,0 +1,8 @@ +let blockDocumentationVersion = document.querySelector('#documentation-version'); +blockDocumentationVersion.querySelector('.documentation-version__button').addEventListener('click', (e) => { + if (blockDocumentationVersion.classList.contains('active')) { + blockDocumentationVersion.classList.remove('active'); + } else { + blockDocumentationVersion.classList.add('active'); + } +}); diff --git a/app/application/resources/site/js/app.js b/app/application/resources/site/js/app.js index ade6ee6..9d10b37 100644 --- a/app/application/resources/site/js/app.js +++ b/app/application/resources/site/js/app.js @@ -1,2 +1,3 @@ import './_menu.js'; import './_choose-language.js'; +import './_choose-documentation-version.js'; diff --git a/app/application/resources/site/scss/app.scss b/app/application/resources/site/scss/app.scss index 3236656..3438838 100644 --- a/app/application/resources/site/scss/app.scss +++ b/app/application/resources/site/scss/app.scss @@ -43,7 +43,8 @@ body { grid-template-columns: 1fr 1fr; grid-template-areas: "logo menu" - "language language"; + "language language" + "documentation-version documentation-version"; padding: 8px; box-shadow: 0 3px 9px rgba(0,0,0,0.48); background: #eee; @@ -195,6 +196,77 @@ body.mobile-menu-open { 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 { display: flex; flex-direction: column; @@ -225,6 +297,15 @@ body.mobile-menu-open { 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) { body { @@ -232,8 +313,8 @@ body.mobile-menu-open { margin: 0 auto; } .header { - grid-template-areas: "logo language"; - grid-template-columns: 1fr 200px; + grid-template-areas: "logo documentation-version language"; + grid-template-columns: 1fr 200px 200px; } .header_logo { width: 150px; @@ -250,6 +331,7 @@ body.mobile-menu-open { .main-container { flex-direction: row; } + #language { margin-right: 10px; } @@ -259,6 +341,17 @@ body.mobile-menu-open { .language__button { 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 { flex: none; background: #eee; diff --git a/app/application/resources/views/components/site/choose-version.blade.php b/app/application/resources/views/components/site/choose-version.blade.php new file mode 100644 index 0000000..9f6820c --- /dev/null +++ b/app/application/resources/views/components/site/choose-version.blade.php @@ -0,0 +1,18 @@ +
+
+ + +
+
diff --git a/app/application/resources/views/components/site/documentation-version.blade.php b/app/application/resources/views/components/site/documentation-version.blade.php new file mode 100644 index 0000000..816cec0 --- /dev/null +++ b/app/application/resources/views/components/site/documentation-version.blade.php @@ -0,0 +1,10 @@ +@if($version->status === \App\Enums\DocumentationVersionStatus::NotSupported) +
+ {{ $websiteTranslations->translate('site.alert-status-not-supported') }} +
+@endif +@if($version->status === \App\Enums\DocumentationVersionStatus::FutureVersion) +
+ {{ $websiteTranslations->translate('site.alert-status-future') }} +
+@endif diff --git a/app/application/resources/views/layout/site.blade.php b/app/application/resources/views/layout/site.blade.php index 570c009..2026b25 100644 --- a/app/application/resources/views/layout/site.blade.php +++ b/app/application/resources/views/layout/site.blade.php @@ -30,6 +30,9 @@ + @if($attributes->has('documentationVersion')) + + @endif
@@ -37,11 +40,15 @@
+ @if($attributes->has('documentationVersion')) + + @endif

@yield('h1', '')

@includeWhen($errors->any(), 'layout.site._errors', ['errors' => $errors->all()]) diff --git a/app/application/resources/views/site/projects/documentation/category.blade.php b/app/application/resources/views/site/projects/documentation/category.blade.php new file mode 100644 index 0000000..06e4931 --- /dev/null +++ b/app/application/resources/views/site/projects/documentation/category.blade.php @@ -0,0 +1,13 @@ +@section('meta_title', $category->content?->title . ' - ' . $project->name . ' ' . $version->title) +@section('h1', $category->content?->title) + + + @foreach($categories as $category) + @continue(! $category->content?->title) +

{{ $category->content->title }}

+ @endforeach + @foreach($documentations as $documentation) + @continue(! $documentation->content?->title) +

{{ $documentation->content->title }}

+ @endforeach +
diff --git a/app/application/resources/views/site/projects/documentation/index.blade.php b/app/application/resources/views/site/projects/documentation/index.blade.php new file mode 100644 index 0000000..9391de0 --- /dev/null +++ b/app/application/resources/views/site/projects/documentation/index.blade.php @@ -0,0 +1,13 @@ +@section('meta_title', $websiteTranslations->translate('site.Documentation') . ' - ' . $project->name . ' ' . $version->title) +@section('h1', $websiteTranslations->translate('site.Documentation')) + + + @foreach($categories as $category) + @continue(! $category->content?->title) +

{{ $category->content->title }}

+ @endforeach + @foreach($documentations as $documentation) + @continue(! $documentation->content?->title) +

{{ $documentation->content->title }}

+ @endforeach +
diff --git a/app/application/resources/views/site/projects/documentation/no-default-version.blade.php b/app/application/resources/views/site/projects/documentation/no-default-version.blade.php new file mode 100644 index 0000000..464dbae --- /dev/null +++ b/app/application/resources/views/site/projects/documentation/no-default-version.blade.php @@ -0,0 +1,6 @@ +@section('meta_title', $websiteTranslations->translate('site.Documentation not created')) +@section('h1', $websiteTranslations->translate('site.Documentation not created')) + + + + diff --git a/app/application/resources/views/site/projects/documentation/view.blade.php b/app/application/resources/views/site/projects/documentation/view.blade.php new file mode 100644 index 0000000..c831e15 --- /dev/null +++ b/app/application/resources/views/site/projects/documentation/view.blade.php @@ -0,0 +1,9 @@ +@section('meta_title', $documentation->content?->title . ' - ' . $project->name . ' ' . $version->title) +@section('h1', $documentation->content?->title) + + +
{!! $documentation->content->content !!}
+ @push('scripts') + @include('_prism') + @endpush +
diff --git a/app/application/routes/web-project.php b/app/application/routes/web-project.php index 330eff2..8dfef3e 100644 --- a/app/application/routes/web-project.php +++ b/app/application/routes/web-project.php @@ -6,6 +6,22 @@ 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::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::get('/', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback'); Route::get('language/{language}', [\App\Http\Controllers\Site\FeedbackController::class, 'index'])->name('feedback-language');