Версия 0.1.0 #1
@ -2,9 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Dto\Builder;
|
namespace App\Dto\Builder;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
final readonly class Project
|
final readonly class Project
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private ?bool $isPublic = null,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
public function isPublic(): ?bool
|
||||||
|
{
|
||||||
|
return $this->isPublic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
16
app/application/app/Enums/CacheTag.php
Normal file
16
app/application/app/Enums/CacheTag.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Illuminate\Cache\TaggedCache;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
enum CacheTag: string
|
||||||
|
{
|
||||||
|
case Project = 'project';
|
||||||
|
|
||||||
|
public function getCache(): TaggedCache
|
||||||
|
{
|
||||||
|
return Cache::tags($this->value);
|
||||||
|
}
|
||||||
|
}
|
31
app/application/app/Enums/Site/ProjectSection.php
Normal file
31
app/application/app/Enums/Site/ProjectSection.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Enums\Site;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectLanguage;
|
||||||
|
|
||||||
|
enum ProjectSection
|
||||||
|
{
|
||||||
|
case Home;
|
||||||
|
|
||||||
|
public function url(Project $project, ?ProjectLanguage $language = null): string
|
||||||
|
{
|
||||||
|
$parameters = [];
|
||||||
|
$prefixProject = '';
|
||||||
|
if ($project->http_host === null) {
|
||||||
|
$prefixProject = 'project.';
|
||||||
|
$parameters['project'] = $project->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefixLanguage = '';
|
||||||
|
if ($language?->is_default === false) {
|
||||||
|
$parameters['language'] = $language->code;
|
||||||
|
$prefixLanguage = '-language';
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($this) {
|
||||||
|
self::Home => \route($prefixProject . 'home' . $prefixLanguage, $parameters),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
18
app/application/app/Http/Controllers/Site/Controller.php
Normal file
18
app/application/app/Http/Controllers/Site/Controller.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Site;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller as BaseController;
|
||||||
|
use app\ServiceResults\Site\PagePossibleWithoutTranslation;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
abstract class Controller extends BaseController
|
||||||
|
{
|
||||||
|
protected function viewPageWithoutTranslation(PagePossibleWithoutTranslation $result): View
|
||||||
|
{
|
||||||
|
return \view('site.page-without-translation', [
|
||||||
|
'project' => $result->getProject(),
|
||||||
|
'language' => $result->getLanguage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Site;
|
||||||
|
|
||||||
|
use App\Enums\Site\ProjectSection;
|
||||||
|
use App\Services\Site\ProjectService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class ProjectsController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectService $projectService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function index(Request $request, ?string $language = null): View
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (\is_null($request->project)) {
|
||||||
|
if (!\is_null($language)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$with = ['storage'];
|
||||||
|
$result = $this->projectService->getProjects($user, $with);
|
||||||
|
if ($result->isError()) {
|
||||||
|
$this->errors($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \view('site.projects.index', $result->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->projectService->getAboutByProject($request->project, $language, $request->user());
|
||||||
|
if ($result->isError()) {
|
||||||
|
$this->errors($result);
|
||||||
|
}
|
||||||
|
if ($result->isTranslation()) {
|
||||||
|
return $this->viewPageWithoutTranslation($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \view('site.projects.about', $result->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function about(string $project, Request $request, ?string $language = null)
|
||||||
|
{
|
||||||
|
$result = $this->projectService->getAbout($project, $language, $request->user());
|
||||||
|
if ($result->isError()) {
|
||||||
|
$this->errors($result);
|
||||||
|
}
|
||||||
|
if ($result->isTranslation()) {
|
||||||
|
return $this->viewPageWithoutTranslation($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result->getProject()->http_href !== null) {
|
||||||
|
$link = ProjectSection::Home->url($result->getProject(), $result->getLanguage());
|
||||||
|
return \redirect($result->getProject()->http_href . $link, 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \view('site.projects.about', $result->getData());
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class AdminPanel
|
final class AdminPanel
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
|
50
app/application/app/Http/Middleware/Project.php
Normal file
50
app/application/app/Http/Middleware/Project.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\CacheTag;
|
||||||
|
use App\Repositories\ProjectRepository;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class Project
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectRepository $projectRepository,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$projectCode = $request->route()?->parameter('project');
|
||||||
|
if ($projectCode === null) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$seconds = 3600;
|
||||||
|
$project = CacheTag::Project->getCache()->remember(self::class . $projectCode, $seconds, function () use ($projectCode) {
|
||||||
|
return $this->projectRepository->getProjectByCode($projectCode) ?? false;
|
||||||
|
});
|
||||||
|
if ($project === false) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($request->route()->parameters['project']);
|
||||||
|
|
||||||
|
$routeName = $request->route()->getName();
|
||||||
|
if (
|
||||||
|
$routeName !== null
|
||||||
|
&& $project->http_host !== null
|
||||||
|
&& $project->http_host !== $request->getSchemeAndHttpHost()
|
||||||
|
) {
|
||||||
|
$routeName = Str::of($routeName)->replaceStart('project.', '')->toString();
|
||||||
|
$route = \route($routeName, $request->route()->parameters(), false);
|
||||||
|
return redirect($project->http_host . $route, 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->merge(['project' => $project]);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
32
app/application/app/Http/Middleware/ProjectDomain.php
Normal file
32
app/application/app/Http/Middleware/ProjectDomain.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\CacheTag;
|
||||||
|
use App\Repositories\ProjectRepository;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
final class ProjectDomain
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectRepository $projectRepository,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$httpHost = $request->getSchemeAndHttpHost();
|
||||||
|
|
||||||
|
$seconds = 3600;
|
||||||
|
$project = CacheTag::Project->getCache()->remember(self::class . $httpHost, $seconds, function () use ($httpHost) {
|
||||||
|
return $this->projectRepository->getProjectByHttpHost($httpHost) ?? false;
|
||||||
|
});
|
||||||
|
if ($project === false) {
|
||||||
|
$project = null;
|
||||||
|
}
|
||||||
|
$request->merge(['project' => $project]);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -18,10 +18,12 @@ class StoreUpdateRequest extends FormRequest implements FormRequestDto
|
|||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$projectId = $this->project ?? null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'code' => ['required', 'string', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/i'],
|
'code' => ['required', 'string', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/'],
|
||||||
'http_host' => ['nullable', 'string', 'max:255', new HttpHost()],
|
'http_host' => ['nullable', 'string', 'max:255', new HttpHost(), 'unique:projects,http_host,' . $projectId],
|
||||||
'is_public' => ['required', 'boolean'],
|
'is_public' => ['required', 'boolean'],
|
||||||
|
|
||||||
'logo.file' => ['nullable', 'numeric', 'min:1'],
|
'logo.file' => ['nullable', 'numeric', 'min:1'],
|
||||||
@ -29,7 +31,7 @@ public function rules(): array
|
|||||||
|
|
||||||
'languages.items.*.id' => ['nullable', 'numeric'],
|
'languages.items.*.id' => ['nullable', 'numeric'],
|
||||||
'languages.items.*.title' => ['required', 'string', 'max:255'],
|
'languages.items.*.title' => ['required', 'string', 'max:255'],
|
||||||
'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-zA-Z_]+$/i'],
|
'languages.items.*.code' => ['required', 'string', 'min:2', 'max:30', 'regex:/^[a-z_]+$/'],
|
||||||
'languages.items.*.sort' => ['required', 'numeric', 'min:-1000', 'max:1000'],
|
'languages.items.*.sort' => ['required', 'numeric', 'min:-1000', 'max:1000'],
|
||||||
'languages.default' => ['required', 'numeric', function (string $attribute, mixed $value, \Closure $fail) {
|
'languages.default' => ['required', 'numeric', function (string $attribute, mixed $value, \Closure $fail) {
|
||||||
$languages = $this->input('languages.items', []);
|
$languages = $this->input('languages.items', []);
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Contracts\Foundation\Application;
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
@ -56,6 +57,9 @@ public function boot(): void
|
|||||||
|
|
||||||
Relation::enforceMorphMap(Morph::map());
|
Relation::enforceMorphMap(Morph::map());
|
||||||
|
|
||||||
|
Route::pattern('language', '[a-z_]+');
|
||||||
|
Route::pattern('project', '[a-z0-9_-]+');
|
||||||
|
|
||||||
$this->configureRateLimiting();
|
$this->configureRateLimiting();
|
||||||
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);
|
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
use App\Models\ProjectLanguage;
|
use App\Models\ProjectLanguage;
|
||||||
|
|
||||||
final class ProjectLanguageRepository
|
final class ProjectLanguageRepository
|
||||||
{
|
{
|
||||||
|
public function getProjectLanguageByCodeOrDefault(Project $project, ?string $code): ?ProjectLanguage
|
||||||
|
{
|
||||||
|
if (\is_null($code)) {
|
||||||
|
return $project->languages()->where('is_default', true)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $project->languages()->where('code', $code)->first();
|
||||||
|
}
|
||||||
|
|
||||||
public function isExistsLanguageById(int $projectId, int $languageId): bool
|
public function isExistsLanguageById(int $projectId, int $languageId): bool
|
||||||
{
|
{
|
||||||
return ProjectLanguage::query()
|
return ProjectLanguage::query()
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
use App\Contracts\Search;
|
use App\Contracts\Search;
|
||||||
|
use App\Models\Project;
|
||||||
use App\Services\ProjectLink\BuilderCommand;
|
use App\Services\ProjectLink\BuilderCommand;
|
||||||
use App\Services\Search\CreateSearchInstanceCommand;
|
use App\Services\Search\CreateSearchInstanceCommand;
|
||||||
use App\Dto\Builder\ProjectLink as ProjectLinkBuilderDto;
|
use App\Dto\Builder\ProjectLink as ProjectLinkBuilderDto;
|
||||||
use App\Models\ProjectLink;
|
use App\Models\ProjectLink;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
final readonly class ProjectLinkRepository
|
final readonly class ProjectLinkRepository
|
||||||
{
|
{
|
||||||
@ -29,4 +32,11 @@ public function getLinkById(int $id): ?ProjectLink
|
|||||||
{
|
{
|
||||||
return ProjectLink::query()->where('id', $id)->first();
|
return ProjectLink::query()->where('id', $id)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLinksByProject(Project $project, int $languageId): Collection
|
||||||
|
{
|
||||||
|
return $project->links()->where(function (Builder $query) use ($languageId) {
|
||||||
|
$query->whereNull('language_id')->orWhere('language_id', $languageId);
|
||||||
|
})->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,11 @@ public function getProjects(ProjectBuilderDto $projectBuilderDto, array $with =
|
|||||||
return $this->createSearchInstanceCommand->execute($query);
|
return $this->createSearchInstanceCommand->execute($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getProjectByHttpHost(string $httpHost): ?Project
|
||||||
|
{
|
||||||
|
return Project::query()->where('http_host', $httpHost)->first();
|
||||||
|
}
|
||||||
|
|
||||||
public function getProjectById(int $id): ?Project
|
public function getProjectById(int $id): ?Project
|
||||||
{
|
{
|
||||||
return Project::query()->where('id', $id)->first();
|
return Project::query()->where('id', $id)->first();
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\ServiceResults\Site;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectLanguage;
|
||||||
|
use App\ServiceResults\ServiceResult;
|
||||||
|
|
||||||
|
final class PagePossibleWithoutTranslation extends ServiceResult
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Project $project,
|
||||||
|
private readonly ProjectLanguage $language,
|
||||||
|
private readonly array $data,
|
||||||
|
private readonly bool $isTranslation,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function isTranslation(): bool
|
||||||
|
{
|
||||||
|
return $this->isTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProject(): Project
|
||||||
|
{
|
||||||
|
return $this->project;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLanguage(): ProjectLanguage
|
||||||
|
{
|
||||||
|
return $this->language;
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
use App\Dto\Builder\Project as ProjectBuilderDto;
|
use App\Dto\Builder\Project as ProjectBuilderDto;
|
||||||
use App\Dto\QuerySettingsDto;
|
use App\Dto\QuerySettingsDto;
|
||||||
use App\Dto\Service\Admin\Project\StoreUpdate;
|
use App\Dto\Service\Admin\Project\StoreUpdate;
|
||||||
|
use App\Enums\CacheTag;
|
||||||
use App\Enums\Morph;
|
use App\Enums\Morph;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectLanguage;
|
use App\Models\ProjectLanguage;
|
||||||
@ -14,6 +15,7 @@
|
|||||||
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\Project\ProjectCommandHandler;
|
use App\Services\Project\ProjectCommandHandler;
|
||||||
use App\Services\ProjectLanguage\ModelSyncCommand;
|
use App\Services\ProjectLanguage\ModelSyncCommand;
|
||||||
use App\Services\Role\CreateAdminRoleForProjectCommand;
|
use App\Services\Role\CreateAdminRoleForProjectCommand;
|
||||||
@ -29,6 +31,7 @@ public function __construct(
|
|||||||
private readonly CreateAdminRoleForProjectCommand $createAdminRoleForProjectCommand,
|
private readonly CreateAdminRoleForProjectCommand $createAdminRoleForProjectCommand,
|
||||||
private readonly ModelSyncCommand $languageModelSyncCommand,
|
private readonly ModelSyncCommand $languageModelSyncCommand,
|
||||||
private readonly StorageService $storageService,
|
private readonly StorageService $storageService,
|
||||||
|
private readonly ClearCacheCommandHandler $clearCacheCommandHandler,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public function index(ProjectBuilderDto $projectBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
|
public function index(ProjectBuilderDto $projectBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
|
||||||
@ -130,6 +133,7 @@ public function store(StoreUpdate $data, User $user): ServiceResultError | Store
|
|||||||
|
|
||||||
return $project;
|
return $project;
|
||||||
});
|
});
|
||||||
|
$this->clearCacheCommandHandler->byTag(CacheTag::Project);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
report($e);
|
report($e);
|
||||||
return $this->errService(__('Server Error'));
|
return $this->errService(__('Server Error'));
|
||||||
@ -172,6 +176,7 @@ public function update(int $id, StoreUpdate $data, User $user): ServiceResultErr
|
|||||||
|
|
||||||
return $project;
|
return $project;
|
||||||
});
|
});
|
||||||
|
$this->clearCacheCommandHandler->byTag(CacheTag::Project);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
report($e);
|
report($e);
|
||||||
return $this->errService(__('Server Error'));
|
return $this->errService(__('Server Error'));
|
||||||
@ -196,6 +201,7 @@ public function destroy(int $id, User $user): ServiceResultError|ServiceResultSu
|
|||||||
DB::transaction(function () use ($project) {
|
DB::transaction(function () use ($project) {
|
||||||
$this->projectCommandHandler->handleDestroy($project);
|
$this->projectCommandHandler->handleDestroy($project);
|
||||||
});
|
});
|
||||||
|
$this->clearCacheCommandHandler->byTag(CacheTag::Project);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
report($e);
|
report($e);
|
||||||
return $this->errService(__('Server Error'));
|
return $this->errService(__('Server Error'));
|
||||||
|
13
app/application/app/Services/ClearCacheCommandHandler.php
Normal file
13
app/application/app/Services/ClearCacheCommandHandler.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\CacheTag;
|
||||||
|
|
||||||
|
final readonly class ClearCacheCommandHandler
|
||||||
|
{
|
||||||
|
public function byTag(CacheTag $tag)
|
||||||
|
{
|
||||||
|
$tag->getCache()->flush();
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,10 @@
|
|||||||
{
|
{
|
||||||
public function execute(Relation | Builder $query, ProjectBuilderDto $projectBuilderDto): Relation | Builder
|
public function execute(Relation | Builder $query, ProjectBuilderDto $projectBuilderDto): Relation | Builder
|
||||||
{
|
{
|
||||||
|
if ($projectBuilderDto->isPublic() !== null) {
|
||||||
|
$query->where('is_public', $projectBuilderDto->isPublic());
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectLanguage;
|
||||||
use App\ServiceResults\ServiceResultArray;
|
use App\ServiceResults\ServiceResultArray;
|
||||||
use App\ServiceResults\ServiceResultError;
|
use App\ServiceResults\ServiceResultError;
|
||||||
use App\ServiceResults\ServiceResultSuccess;
|
use App\ServiceResults\ServiceResultSuccess;
|
||||||
|
use App\ServiceResults\Site\PagePossibleWithoutTranslation;
|
||||||
use App\ServiceResults\StoreUpdateResult;
|
use App\ServiceResults\StoreUpdateResult;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
abstract class Service
|
abstract class Service
|
||||||
{
|
{
|
||||||
@ -56,6 +60,11 @@ final protected function result(array $data = []): ServiceResultArray
|
|||||||
return new ServiceResultArray(data: $data);
|
return new ServiceResultArray(data: $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final protected function resultSitePage(Project $project, ProjectLanguage $language, array $data, bool $isTranslation): PagePossibleWithoutTranslation
|
||||||
|
{
|
||||||
|
return new PagePossibleWithoutTranslation($project, $language, $data, $isTranslation);
|
||||||
|
}
|
||||||
|
|
||||||
final protected function error(int $code, string $message, array $errors = []): ServiceResultError
|
final protected function error(int $code, string $message, array $errors = []): ServiceResultError
|
||||||
{
|
{
|
||||||
return new ServiceResultError(
|
return new ServiceResultError(
|
||||||
|
73
app/application/app/Services/Site/ProjectService.php
Normal file
73
app/application/app/Services/Site/ProjectService.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services\Site;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Dto\Builder\Project as ProjectBuilderDto;
|
||||||
|
use App\Repositories\ProjectContentRepository;
|
||||||
|
use App\Repositories\ProjectLanguageRepository;
|
||||||
|
use App\Repositories\ProjectLinkRepository;
|
||||||
|
use App\Repositories\ProjectRepository;
|
||||||
|
use App\ServiceResults\ServiceResultArray;
|
||||||
|
use App\ServiceResults\ServiceResultError;
|
||||||
|
use App\ServiceResults\Site\PagePossibleWithoutTranslation;
|
||||||
|
use App\Services\Service;
|
||||||
|
|
||||||
|
final class ProjectService extends Service
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectRepository $projectRepository,
|
||||||
|
private readonly ProjectLanguageRepository $projectLanguageRepository,
|
||||||
|
private readonly ProjectContentRepository $projectContentRepository,
|
||||||
|
private readonly ProjectLinkRepository $projectLinkRepository,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function getProjects(?User $user, array $with): ServiceResultError | ServiceResultArray
|
||||||
|
{
|
||||||
|
$isPublic = null;
|
||||||
|
if (\is_null($user) || $user->cannot('viewAny', Project::class)) {
|
||||||
|
$isPublic = true;
|
||||||
|
}
|
||||||
|
$projectBuilderDto = new ProjectBuilderDto(
|
||||||
|
isPublic: $isPublic,
|
||||||
|
);
|
||||||
|
$projects = $this->projectRepository->getProjects($projectBuilderDto, $with);
|
||||||
|
|
||||||
|
return $this->result([
|
||||||
|
'projects' => $projects->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAbout(string $projectCode, ?string $languageCode, ?User $user = null): ServiceResultError | PagePossibleWithoutTranslation
|
||||||
|
{
|
||||||
|
$project = $this->projectRepository->getProjectByCode($projectCode);
|
||||||
|
if (\is_null($project)) {
|
||||||
|
return $this->errNotFound('Project not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getAboutByProject($project, $languageCode, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAboutByProject(Project $project, ?string $languageCode, ?User $user = null): ServiceResultError | PagePossibleWithoutTranslation
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
$project->is_public === false
|
||||||
|
&& ( $user === null || $user->cannot('view', $project) )
|
||||||
|
) {
|
||||||
|
return $this->errFobidden(__('Access is denied'));
|
||||||
|
}
|
||||||
|
$language = $this->projectLanguageRepository->getProjectLanguageByCodeOrDefault($project, $languageCode);
|
||||||
|
if (!$language) {
|
||||||
|
return $this->errNotFound(__('Language not found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'project' => $project,
|
||||||
|
'language' => $language,
|
||||||
|
'content' => $this->projectContentRepository->getContentByLanguageId($project->id, $language->id),
|
||||||
|
'links' => $this->projectLinkRepository->getLinksByProject($project, $language->id),
|
||||||
|
];
|
||||||
|
return $this->resultSitePage($project, $language, $data, \is_null($data['content']));
|
||||||
|
}
|
||||||
|
}
|
14
app/application/app/View/Components/HomeLayout.php
Normal file
14
app/application/app/View/Components/HomeLayout.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class HomeLayout extends Component
|
||||||
|
{
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layout.home');
|
||||||
|
}
|
||||||
|
}
|
31
app/application/app/View/Components/Site/ChooseLanguage.php
Normal file
31
app/application/app/View/Components/Site/ChooseLanguage.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\View\Components\Site;
|
||||||
|
|
||||||
|
use App\Models\ProjectLanguage;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class ChooseLanguage extends Component
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProjectLanguage $language,
|
||||||
|
private readonly Collection $languages,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
$link = Str::of( request()->getRequestUri() )->rtrim('/');
|
||||||
|
if ($link->endsWith('/language/' . $this->language->code)) {
|
||||||
|
$link = $link->replace('/language/' . $this->language->code, '', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('components.site.choose-language', [
|
||||||
|
'language' => $this->language,
|
||||||
|
'languages' => $this->languages,
|
||||||
|
'link' => (string) $link,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
26
app/application/app/View/Components/Site/Layout.php
Normal file
26
app/application/app/View/Components/Site/Layout.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\View\Components\Site;
|
||||||
|
|
||||||
|
use App\Enums\StorageType;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\ProjectLanguage;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class Layout extends Component
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Project $project,
|
||||||
|
private readonly ProjectLanguage $language,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layout.site', [
|
||||||
|
'project' => $this->project,
|
||||||
|
'logo' => $this->project->getStorageOne(StorageType::Logo),
|
||||||
|
'language' => $this->language,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -3,12 +3,23 @@
|
|||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
|
then: function () {
|
||||||
|
Route::middleware([\App\Http\Middleware\ProjectDomain::class])->group(base_path('routes/web-project.php'));
|
||||||
|
Route::middleware([
|
||||||
|
\App\Http\Middleware\ProjectDomain::class,
|
||||||
|
\App\Http\Middleware\Project::class,
|
||||||
|
])
|
||||||
|
->prefix('project/{project}')
|
||||||
|
->as('project.')
|
||||||
|
->group(base_path('routes/web-project.php'));
|
||||||
|
},
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
//
|
//
|
||||||
|
@ -254,4 +254,6 @@
|
|||||||
"The link was successfully created": "The link was successfully created",
|
"The link was successfully created": "The link was successfully created",
|
||||||
"The link was successfully updated": "The link was successfully updated",
|
"The link was successfully updated": "The link was successfully updated",
|
||||||
"The link has been deleted": "The link has been deleted",
|
"The link has been deleted": "The link has been deleted",
|
||||||
|
"Language not found": "Language not found",
|
||||||
|
"Project not found": "Project not found"
|
||||||
}
|
}
|
||||||
|
10
app/application/lang/en/site.php
Normal file
10
app/application/lang/en/site.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
"Menu" => "Menu",
|
||||||
|
'Projects' => 'Projects',
|
||||||
|
'Project' => 'Project',
|
||||||
|
'About project' => 'About the project',
|
||||||
|
'Page without translation' => 'Page without translation',
|
||||||
|
'Powered by service' => 'Powered by the My Projects website engine',
|
||||||
|
'Choose language' => 'Choose language',
|
||||||
|
];
|
@ -253,5 +253,7 @@
|
|||||||
"in all languages": "на всех языках",
|
"in all languages": "на всех языках",
|
||||||
"The link was successfully created": "Ссылка успешно создана",
|
"The link was successfully created": "Ссылка успешно создана",
|
||||||
"The link was successfully updated": "Ссылка успешно обновлена",
|
"The link was successfully updated": "Ссылка успешно обновлена",
|
||||||
"The link has been deleted": "Ссылка удалена"
|
"The link has been deleted": "Ссылка удалена",
|
||||||
|
"Language not found": "Язык не найден",
|
||||||
|
"Project not found": "Проект не найден"
|
||||||
}
|
}
|
||||||
|
10
app/application/lang/ru/site.php
Normal file
10
app/application/lang/ru/site.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
"Menu" => "Меню",
|
||||||
|
'Projects' => 'Проекты',
|
||||||
|
'Project' => 'Проект',
|
||||||
|
'About project' => 'О проекте',
|
||||||
|
'Page without translation' => 'Страница без перевода',
|
||||||
|
'Powered by service' => 'Работает на движке сайта «Мои проекты»',
|
||||||
|
'Choose language' => 'Выберите язык',
|
||||||
|
];
|
72
app/application/resources/home/scss/app.scss
Normal file
72
app/application/resources/home/scss/app.scss
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
||||||
|
@import "reset";
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0 20px;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-list {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover .projects-list__title {
|
||||||
|
box-shadow: 0px -1px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.projects-list__img {
|
||||||
|
display: block;
|
||||||
|
background: #ddd;
|
||||||
|
height: 150px;
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.projects-list__title {
|
||||||
|
background: #ccc;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0px -1px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: box-shadow 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.projects-list {
|
||||||
|
display: grid;
|
||||||
|
margin-left: -20px;
|
||||||
|
li {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) and (max-width: 899px) {
|
||||||
|
.projects-list {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 900px) and (max-width: 1100px) {
|
||||||
|
.projects-list {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1001px) {
|
||||||
|
body {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.projects-list {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
73
app/application/resources/home/scss/reset.scss
Normal file
73
app/application/resources/home/scss/reset.scss
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/* Box sizing rules */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent font size inflation */
|
||||||
|
html {
|
||||||
|
-moz-text-size-adjust: none;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default margin in favour of better control in authored CSS */
|
||||||
|
body, h1, h2, h3, h4, p,
|
||||||
|
figure, blockquote, dl, dd {
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||||
|
ul[role='list'],
|
||||||
|
ol[role='list'] {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set core body defaults */
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set shorter line heights on headings and interactive elements */
|
||||||
|
h1, h2, h3, h4,
|
||||||
|
button, input, label {
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Balance text wrapping on headings */
|
||||||
|
h1, h2,
|
||||||
|
h3, h4 {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A elements that don't have a class get default styles */
|
||||||
|
a:not([class]) {
|
||||||
|
text-decoration-skip-ink: auto;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make images easier to work with */
|
||||||
|
img,
|
||||||
|
picture {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inherit fonts for inputs and buttons */
|
||||||
|
input, button,
|
||||||
|
textarea, select {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure textareas without a rows attribute are not tiny */
|
||||||
|
textarea:not([rows]) {
|
||||||
|
min-height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anything that has been anchored to should have extra scroll margin */
|
||||||
|
:target {
|
||||||
|
scroll-margin-block: 5ex;
|
||||||
|
}
|
8
app/application/resources/site/js/_choose-language.js
Normal file
8
app/application/resources/site/js/_choose-language.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
let blockLanguage = document.querySelector('#language');
|
||||||
|
blockLanguage.querySelector('.language__button').addEventListener('click', (e) => {
|
||||||
|
if (blockLanguage.classList.contains('active')) {
|
||||||
|
blockLanguage.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
blockLanguage.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
8
app/application/resources/site/js/_menu.js
Normal file
8
app/application/resources/site/js/_menu.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
let body = document.querySelector('body');
|
||||||
|
document.querySelector('#mobile-menu').addEventListener('click', (e) => {
|
||||||
|
if (body.classList.contains('mobile-menu-open')) {
|
||||||
|
body.classList.remove('mobile-menu-open');
|
||||||
|
} else {
|
||||||
|
body.classList.add('mobile-menu-open');
|
||||||
|
}
|
||||||
|
});
|
2
app/application/resources/site/js/app.js
Normal file
2
app/application/resources/site/js/app.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import './_menu.js';
|
||||||
|
import './_choose-language.js';
|
252
app/application/resources/site/scss/app.scss
Normal file
252
app/application/resources/site/scss/app.scss
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap');
|
||||||
|
@import "reset";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"logo menu"
|
||||||
|
"language language";
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: 0 3px 9px rgba(0,0,0,0.48);
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
.header_logo {
|
||||||
|
grid-area: logo;
|
||||||
|
max-height: 50px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #0a0e17;
|
||||||
|
font-size: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid #0a0e17;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#mobile-menu {
|
||||||
|
grid-area: menu;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: #FF2D20;
|
||||||
|
margin-left: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: none;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#menu {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 10px 0 0 20px;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
color: #0a0e17;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menu__title {
|
||||||
|
background: #ccc;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 12px 0 12px 20px;
|
||||||
|
box-shadow: 0 3px 9px rgba(0,0,0,0.48);
|
||||||
|
}
|
||||||
|
body.mobile-menu-open {
|
||||||
|
#mobile-menu {
|
||||||
|
.open {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#menu {
|
||||||
|
background: #ddd;
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#language {
|
||||||
|
grid-area: language;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.language__block {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.language__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;
|
||||||
|
}
|
||||||
|
.language__button__str {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 8px);
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
.language__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;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
font-size: 16px;
|
||||||
|
display: block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#language.active {
|
||||||
|
.language__list {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.language__button {
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 15px 0 15px 15px;
|
||||||
|
box-shadow: 5px 0px 5px rgba(0,0,0,0.48);
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0a0e17;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
body {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
grid-template-areas: "logo language";
|
||||||
|
grid-template-columns: 1fr 200px;
|
||||||
|
}
|
||||||
|
#mobile-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.main-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.language__button {
|
||||||
|
padding: 7px 10px 7px 0;
|
||||||
|
}
|
||||||
|
#menu {
|
||||||
|
background: #eee;
|
||||||
|
display: block;
|
||||||
|
width: 175px;
|
||||||
|
box-shadow: 0px 7px 8px rgba(0,0,0,0.48);
|
||||||
|
}
|
||||||
|
.menu__title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
73
app/application/resources/site/scss/reset.scss
Normal file
73
app/application/resources/site/scss/reset.scss
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/* Box sizing rules */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent font size inflation */
|
||||||
|
html {
|
||||||
|
-moz-text-size-adjust: none;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default margin in favour of better control in authored CSS */
|
||||||
|
body, h1, h2, h3, h4, p,
|
||||||
|
figure, blockquote, dl, dd {
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||||
|
ul[role='list'],
|
||||||
|
ol[role='list'] {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set core body defaults */
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set shorter line heights on headings and interactive elements */
|
||||||
|
h1, h2, h3, h4,
|
||||||
|
button, input, label {
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Balance text wrapping on headings */
|
||||||
|
h1, h2,
|
||||||
|
h3, h4 {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A elements that don't have a class get default styles */
|
||||||
|
a:not([class]) {
|
||||||
|
text-decoration-skip-ink: auto;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make images easier to work with */
|
||||||
|
img,
|
||||||
|
picture {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inherit fonts for inputs and buttons */
|
||||||
|
input, button,
|
||||||
|
textarea, select {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure textareas without a rows attribute are not tiny */
|
||||||
|
textarea:not([rows]) {
|
||||||
|
min-height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anything that has been anchored to should have extra scroll margin */
|
||||||
|
:target {
|
||||||
|
scroll-margin-block: 5ex;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
<div id="language">
|
||||||
|
<div class="language__block">
|
||||||
|
<button class="language__button" type="button" aria-label="{{ __('site.Choose language') }}">
|
||||||
|
<svg width="16" height="16" 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="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"></path>
|
||||||
|
</svg>
|
||||||
|
{{ $language->title }}
|
||||||
|
<svg width="16" height="16" class="language__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="language__list">
|
||||||
|
@foreach($languages as $lang)
|
||||||
|
@php
|
||||||
|
$code = '';
|
||||||
|
if ($lang->is_default !== true) {
|
||||||
|
$code = '/language/' . $lang->code;
|
||||||
|
} else if($link === '') {
|
||||||
|
$code = '/';
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<li><a href="{{ $link . $code }}">{{ $lang->title }}</a></li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
16
app/application/resources/views/layout/home.blade.php
Normal file
16
app/application/resources/views/layout/home.blade.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<title>@yield('meta_title', '')</title>
|
||||||
|
<meta name="keywords" content="@yield('meta_keywords', '')" />
|
||||||
|
<meta name="description" content="@yield('meta_description', '')" />
|
||||||
|
|
||||||
|
@vite('resources/home/scss/app.scss')
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ $slot }}
|
||||||
|
</body>
|
||||||
|
</html>
|
55
app/application/resources/views/layout/site.blade.php
Normal file
55
app/application/resources/views/layout/site.blade.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ $language->code }}">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<title>@yield('meta_title', '')</title>
|
||||||
|
<meta name="keywords" content="@yield('meta_keywords', '')" />
|
||||||
|
<meta name="description" content="@yield('meta_description', '')" />
|
||||||
|
|
||||||
|
@vite('resources/site/scss/app.scss')
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<header class="header">
|
||||||
|
<div class="header_logo">
|
||||||
|
<a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $language) }}">
|
||||||
|
@if($logo)
|
||||||
|
<img src="{{ $logo->url }}" alt="{{ $project->name }}">
|
||||||
|
@else
|
||||||
|
<strong>{{ $project->name }}</strong>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button id="mobile-menu" type="button" aria-label="{{ __('site.Menu') }}">
|
||||||
|
<svg class="open" width="45" height="45" 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="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
|
||||||
|
</svg>
|
||||||
|
<svg class="close" width="45" height="45" 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="M6 18 18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<x-site.choose-language :language="$language" :languages="$project->languages" />
|
||||||
|
</header>
|
||||||
|
<div class="main-container">
|
||||||
|
<nav id="menu">
|
||||||
|
<div class="menu__title">{{ __('site.Menu') }}</div>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ \App\Enums\Site\ProjectSection::Home->url($project, $language) }}" @class(['active' => request()->route()->named(['home', 'language.home'])])>{{ __('site.About project') }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="section-container">
|
||||||
|
<div class="content">
|
||||||
|
<h1>@yield('h1', '')</h1>
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
<a href="https://git.kor-elf.net/kor-elf/my-projects-website" target="_blank">{{ __('site.Powered by service') }}</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@vite('resources/site/js/app.js')
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,5 @@
|
|||||||
|
@section('h1', __('site.Page without translation'))
|
||||||
|
|
||||||
|
<x-site.layout :project="$project" :language="$language">
|
||||||
|
|
||||||
|
</x-site.layout>
|
@ -0,0 +1,6 @@
|
|||||||
|
@section('meta_title', __('site.Project') . ': ' . $content->title)
|
||||||
|
@section('h1', $content->title)
|
||||||
|
|
||||||
|
<x-site.layout :project="$project" :language="$language">
|
||||||
|
{!! $content->description !!}
|
||||||
|
</x-site.layout>
|
@ -0,0 +1,18 @@
|
|||||||
|
@section('meta_title', __('site.Projects'))
|
||||||
|
|
||||||
|
<x-home-layout>
|
||||||
|
<h1>{{ __('site.Projects') }}</h1>
|
||||||
|
<ul class="projects-list">
|
||||||
|
@foreach($projects as $project)
|
||||||
|
@php
|
||||||
|
$logo = $project->getStorageOne(\App\Enums\StorageType::Logo);
|
||||||
|
@endphp
|
||||||
|
<li>
|
||||||
|
<a href="{{ \App\Enums\Site\ProjectSection::Home->url($project) }}">
|
||||||
|
<span class="projects-list__img">@if($logo)<img src="{{ $logo->url }}" alt="{{ $project->name }}">@endif</span>
|
||||||
|
<span class="projects-list__title">{{ $project->name }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</x-home-layout>
|
6
app/application/routes/web-project.php
Normal file
6
app/application/routes/web-project.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('/', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home');
|
||||||
|
Route::get('/language/{language}', [\App\Http\Controllers\Site\ProjectsController::class, 'index'])->name('home-language');
|
@ -8,6 +8,11 @@ export default defineConfig({
|
|||||||
input: [
|
input: [
|
||||||
'resources/volt/scss/app.scss',
|
'resources/volt/scss/app.scss',
|
||||||
'resources/volt/js/app.js',
|
'resources/volt/js/app.js',
|
||||||
|
|
||||||
|
'resources/home/scss/app.scss',
|
||||||
|
|
||||||
|
'resources/site/scss/app.scss',
|
||||||
|
'resources/site/js/app.js',
|
||||||
],
|
],
|
||||||
refresh: true,
|
refresh: true,
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user