This commit is contained in:
2026-05-13 17:11:09 +02:00
commit ea63897455
2785 changed files with 359868 additions and 0 deletions

View File

@@ -0,0 +1,774 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreContactMessageRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Klevze\ControlPanel\Facades\ActiveLanguage;
use cPad\Plugins\Pages\Services\Page as CmsPage;
class PageController extends Controller
{
public function home(): View
{
$frontpageId = DB::table('contents')
->where('page_type', 'frontpage')
->value('content_id');
$frontpage = $frontpageId
? app(CmsPage::class)->load((int) $frontpageId, app()->getLocale())
: null;
$seo = $this->pageSeo($frontpage, 'Home');
return $this->render('pages.home', $seo['title'], 'home', [
'seo' => $seo,
]);
}
public function about(): View
{
return $this->render('pages.about', 'About Us', 'about');
}
public function work(): View
{
$categoryMap = $this->projectCategoryMap();
return $this->render('pages.work', 'Works', 'work', [
'artworks' => $this->portfolioProjects($categoryMap),
'categories' => array_values($categoryMap),
]);
}
public function project(string $locale, ?int $projectId = null, ?string $slug = null): View
{
$project = $this->findPublicProject($projectId);
abort_if(!$project, 404);
$payload = $this->projectRendererPayload($project);
$headline = (string) ($project->headline ?: 'Single Project');
$metaTitle = (string) ($project->meta_title ?: $project->page_title ?: $headline);
return $this->render('pages.project', $metaTitle, 'work', [
'project' => $this->normalizeProjectForBlade($payload),
'nextProject' => $this->nextPublicProject((int) $project->project_id),
'seo' => [
'title' => $metaTitle,
'meta_description' => (string) ($project->meta_description ?? ''),
'og_title' => (string) ($project->og_title ?: $metaTitle),
'og_description' => (string) ($project->og_description ?: $project->meta_description ?? ''),
'og_image' => $this->projectCoverUrl($project->picture_catalog ?? null)
?? $this->projectCoverUrl($project->picture_cover ?? null)
?? '',
'og_url' => request()->url(),
],
]);
}
public function contact(): View
{
return $this->render('pages.contact', 'Contact Us', 'contact');
}
public function terms(): View
{
return $this->render('pages.terms', 'Terms', 'terms');
}
public function thankyou(): View
{
return $this->render('pages.thankyou', 'Thank you!', 'contact');
}
public function submitContact(StoreContactMessageRequest $request): RedirectResponse
{
Log::info('Website contact form submission', $request->validated());
return redirect()->route('thankyou');
}
private function render(string $view, string $title, string $page, array $data = []): View
{
return view($view, array_merge($data, [
'activeLanguages' => ActiveLanguage::getList('fp'),
'assetBase' => asset('assets'),
'page' => $page,
'title' => $title,
]));
}
private function pageSeo(?object $page, ?string $fallbackTitle = null): array
{
$meta = data_get($page, 'meta') ?? [];
$og = data_get($page, 'og') ?? [];
$translation = data_get($page, 'translation');
$content = data_get($page, 'content');
$title = (string) (data_get($meta, 'title')
?: data_get($translation, 'page_title')
?: data_get($content, 'headline')
?: $fallbackTitle
?: 'Home');
$description = (string) (data_get($meta, 'description')
?: data_get($translation, 'meta_description')
?: '');
return [
'title' => $title,
'meta_description' => $description,
'meta_keywords' => (string) (data_get($meta, 'keywords') ?: ''),
'meta_author' => (string) (data_get($meta, 'author') ?: ''),
'meta_publisher' => (string) (data_get($meta, 'publisher') ?: ''),
'meta_copyright' => (string) (data_get($meta, 'copyright') ?: ''),
'meta_refresh' => data_get($meta, 'refresh'),
'og_title' => (string) (data_get($og, 'title') ?: $title),
'og_description' => (string) (data_get($og, 'description') ?: $description),
'og_image' => (string) (data_get($og, 'image') ?: ''),
'og_type' => (string) (data_get($og, 'type') ?: 'website'),
];
}
private function portfolioProjects(array $categoryMap): array
{
return $this->applyPublicProjectOrder($this->publicProjectsQuery())
->get()
->values()
->map(function ($project, int $index) use ($categoryMap) {
$categoryIds = collect($this->decodeCategoryIds($project->categories ?? null))
->filter(fn ($id) => is_numeric($id) && (int) $id > 0)
->map(fn ($id) => (int) $id)
->unique()
->values();
$filters = $categoryIds
->map(fn (int $id) => $categoryMap[$id]['filter'] ?? null)
->filter()
->values()
->all();
$headline = (string) ($project->headline ?: $project->name ?: 'Untitled project');
return [
'id' => (int) $project->project_id,
'sort_order' => (int) ($project->num ?? 0),
'title' => $headline,
'subline' => (string) ($project->subline ?: ''),
'subtitle' => '',
'thumbnail' => $this->projectThumbnailForGrid($project, $index),
'image' => $this->projectCoverForGrid($project, $index),
'filters' => $filters,
'group' => $this->defaultProjectGroup(),
'url' => route('project', [
'locale' => app()->getLocale(),
'projectId' => $project->project_id,
'slug' => Str::slug($headline),
]),
];
})
->all();
}
private function projectCategoryMap(): array
{
$locale = app()->getLocale();
$fallbackLocale = ActiveLanguage::getDefaultLanguage('fp') ?: config('app.fallback_locale');
return DB::table('projects_categories as c')
->leftJoin('projects_categories_description as cd', function ($join) use ($locale) {
$join->on('c.category_id', '=', 'cd.category_id')
->where('cd.iso', '=', $locale);
})
->leftJoin('projects_categories_description as fallback_cd', function ($join) use ($fallbackLocale) {
$join->on('c.category_id', '=', 'fallback_cd.category_id')
->where('fallback_cd.iso', '=', $fallbackLocale);
})
->where('c.parent_id', 0)
->where('c.active', 1)
->orderBy('c.num')
->select('c.category_id', DB::raw('COALESCE(cd.title, fallback_cd.title) as title'))
->get()
->mapWithKeys(function ($category) {
$title = (string) ($category->title ?? '');
if ($title === '') {
return [];
}
return [(int) $category->category_id => [
'id' => (int) $category->category_id,
'title' => $title,
'filter' => (string) Str::of($title)->lower()->replaceMatches('/[^a-z0-9]+/', ''),
]];
})
->all();
}
private function findPublicProject(?int $projectId = null): ?object
{
$query = $this->applyPublicProjectOrder($this->publicProjectsQuery());
if ($projectId !== null) {
$query->where('p.project_id', $projectId);
}
return $query->first();
}
private function nextPublicProject(int $currentProjectId): ?array
{
$current = $this->findPublicProject($currentProjectId);
if (!$current) {
return null;
}
$next = $this->applyPublicProjectOrder(
$this->publicProjectsQuery()
->where(function ($query) use ($current, $currentProjectId) {
$query->where('p.num', '>', (int) ($current->num ?? 0))
->orWhere(function ($nestedQuery) use ($current, $currentProjectId) {
$nestedQuery->where('p.num', '=', (int) ($current->num ?? 0))
->where('p.project_id', '<', $currentProjectId);
});
})
)->first();
if (!$next) {
$next = $this->applyPublicProjectOrder(
$this->publicProjectsQuery()
->where('p.project_id', '<>', $currentProjectId)
)->first();
}
if (!$next) {
return null;
}
$headline = (string) ($next->headline ?: $next->name ?: 'Next project');
return [
'title' => $headline,
'url' => route('project', [
'locale' => app()->getLocale(),
'projectId' => $next->project_id,
'slug' => Str::slug($headline),
]),
];
}
private function publicProjectsQuery()
{
$locale = app()->getLocale();
$fallbackLocale = ActiveLanguage::getDefaultLanguage('fp') ?: config('app.fallback_locale');
return DB::table('projects as p')
->leftJoin('projects_description as pd', function ($join) use ($locale) {
$join->on('p.project_id', '=', 'pd.project_id')
->where('pd.iso', '=', $locale);
})
->leftJoin('projects_description as fallback_pd', function ($join) use ($fallbackLocale) {
$join->on('p.project_id', '=', 'fallback_pd.project_id')
->where('fallback_pd.iso', '=', $fallbackLocale);
})
->where('p.active', 1)
->select([
'p.project_id',
'p.num',
'p.categories',
'p.picture_cover',
'p.picture_catalog',
'p.picture_catalog_full',
'p.video',
'p.structure',
DB::raw('COALESCE(pd.headline, fallback_pd.headline) as headline'),
DB::raw('COALESCE(pd.subline, fallback_pd.subline) as subline'),
DB::raw('COALESCE(pd.name, fallback_pd.name) as name'),
DB::raw('COALESCE(pd.year, fallback_pd.year) as year'),
DB::raw('COALESCE(pd.preview, fallback_pd.preview) as preview'),
DB::raw('COALESCE(pd.content, fallback_pd.content) as content'),
DB::raw('COALESCE(pd.tag1, fallback_pd.tag1) as tag1'),
DB::raw('COALESCE(pd.tag2, fallback_pd.tag2) as tag2'),
DB::raw('COALESCE(pd.tag3, fallback_pd.tag3) as tag3'),
DB::raw('COALESCE(pd.youtube, fallback_pd.youtube) as youtube'),
DB::raw('COALESCE(pd.page_title, fallback_pd.page_title) as page_title'),
DB::raw('COALESCE(pd.meta_title, fallback_pd.meta_title) as meta_title'),
DB::raw('COALESCE(pd.meta_description, fallback_pd.meta_description) as meta_description'),
DB::raw('COALESCE(pd.og_title, fallback_pd.og_title) as og_title'),
DB::raw('COALESCE(pd.og_description, fallback_pd.og_description) as og_description'),
]);
}
private function applyPublicProjectOrder($query)
{
return $query
->orderBy('p.num')
->orderByDesc('p.project_id');
}
private function normalizeProjectForBlade(array $payload): array
{
$structure = $payload['structure'] ?? [];
$legacy = $payload['legacy'] ?? [];
$lang = $payload['defaultLanguage'] ?? app()->getLocale();
$hasModernStructure = !empty($structure) && (
array_key_exists('header', $structure) ||
array_key_exists('heroMedia', $structure) ||
array_key_exists('contentBlocks', $structure) ||
array_key_exists('blocks', $structure)
);
if ($hasModernStructure) {
$legacyAwards = $this->bladeNormalizeHtmlList((array) ($legacy['awarded'] ?? []));
$structureAwards = $this->bladeNormalizeHtmlList((array) ($structure['metadata']['awarded'] ?? []));
return [
'headline' => $this->bladeFirstFilledHtml($structure['header']['headline'] ?? null, $legacy['headline'] ?? null),
'subline' => $this->bladeFirstFilledHtml($structure['header']['subline'] ?? null, $legacy['subline'] ?? null),
'hero' => $this->bladeHeroSlot($structure['heroMedia'] ?? null),
'year' => $this->bladeFirstFilledText($structure['metadata']['year'] ?? null, $legacy['year'] ?? null),
'clientName' => $this->bladeFirstFilledHtml($structure['metadata']['clientName'] ?? null, $legacy['clientName'] ?? null),
'awarded' => !empty($structureAwards) ? $structureAwards : $legacyAwards,
'description'=> $this->bladeFirstFilledHtml($legacy['description'] ?? null, $structure['metadata']['description'] ?? null),
'blocks' => $this->bladeNormalizeBlocks(
$structure['contentBlocks'] ?? $structure['blocks'] ?? [],
$lang
),
];
}
// Fall back to legacy flat fields
$heroMedia = null;
if (!empty($legacy['youtube'])) {
$heroMedia = ['type' => 'youtube', 'url' => $legacy['youtube'], 'poster' => ''];
} elseif (!empty($legacy['video'])) {
$heroMedia = ['type' => 'video', 'url' => $legacy['video'], 'poster' => ''];
} elseif (!empty($legacy['pictureCatalogFull'])) {
$heroMedia = ['type' => 'image', 'url' => $legacy['pictureCatalogFull'], 'poster' => ''];
} elseif (!empty($legacy['pictureCover'])) {
$heroMedia = ['type' => 'image', 'url' => $legacy['pictureCover'], 'poster' => ''];
}
return [
'headline' => $this->bladeNormalizeHtml((string) ($legacy['headline'] ?? '')),
'subline' => $this->bladeNormalizeHtml((string) ($legacy['subline'] ?? '')),
'hero' => $this->bladeHeroSlot($heroMedia),
'year' => $this->bladeNormalizeText((string) ($legacy['year'] ?? '')),
'clientName' => $this->bladeNormalizeHtml((string) ($legacy['clientName'] ?? '')),
'awarded' => array_map($this->bladeNormalizeHtml(...), (array) ($legacy['awarded'] ?? [])),
'description' => $this->bladeNormalizeHtml((string) ($legacy['description'] ?? '')),
'blocks' => [],
];
}
private function bladeHeroSlot(?array $heroMedia): ?array
{
if (!$heroMedia) {
return null;
}
$type = $heroMedia['type'] ?? 'image';
$url = $heroMedia['url'] ?? '';
if (!$url) {
return null;
}
if ($type === 'image') {
return [
'type' => 'image',
'text' => '',
'image' => ['url' => $url, 'alt' => ''],
'media' => null,
];
}
return [
'type' => 'video',
'text' => '',
'image' => ['url' => $heroMedia['poster'] ?? '', 'alt' => ''],
'media' => $this->bladeNormalizeMedia($heroMedia),
];
}
private function bladeNormalizeBlocks(array $blocks, string $lang): array
{
$result = [];
foreach ($blocks as $block) {
$type = $block['type'] ?? '';
// Legacy type migrations
if ($type === 'FullWidthText') {
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => [
'type' => 'text',
'text' => $this->bladeResolveText($block['content'] ?? [], $lang),
'image' => null,
'media' => null,
],
];
continue;
}
if ($type === 'FullWidthImage') {
$img = $block['image'] ?? [];
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => [
'type' => 'image',
'text' => '',
'image' => ['url' => $img['url'] ?? '', 'alt' => $this->bladeResolveText($img['alt'] ?? [], $lang)],
'media' => null,
],
];
continue;
}
if ($type === 'Video') {
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => [
'type' => 'video',
'text' => '',
'image' => null,
'media' => $this->bladeNormalizeMedia($block['media'] ?? null),
],
];
continue;
}
if ($type === 'TwoColumnImages') {
$images = $block['images'] ?? [];
$result[] = [
'type' => 'TwoColumns',
'hidden' => (bool) ($block['hidden'] ?? false),
'left' => $this->bladeImageSlot($images[0] ?? [], $lang),
'right' => $this->bladeImageSlot($images[1] ?? [], $lang),
];
continue;
}
if ($type === 'TwoColumns') {
$result[] = [
'type' => 'TwoColumns',
'hidden' => (bool) ($block['hidden'] ?? false),
'left' => $this->bladeNormalizeSlot($block['left'] ?? [], $lang),
'right' => $this->bladeNormalizeSlot($block['right'] ?? [], $lang),
];
continue;
}
// FullWidth (default)
$result[] = [
'type' => 'FullWidth',
'hidden' => (bool) ($block['hidden'] ?? false),
'slot' => $this->bladeNormalizeSlot($block['slot'] ?? [], $lang),
];
}
return $result;
}
private function bladeNormalizeSlot(array $slot, string $lang): array
{
return [
'type' => $slot['type'] ?? 'text',
'text' => $this->bladeResolveText($slot['content'] ?? [], $lang),
'image' => [
'url' => $slot['image']['url'] ?? '',
'alt' => $this->bladeResolveText($slot['image']['alt'] ?? [], $lang),
],
'media' => $this->bladeNormalizeMedia($slot['media'] ?? null),
];
}
private function bladeImageSlot(array $image, string $lang): array
{
return [
'type' => 'image',
'text' => '',
'image' => ['url' => $image['url'] ?? '', 'alt' => $this->bladeResolveText($image['alt'] ?? [], $lang)],
'media' => null,
];
}
private function bladeNormalizeMedia(?array $media): ?array
{
if (!$media) {
return null;
}
$type = $media['type'] ?? 'image';
$url = $media['url'] ?? '';
if (!$url) {
return null;
}
return [
'type' => $type,
'url' => $url,
'poster' => $media['poster'] ?? '',
'autoplay' => ($media['autoplay'] ?? false) === true,
'loop' => ($media['loop'] ?? false) === true,
'muted' => ($media['muted'] ?? false) === true,
'embedUrl' => $this->bladeEmbedUrl($type, $media),
];
}
private function bladeResolveText(mixed $content, string $lang): string
{
if (is_string($content)) {
return $this->bladeNormalizeHtml($content);
}
if (!is_array($content) || empty($content)) {
return '';
}
$values = array_values($content);
return $this->bladeNormalizeHtml((string) ($content[$lang] ?? $values[0] ?? ''));
}
private function bladeNormalizeHtml(string $value): string
{
$normalized = $value;
for ($i = 0; $i < 3; $i++) {
$decoded = html_entity_decode($normalized, ENT_QUOTES | ENT_HTML5, 'UTF-8');
if ($decoded === $normalized) {
break;
}
$normalized = $decoded;
}
return $normalized;
}
private function bladeFirstFilledHtml(mixed ...$values): string
{
foreach ($values as $value) {
$normalized = $this->bladeNormalizeHtml((string) ($value ?? ''));
if (trim(strip_tags($normalized)) !== '') {
return $normalized;
}
}
return '';
}
private function bladeNormalizeText(string $value): string
{
return trim(strip_tags($this->bladeNormalizeHtml($value)));
}
private function bladeFirstFilledText(mixed ...$values): string
{
foreach ($values as $value) {
$normalized = $this->bladeNormalizeText((string) ($value ?? ''));
if ($normalized !== '') {
return $normalized;
}
}
return '';
}
private function bladeNormalizeHtmlList(array $values): array
{
$normalized = [];
foreach ($values as $value) {
$item = $this->bladeNormalizeHtml((string) ($value ?? ''));
if (trim(strip_tags($item)) !== '') {
$normalized[] = $item;
}
}
return $normalized;
}
private function bladeEmbedUrl(string $type, array|string $media): string
{
$mediaObject = is_array($media) ? $media : ['url' => $media];
$url = (string) ($mediaObject['url'] ?? '');
if ($type === 'youtube') {
preg_match('/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/', $url, $m);
return isset($m[1])
? 'https://www.youtube.com/embed/' . $m[1]
. '?autoplay=' . (($mediaObject['autoplay'] ?? false) ? '1' : '0')
. '&mute=' . (($mediaObject['muted'] ?? false) ? '1' : '0')
. '&loop=' . (($mediaObject['loop'] ?? false) ? '1' : '0')
. ((($mediaObject['loop'] ?? false) ? '&playlist=' . $m[1] : ''))
: '';
}
if ($type === 'bunny') {
// Normalize /play/ → /embed/ regardless of what was stored
$url = preg_replace('#player\.mediadelivery\.net/play/#', 'player.mediadelivery.net/embed/', $url);
$params = http_build_query([
'autoplay' => ($mediaObject['autoplay'] ?? false) ? 'true' : 'false',
'loop' => ($mediaObject['loop'] ?? false) ? 'true' : 'false',
'muted' => ($mediaObject['muted'] ?? false) ? 'true' : 'false',
'preload' => 'true',
'responsive' => 'true',
]);
return $url . '?' . $params;
}
// frameio, video — url is used as-is (embed src or file src)
return $url;
}
private function projectRendererPayload(object $project): array
{
return [
'structure' => $this->decodeStructure($project->structure ?? null),
'defaultLanguage' => app()->getLocale(),
'legacy' => [
'headline' => (string) ($project->headline ?? ''),
'subline' => (string) ($project->subline ?? ''),
'year' => (string) ($project->year ?? ''),
'clientName' => (string) ($project->name ?? ''),
'description' => (string) ($project->preview ?: $project->content ?: ''),
'awarded' => array_values(array_filter([
$project->tag1 ?? null,
$project->tag2 ?? null,
$project->tag3 ?? null,
])),
'youtube' => (string) ($project->youtube ?? ''),
'video' => $this->projectVideoUrl($project->video ?? null),
'pictureCover' => $this->projectCoverUrl($project->picture_cover ?? null),
'pictureCatalog' => $this->projectCoverUrl($project->picture_catalog ?? null),
'pictureCatalogFull' => $this->projectCoverUrl($project->picture_catalog_full ?? null),
'gallery' => $this->projectGallery((int) $project->project_id),
],
];
}
private function projectGallery(int $projectId): array
{
return DB::table('projects_gallery')
->where('project_id', $projectId)
->orderBy('num')
->get()
->map(function ($photo) {
return [
'url' => asset('files/projects/gallery/' . (int) floor($photo->project_id / 100) . '/' . $photo->project_id . '/' . $photo->diskname),
'alt' => (string) ($photo->name ?? ''),
];
})
->values()
->all();
}
private function decodeCategoryIds(?string $categories): array
{
$decoded = json_decode($categories ?? '[]', true);
return is_array($decoded) ? $decoded : [];
}
private function decodeStructure(?string $structure): array
{
$normalized = html_entity_decode((string) ($structure ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$decoded = json_decode($normalized, true);
return is_array($decoded) ? $decoded : [];
}
private function projectCoverForGrid(object $project, int $index): string
{
return $this->projectCoverUrl($project->picture_cover ?? null)
?? $this->projectCoverUrl($project->picture_catalog ?? null)
?? asset('assets/img/portfolio-' . (($index % 6) + 1) . '.jpg');
}
private function projectThumbnailForGrid(object $project, int $index): array
{
$structure = $this->decodeStructure($project->structure ?? null);
$rawThumbnailMedia = is_array($structure['thumbnailMedia'] ?? null) ? $structure['thumbnailMedia'] : null;
$thumbnailMedia = $this->bladeNormalizeMedia(
$rawThumbnailMedia
);
if (($thumbnailMedia['type'] ?? null) === 'bunny' && !empty($thumbnailMedia['embedUrl'])) {
return [
'type' => 'bunny',
'url' => '',
'embedUrl' => $thumbnailMedia['embedUrl'],
];
}
$heroMedia = $this->bladeNormalizeMedia(
is_array($structure['heroMedia'] ?? null) ? $structure['heroMedia'] : null
);
$prefersBunnyThumbnail = ($rawThumbnailMedia['type'] ?? null) === 'bunny';
if ($prefersBunnyThumbnail && ($heroMedia['type'] ?? null) === 'bunny' && !empty($heroMedia['embedUrl'])) {
return [
'type' => 'bunny',
'url' => '',
'embedUrl' => $heroMedia['embedUrl'],
];
}
return [
'type' => 'image',
'url' => $this->projectCoverForGrid($project, $index),
'embedUrl' => '',
];
}
private function projectCoverUrl(?string $filename): ?string
{
if (!$filename) {
return null;
}
return asset('files/projects/cover/' . $filename);
}
private function projectVideoUrl(?string $filename): ?string
{
if (!$filename) {
return null;
}
return asset('files/projects/video/' . $filename);
}
private function defaultProjectGroup(): array
{
return [
'name' => 'Aritmija',
'label' => 'Uploaded by',
'icon' => 'fas fa-layer-group',
'color' => '#4050FF',
];
}
}