Added the ability to add project information.

This commit is contained in:
Leonid Nikitin 2024-04-14 15:27:51 +05:00
parent b7d0a2453e
commit 2769585de0
Signed by: kor-elf
GPG Key ID: 3C0F720C170F6E1D
35 changed files with 862 additions and 14 deletions

View File

@ -22,6 +22,8 @@ APP_MAINTENANCE_STORE=database
#LOGIN_MAX_REQUEST=50 #LOGIN_MAX_REQUEST=50
#LOGIN_MAX_EMAIL_REQUEST=10 #LOGIN_MAX_EMAIL_REQUEST=10
TINYMCE_LICENSE_KEY="gpl"
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stack

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Project\About;
use App\Dto\Service\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $title,
private string $description,
) { }
public function getTitle(): string
{
return $this->title;
}
public function getDescription(): string
{
return $this->description;
}
}

View File

@ -8,6 +8,7 @@ enum Permission: string
case Role = 'role'; case Role = 'role';
case User = 'user'; case User = 'user';
case Project = 'project'; case Project = 'project';
case ProjectContent = 'project-content';
public function getPermissions(): array public function getPermissions(): array
{ {
@ -15,6 +16,11 @@ public function getPermissions(): array
self::AdminPanel => [ self::AdminPanel => [
'view' => __('permissions.Administrative panel allowed'), 'view' => __('permissions.Administrative panel allowed'),
], ],
self::ProjectContent => [
'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'),
'update' => __('permissions.Allowed to edit'),
],
default => $this->getBasePermissions() default => $this->getBasePermissions()
}; };

View File

@ -0,0 +1,54 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin\Projects;
use App\Http\Controllers\Admin\Controller;
use App\Http\Requests\Admin\Projects\About\StoreUpdateRequest;
use App\Services\Admin\Project\AboutService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class AboutController extends Controller
{
public function __construct(
private readonly AboutService $aboutService,
) { }
public function languages(int $project, Request $request): View
{
$user = $request->user();
$result = $this->aboutService->languages($project, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/projects/about/languages', $result->getData());
}
public function edit(int $project, int $language, Request $request): View
{
$user = $request->user();
$result = $this->aboutService->edit($project, $language, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/projects/about/edit', $result->getData());
}
public function update(int $project, int $language, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->aboutService->storeOrUpdate($project, $language, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('admin.projects.about.edit', [
'project' => $project,
'language' => $language,
])->withSuccess($result->getMessage());
}
}

View File

@ -34,6 +34,17 @@ public function index(IndexRequest $request): View
return view('admin/projects/index', $result->getData()); return view('admin/projects/index', $result->getData());
} }
public function show(int $id, Request $request): View
{
$user = $request->user();
$result = $this->projectService->show($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('admin/projects/show', $result->getData());
}
public function create(Request $request): View public function create(Request $request): View
{ {
$user = $request->user(); $user = $request->user();

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Projects\About;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Project\About\StoreUpdate;
use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255',],
'description' => ['nullable', 'string',],
];
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
title: $this->input('title'),
description: $this->input('description'),
);
}
}

View File

@ -47,4 +47,9 @@ public function languages(): HasMany
{ {
return $this->hasMany(ProjectLanguage::class); return $this->hasMany(ProjectLanguage::class);
} }
public function contents(): HasMany
{
return $this->hasMany(ProjectContent::class);
}
} }

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
final class ProjectContent extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'project_content';
protected $fillable = [
'title',
'description',
];
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\ProjectContent;
use App\Models\User;
final readonly class ProjectContentPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('project-content.view');
}
public function view(User $user, ProjectContent $projectContent): bool
{
return $user->hasPermission('project-content.view');
}
public function create(User $user): bool
{
return $user->hasPermission('project-content.create');
}
public function update(User $user, ProjectContent $projectContent): bool
{
return $user->hasPermission('project-content.update');
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Models\ProjectContent;
final class ProjectContentRepository
{
public function getContentByLanguageId(int $projectId, int $languageId): ?ProjectContent
{
return ProjectContent::query()
->where(['project_id' => $projectId, 'language_id' => $languageId])
->first();
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Models\ProjectLanguage;
final class ProjectLanguageRepository
{
public function isExistsLanguageById(int $projectId, int $languageId): bool
{
return ProjectLanguage::query()
->where(['id' => $languageId, 'project_id' => $projectId])
->exists();
}
}

View File

@ -0,0 +1,132 @@
<?php declare(strict_types=1);
namespace App\Services\Admin\Project;
use App\Contracts\ServiceResultError;
use App\Dto\Service\Admin\Project\About\StoreUpdate;
use App\Models\ProjectContent;
use App\Models\User;
use App\Repositories\ProjectContentRepository;
use App\Repositories\ProjectLanguageRepository;
use App\Repositories\ProjectRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\StoreUpdateResult;
use App\Services\ProjectContent\ProjectContentCommandHandler;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
final class AboutService extends Service
{
public function __construct(
private readonly ProjectRepository $projectRepository,
private readonly ProjectLanguageRepository $projectLanguageRepository,
private readonly ProjectContentRepository $projectContentRepository,
private readonly ProjectContentCommandHandler $projectContentCommandHandler,
) { }
public function languages(int $projectId, User $user): ServiceResultError | ServiceResultArray
{
$project = $this->projectRepository->getProjectById($projectId);
if (is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('viewAny', ProjectContent::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'project' => $project,
]);
}
public function edit(int $projectId, int $languageId, User $user): ServiceResultError | ServiceResultArray
{
$project = $this->projectRepository->getProjectById($projectId);
$language = $project?->languages()->firstWhere('id', $languageId);
if (\is_null($project) || \is_null($language)) {
return $this->errNotFound(__('Not Found'));
}
$content = $project->contents()->firstWhere('language_id', $languageId);
if (\is_null($content)) {
if ($user->cannot('create', ProjectContent::class)) {
return $this->errFobidden(__('Access is denied'));
}
$content = new ProjectContent();
$content->project_id = $project->id;
$content->language_id = $language->id;
} else if($user->cannot('update', $content)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'project' => $project,
'language' => $language,
'content' => $content,
]);
}
public function storeOrUpdate(int $projectId, int $languageId, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if (! $this->projectLanguageRepository->isExistsLanguageById($projectId, $languageId)) {
return $this->errNotFound(__('Not Found'));
}
$content = $this->projectContentRepository->getContentByLanguageId($projectId, $languageId);
if (is_null($content)) {
return $this->store($projectId, $languageId, $data, $user);
}
return $this->update($content, $data, $user);
}
private function store(int $projectId, int $languageId, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', ProjectContent::class)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$aboutProject = DB::transaction(function () use ($data, $projectId, $languageId) {
$dataAboutProject = $this->getDataAboutProject($data);
return $this->projectContentCommandHandler->handleStore($projectId, $languageId, $dataAboutProject);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($aboutProject, __('Project information has been successfully updated'));
}
private function update(ProjectContent $content, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('update', $content)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$aboutProject = DB::transaction(function () use ($data, $content) {
$dataAboutProject = $this->getDataAboutProject($data);
return $this->projectContentCommandHandler->handleUpdate($content, $dataAboutProject);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($aboutProject, __('Project information has been successfully updated'));
}
private function getDataAboutProject(StoreUpdate $data): array
{
return [
'title' => $data->getTitle(),
'description' => $data->getDescription(),
];
}
}

View File

@ -50,6 +50,23 @@ public function index(ProjectBuilderDto $projectBuilderDto, QuerySettingsDto $qu
]); ]);
} }
public function show(int $id, User $user): ServiceResultError | ServiceResultArray
{
$project = $this->projectRepository->getProjectById($id);
if (is_null($project)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $project)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'project' => $project,
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray public function create(User $user): ServiceResultError | ServiceResultArray
{ {
if ($user->cannot('create', Project::class)) { if ($user->cannot('create', Project::class)) {

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Services\ProjectContent;
use App\Models\ProjectContent;
final readonly class ProjectContentCommandHandler
{
public function handleStore(int $projectId, int $languageId, array $data): ProjectContent
{
$content = new ProjectContent();
$content->project_id = $projectId;
$content->language_id = $languageId;
$content->fill($data);
$content->save();
return $content;
}
public function handleUpdate(ProjectContent $content, array $data): ProjectContent
{
$content->update($data);
return $content;
}
}

View File

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Textarea extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly ?string $value = '',
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.textarea', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'value' => $this->getValue(),
]);
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class TextareaWysiwyg extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly ?string $value = '',
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
/**
* @inheritDoc
*/
public function render(): View
{
$tinymceLicenseKey = config('tinymce.license_key');
return view('components.volt.forms.textarea-wysiwyg', [
'tinymceLicenseKey' => $tinymceLicenseKey,
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'value' => $this->getValue(),
]);
}
}

View File

@ -0,0 +1,4 @@
<?php
return [
'license_key' => (string) env('TINYMCE_LICENSE_KEY', 'gpl'),
];

View File

@ -248,5 +248,6 @@
"Add language": "Add language", "Add language": "Add language",
"There was an error adding a language": "There was an error adding a language", "There was an error adding a language": "There was an error adding a language",
"Select images": "Select images", "Select images": "Select images",
"loading": "loading" "loading": "loading",
"Project information has been successfully updated": "Project information has been successfully updated"
} }

View File

@ -5,4 +5,9 @@
'Users' => 'Users', 'Users' => 'Users',
'AdminPanel' => 'Admin Panel', 'AdminPanel' => 'Admin Panel',
'Projects' => 'Projects', 'Projects' => 'Projects',
'Project' => 'Project',
'Sections' => 'Sections',
'About project' => 'About the project',
'Languages' => 'Languages',
'Last update' => 'Last update',
]; ];

View File

@ -10,4 +10,5 @@
'Administrative panel allowed' => 'Administrative panel allowed', 'Administrative panel allowed' => 'Administrative panel allowed',
'AdminPanel' => 'Administrative panel allowed', 'AdminPanel' => 'Administrative panel allowed',
'Project' => 'Projects', 'Project' => 'Projects',
'ProjectContent' => 'About the project',
]; ];

View File

@ -248,5 +248,6 @@
"Add language": "Добавить язык", "Add language": "Добавить язык",
"There was an error adding a language": "Произошла ошибка при добавлении языка", "There was an error adding a language": "Произошла ошибка при добавлении языка",
"Select images": "Выберите изображения", "Select images": "Выберите изображения",
"loading": "загрузка" "loading": "загрузка",
"Project information has been successfully updated": "Информация о проекте успешно обновлена"
} }

View File

@ -5,4 +5,9 @@
'Users' => 'Пользователи', 'Users' => 'Пользователи',
'AdminPanel' => 'Админка', 'AdminPanel' => 'Админка',
'Projects' => 'Проекты', 'Projects' => 'Проекты',
'Project' => 'Проект',
'Sections' => 'Разделы',
'About project' => 'О проекте',
'Languages' => 'Языки',
'Last update' => 'Последнее обновление',
]; ];

View File

@ -11,4 +11,5 @@
'Administrative panel allowed' => 'Административная панель разрешена', 'Administrative panel allowed' => 'Административная панель разрешена',
'AdminPanel' => 'Административная панель разрешена', 'AdminPanel' => 'Административная панель разрешена',
'Project' => 'Проекты', 'Project' => 'Проекты',
'ProjectContent' => 'О проекте',
]; ];

View File

@ -17,6 +17,7 @@
"simplebar": "^5.3.4", "simplebar": "^5.3.4",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"sweetalert2": "^11.0.18", "sweetalert2": "^11.0.18",
"tinymce": "^7.0.1",
"vanillajs-datepicker": "^1.2.0", "vanillajs-datepicker": "^1.2.0",
"waypoints": "^4.0.1" "waypoints": "^4.0.1"
}, },
@ -24,7 +25,8 @@
"axios": "^1.6.4", "axios": "^1.6.4",
"laravel-vite-plugin": "^1.0", "laravel-vite-plugin": "^1.0",
"sass-loader": "^13.3.2", "sass-loader": "^13.3.2",
"vite": "^5.0" "vite": "^5.0",
"vite-plugin-static-copy": "^1.0.2"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
@ -464,6 +466,41 @@
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
}, },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@popperjs/core": { "node_modules/@popperjs/core": {
"version": "2.11.8", "version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@ -1308,6 +1345,22 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.4"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -1315,6 +1368,15 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -1360,6 +1422,20 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1395,8 +1471,7 @@
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true, "dev": true
"peer": true
}, },
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
@ -1480,6 +1555,18 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/just-extend": { "node_modules/just-extend": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
@ -1536,6 +1623,28 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -1673,6 +1782,26 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/randombytes": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1694,6 +1823,16 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.13.2", "version": "4.13.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz",
@ -1728,6 +1867,29 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1967,6 +2129,11 @@
} }
} }
}, },
"node_modules/tinymce": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.0.1.tgz",
"integrity": "sha512-0a7DJnhniBx2psRuKcVQ9g4hujN6PAR4fPS0NSF1T1luH1RBDZVVEn2pGND6Ly+AW1lUm/cHOHjsasqBelMhbw=="
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -1985,6 +2152,15 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@ -2096,6 +2272,24 @@
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
} }
}, },
"node_modules/vite-plugin-static-copy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-1.0.2.tgz",
"integrity": "sha512-AfmEF+a/mfjsUsrgjbCkhzUCeIUF4EKQXXt3Ie1cour9MBpy6f6GphbdW2td28oYfOrwCyRzFCksgLkpk58q6Q==",
"dev": true,
"dependencies": {
"chokidar": "^3.5.3",
"fast-glob": "^3.2.11",
"fs-extra": "^11.1.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",

View File

@ -18,6 +18,7 @@
"simplebar": "^5.3.4", "simplebar": "^5.3.4",
"smooth-scroll": "^16.1.3", "smooth-scroll": "^16.1.3",
"sweetalert2": "^11.0.18", "sweetalert2": "^11.0.18",
"tinymce": "^7.0.1",
"vanillajs-datepicker": "^1.2.0", "vanillajs-datepicker": "^1.2.0",
"waypoints": "^4.0.1" "waypoints": "^4.0.1"
}, },
@ -25,6 +26,7 @@
"axios": "^1.6.4", "axios": "^1.6.4",
"laravel-vite-plugin": "^1.0", "laravel-vite-plugin": "^1.0",
"sass-loader": "^13.3.2", "sass-loader": "^13.3.2",
"vite": "^5.0" "vite": "^5.0",
"vite-plugin-static-copy": "^1.0.2"
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
@csrf
<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" />
@canany(['create', 'update'], $content)
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
@endcanany

View File

@ -0,0 +1,17 @@
@section('meta_title', __('admin-sections.Project') . ': ' . $project->name)
@section('h1', __('admin-sections.Project') . ': ' . $project->name)
<x-admin.layout>
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<h3 id="category" class="mb-4">{{ __('admin-sections.About project') }} ({{ $language->title }})</h3>
<form method="post" action="{{ route('admin.projects.about.update', ['project' => $project->id, 'language' => $language->id]) }}">
@method('PUT')
@include('admin.projects.about._from')
</form>
</div>
</div>
</div>
</div>
</x-admin.layout>

View File

@ -0,0 +1,42 @@
@section('meta_title', __('admin-sections.Project') . ': ' . $project->name)
@section('h1', __('admin-sections.Project') . ': ' . $project->name)
@php
$contents = $project->contents;
@endphp
<x-admin.layout>
<div class="card border-0 shadow mb-4">
<div class="card-body">
<h3 id="category" class="mb-4">{{ __('admin-sections.About project') }}</h3>
<div class="table-responsive">
<table class="table table-centered table-nowrap mb-0 rounded">
<thead class="thead-light">
<tr>
<th class="border-0">{{ __('admin-sections.Languages') }}</th>
<th class="border-0">{{ __('admin-sections.Last update') }}</th>
</tr>
</thead>
<tbody>
@foreach($project->languages as $language)
@php
$content = $contents->firstWhere('language_id', $language->id);
@endphp
<tr>
<td>
<a href="{{ route('admin.projects.about.edit', ['project' => $project->id, 'language' => $language->id]) }}" class="fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{{ $language->title }}
</a>
</td>
<td>
{{ $content?->updated_at }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</x-admin.layout>

View File

@ -11,6 +11,7 @@
<th class="border-0">{{ __('validation.attributes.name') }}</th> <th class="border-0">{{ __('validation.attributes.name') }}</th>
<th class="border-0">{{ __('validation.attributes.code') }}</th> <th class="border-0">{{ __('validation.attributes.code') }}</th>
<th class="border-0">{{ __('validation.attributes.http_host') }}</th> <th class="border-0">{{ __('validation.attributes.http_host') }}</th>
<th class="border-0">{{ __('validation.attributes.is_public') }}</th>
<th class="border-0 rounded-end" style="width: 150px"></th> <th class="border-0 rounded-end" style="width: 150px"></th>
</tr> </tr>
</thead> </thead>
@ -18,9 +19,9 @@
@foreach($projects as $project) @foreach($projects as $project)
<tr> <tr>
<td> <td>
<a href="{{ route('admin.projects.edit', $project) }}" class="fw-bold"> <a href="{{ route('admin.projects.show', $project) }}" class="fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16"> <svg data-slot="icon" width="16" height="16" fill="currentColor" class="align-text-top" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z"></path>
</svg> </svg>
{{ $project->name }} {{ $project->name }}
</a> </a>
@ -32,12 +33,26 @@
{{ $project->http_host }} {{ $project->http_host }}
</td> </td>
<td> <td>
@if($project->is_public)
{{ __('Yes') }}
@else
{{ __('No') }}
@endif
</td>
<td>
<a href="{{ route('admin.projects.edit', $project) }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Edit') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
</a>
@can('delete', $project) @can('delete', $project)
<form method="post" action="{{ route('admin.projects.destroy', $project) }}"> <form method="post" class="d-inline-block" action="{{ route('admin.projects.destroy', $project) }}">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="btn btn-danger click-confirm"> <button type="submit" class="btn btn-danger click-confirm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Delete') }}">
{{ __('Delete') }} <svg data-slot="icon" fill="currentColor" width="20" height="20" class="align-text-center" 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="M6 18 18 6M6 6l12 12"></path>
</svg>
</button> </button>
</form> </form>
@endcan @endcan

View File

@ -0,0 +1,32 @@
@section('meta_title', __('admin-sections.Project') . ': ' . $project->name)
@section('h1', __('admin-sections.Project') . ': ' . $project->name)
<x-admin.layout>
@include('admin.projects._top')
<div class="card border-0 shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-centered table-nowrap mb-0 rounded">
<thead class="thead-light">
<tr>
<th class="border-0">{{ __('admin-sections.Sections') }}</th>
</tr>
</thead>
<tbody>
@can('viewAny', \App\Models\ProjectContent::class)
<tr>
<td>
<a href="{{ route('admin.projects.about.languages', ['project' => $project->id]) }}" class="fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{{ __('admin-sections.About project') }}
</a>
</td>
</tr>
@endcan
</tbody>
</table>
</div>
</div>
</div>
</x-admin.layout>

View File

@ -0,0 +1,24 @@
<div class="mb-3">
<label for="form-textarea-wysiwyg-{{ $requestName }}">{{ $title }}</label>
<textarea class="form-control textarea-tinymce @error($requestName) is-invalid @enderror" name="{{ $name }}" id="form-textarea-wysiwyg-{{ $requestName }}" rows="3">{{ $value }}</textarea>
@error($requestName)
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
@pushOnce('scripts')
<script src="{{ asset('/build/tinymce/tinymce.min.js') }}" referrerpolicy="origin"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
tinymce.init({
selector: '.textarea-tinymce',
@if(in_array(app()->getLocale(), ['ru'], true))
language: '{{ app()->getLocale() }}',
@endif
license_key: '{{ $tinymceLicenseKey }}',
plugins: 'advlist code emoticons link lists table codesample',
toolbar: 'bold italic | bullist numlist | link emoticons codesample',
referrer_policy: 'origin',
});
});
</script>
@endpushonce

View File

@ -0,0 +1,7 @@
<div class="mb-3">
<label for="form-textarea-{{ $requestName }}">{{ $title }}</label>
<textarea class="form-control @error($requestName) is-invalid @enderror" name="{{ $name }}" id="form-textarea-{{ $requestName }}" rows="3">{{ $value }}</textarea>
@error($requestName)
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>

View File

@ -18,7 +18,14 @@
Route::resource('roles', \App\Http\Controllers\Admin\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']); Route::resource('roles', \App\Http\Controllers\Admin\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']);
Route::resource('projects', \App\Http\Controllers\Admin\ProjectsController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['project' => '[0-9]+']); Route::resource('projects', \App\Http\Controllers\Admin\ProjectsController::class)->where(['project' => '[0-9]+']);
Route::prefix('projects/{project}')->as('projects.')->group(function () {
Route::get('about', [\App\Http\Controllers\Admin\Projects\AboutController::class, 'languages'])->name('about.languages');
Route::get('about/{language}', [\App\Http\Controllers\Admin\Projects\AboutController::class, 'edit'])->name('about.edit')->where(['language' => '[0-9]+']);
Route::put('about/{language}', [\App\Http\Controllers\Admin\Projects\AboutController::class, 'update'])->name('about.update')->where(['language' => '[0-9]+']);
})->where(['project' => '[0-9]+']);
Route::post('languages/new-language', [\App\Http\Controllers\Admin\LanguagesController::class, 'newLanguage'])->name('new-language'); Route::post('languages/new-language', [\App\Http\Controllers\Admin\LanguagesController::class, 'newLanguage'])->name('new-language');
}); });

View File

@ -1,14 +1,23 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin'; import laravel from 'laravel-vite-plugin';
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
laravel({ laravel({
input: [ input: [
'resources/volt/scss/app.scss', 'resources/volt/scss/app.scss',
'resources/volt/js/app.js' 'resources/volt/js/app.js',
], ],
refresh: true, refresh: true,
}), }),
viteStaticCopy({
targets: [
{
src: ['node_modules/tinymce/*', 'resources/tinymce/*'],
dest: 'tinymce'
}
]
})
], ],
}); });