function ($query) { $query->select(['id', 'name']); }, 'category' => function ($query) { $query->with('translations'); }, 'translations' => function ($query) { $query ->where('locale', App::getLocale()); // ->whereDate('from', '<=', now()) //TODO: Exclude date queries ONLY for post Admins! // ->where( function($query) { // $query->whereDate('till', '>', now())->orWhereNull('till'); // }) }, ]) ->where('id', $id) ->firstOrFail(); // Poll posts have their own page if (\App\Models\Poll::where('post_id', $post->id)->exists()) { $slug = $post->translations->first()?->slug ?? PostTranslation::where('post_id', $post->id)->value('slug'); if ($slug) { return redirect()->route('poll.show', ['slug' => $slug]); } } if ($post->media) { $media = Post::find($id)->getFirstMedia('posts'); } if ($post->translations->count() >= 1) { if ($post->category->translations) { $category = $post->category->translations->where('locale', App::getLocale())->first()->name; } $update = Carbon::createFromTimeStamp(strtotime($post->translations->first()->updated_at), config('app.timezone'))->isoFormat('LL'); } else { // No translation in current locale - check for fallback translations return $this->handleNoTranslation($post, $id); } // Use guest layout for non-authenticated users, app layout for authenticated users $viewName = Auth::check() ? 'posts.show' : 'posts.show-guest'; return ($post != null ? view($viewName, compact(['post','media','category','update'])) : abort(403)); } /** * Handle case when no translation exists for current locale. * Check for available translations in other locales and offer fallback. */ private function handleNoTranslation($post, $postId) { // Get all available translations for this post (active ones) $availableTranslation = PostTranslation::where('post_id', $postId) ->whereDate('from', '<=', now()) ->where(function ($query) { $query->whereDate('till', '>', now())->orWhereNull('till'); }) ->first(); $viewData = [ 'fallbackLocale' => null, 'fallbackLanguageName' => null, 'fallbackUrl' => null, 'post' => $post, ]; if ($availableTranslation) { // Found an available translation in another locale $fallbackLocale = $availableTranslation->locale; $fallbackLanguageName = Language::where('lang_code', $fallbackLocale)->first()->name ?? $fallbackLocale; // Generate URL to the same post in the fallback locale $fallbackUrl = \LaravelLocalization::getLocalizedURL($fallbackLocale, route('post.show', ['id' => $postId])); $viewData['fallbackLocale'] = $fallbackLocale; $viewData['fallbackLanguageName'] = __($fallbackLanguageName); $viewData['fallbackUrl'] = $fallbackUrl; } // Use guest layout for non-authenticated users, app layout for authenticated users $viewName = Auth::check() ? 'posts.no_translation' : 'posts.no_translation-guest'; return view($viewName, $viewData); } /** * Show view by .../posts/{slug}. * Post will be shown in user's App:locale() language if available, even is the slug is in another language. * * @param mixed $slug * @return void */ public function showBySlug($slug) { $postTranslation = PostTranslation::where('slug', $slug)->get()->first(); if (!$postTranslation) { return view('posts.not_found'); } $id = $postTranslation->post_id; $locale = $postTranslation->locale; // Poll posts have their own page if (\App\Models\Poll::where('post_id', $id)->exists()) { return redirect()->route('poll.show', ['slug' => $slug]); } $post = Post::with([ 'postable' => function ($query) { $query->select(['id', 'name']); }, 'category' => function ($query) { $query->with('translations'); }, 'meeting', 'translations' => function ($query) { //TODO!: Currently only user 1 (Super-admin) can view unpublished posts, change to permission/role based! $query->where('locale', App::getLocale()); // Only show published posts for guests and non-admin users if (!Auth::guard('web')->check() || Auth::guard('web')->user()->id != 1) { $query->whereDate('from', '<=', now()) ->where(function ($query) { $query->whereDate('till', '>', now())->orWhereNull('till'); }); } } ]) ->where('id', $id) ->first(); if ($post->media) { $media = Post::find($id)->getFirstMedia('posts'); } if ($post->translations->count() >= 1) { if ($post->category->translations) { $category = $post->category->translations->where('locale', App::getLocale())->first()->name; } $update = Carbon::createFromTimeStamp(strtotime($post->translations->first()->updated_at), config('app.timezone'))->isoFormat('LL'); } else { // No translation in current locale - check for fallback translations return $this->handleNoTranslation($post, $id); } // Use guest layout for non-authenticated users, app layout for authenticated users $viewName = Auth::check() ? 'posts.show' : 'posts.show-guest'; return ($post != null ? view($viewName, compact(['post','media','category','update'])) : abort(403)); } public function manage() { if (getActiveProfileType() !== 'Admin') { abort(403, __('Admin profile required')); } return view('posts.manage'); } public function postPdf($id) { $data = $this->buildPostPdfData($id); $pdf = Pdf::loadView('posts.pdf', $data) ->setPaper('A4', 'portrait') ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), ]); return $pdf->download('post-' . $data['post']->id . '.pdf'); } public function postPdfBulk(\Illuminate\Http\Request $request) { $ids = array_filter(array_map('intval', explode(',', $request->input('ids', '')))); $perPage = in_array((int) $request->input('per_page', 1), [1, 2, 4]) ? (int) $request->input('per_page', 1) : 1; if (empty($ids)) abort(400); $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $isAdmin = $activeProfileType === \App\Models\Admin::class || ($activeProfileType === \App\Models\Bank::class && \App\Models\Bank::find($activeProfileId)?->level === 0); if (!$isAdmin) abort(403); $posts = []; foreach ($ids as $translationId) { $translation = PostTranslation::withTrashed()->find($translationId); if (!$translation) continue; $locale = $translation->locale ?? App::getLocale(); $posts[] = $this->buildPostPdfDataForLocale($translation->post_id, $locale); } if (empty($posts)) abort(403); $view = 'posts.pdf-bulk'; $orientation = $perPage === 2 ? 'landscape' : 'portrait'; $pdf = Pdf::loadView($view, compact('posts', 'perPage')) ->setPaper('A4', $orientation) ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), 'margin_top' => 0, 'margin_right' => 0, 'margin_bottom' => 0, 'margin_left' => 0, ]); return $pdf->download('posts-bulk-' . now()->format('Ymd-His') . '.pdf'); } private function buildPostPdfDataForLocale(int $postId, string $locale): array { $previous = App::getLocale(); App::setLocale($locale); try { return $this->buildPostPdfData($postId); } finally { App::setLocale($previous); } } private function buildPostPdfData($id): array { $locale = App::getLocale(); $post = Post::with([ 'author', 'category.translations', 'translations' => function ($query) use ($locale) { $query->where('locale', $locale); }, 'meeting.meetingable', 'meeting.location.city.translations', ])->findOrFail($id); // Polls are for logged-in profiles only abort_if($post->isPoll() && !Auth::guard('web')->check(), 404); $translation = $post->translations->first(); $categoryName = $post->category?->translations->firstWhere('locale', $locale)?->name ?? $post->category?->translations->first()?->name; $categoryType = $post->category?->type ?? ''; $isEvent = $categoryType === 'App\Models\Meeting'; $isArticle = $categoryType === 'App\Models\Article'; $isNews = $categoryType === 'App\Models\News'; $isImagePost = str_starts_with($categoryType, 'App\Models\ImagePost'); $update = $translation ? Carbon::parse($translation->updated_at)->isoFormat('LL') : null; // Hero image as base64 for DomPDF (GD for webp, direct read for others) $heroBase64 = null; $heroMedia = $post->getFirstMedia('posts'); if ($heroMedia) { $heroPath = $heroMedia->getPath(); if (file_exists($heroPath)) { $raw = @file_get_contents($heroPath); if ($raw) { $src = @imagecreatefromstring($raw); if ($src) { // Crop to A4 portrait aspect ratio (cover behaviour) $srcW = imagesx($src); $srcH = imagesy($src); $dstW = 1240; // 210mm @ 150dpi $dstH = 1754; // 297mm @ 150dpi $scale = max($dstW / $srcW, $dstH / $srcH); $scaledW = (int) round($srcW * $scale); $scaledH = (int) round($srcH * $scale); $cropX = (int) round(($scaledW - $dstW) / 2); $cropY = (int) round(($scaledH - $dstH) / 2); $dst = imagecreatetruecolor($dstW, $dstH); imagecopyresampled($dst, $src, 0, 0, (int) round($cropX / $scale), (int) round($cropY / $scale), $dstW, $dstH, (int) round($dstW / $scale), (int) round($dstH / $scale)); // Boost saturation +50%: blend color image with its grayscale at factor > 1 // output = gray + (color - gray) * 1.5 → clamp to 0-255 $gray = imagecreatetruecolor($dstW, $dstH); imagecopy($gray, $dst, 0, 0, 0, 0, $dstW, $dstH); imagefilter($gray, IMG_FILTER_GRAYSCALE); for ($px = 0; $px < $dstW; $px++) { for ($py = 0; $py < $dstH; $py++) { $c = imagecolorat($dst, $px, $py); $g = imagecolorat($gray, $px, $py); $gr = ($g >> 16) & 0xFF; $nr = max(0, min(255, (int) round($gr + ((($c >> 16) & 0xFF) - $gr) * 1.75))); $ng = max(0, min(255, (int) round($gr + ((($c >> 8) & 0xFF) - $gr) * 1.75))); $nb = max(0, min(255, (int) round($gr + (($c & 0xFF) - $gr) * 1.75))); imagesetpixel($dst, $px, $py, ($nr << 16) | ($ng << 8) | $nb); } } imagedestroy($gray); ob_start(); imagejpeg($dst, null, 85); $heroBase64 = 'data:image/jpeg;base64,' . base64_encode(ob_get_clean()); imagedestroy($src); imagedestroy($dst); } } } } // Author avatar as base64 (SVG skipped — GD cannot render SVG) $avatarBase64 = null; $author = $post->author; if ($author && $author->profile_photo_path) { $avatarPath = storage_path('app/public/' . $author->profile_photo_path); $ext = strtolower(pathinfo($avatarPath, PATHINFO_EXTENSION)); if (file_exists($avatarPath) && $ext !== 'svg') { $mime = mime_content_type($avatarPath) ?: 'image/jpeg'; $avatarBase64 = 'data:' . $mime . ';base64,' . base64_encode(file_get_contents($avatarPath)); } } $brandColor = theme_color('brand') ?: '#000000'; // Platform logo $logoBase64 = null; $logoPath = theme_email_logo_file(); if (file_exists($logoPath)) { $logoBase64 = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath)); } // Hero logo: invert RGB to white while preserving alpha transparency $logoWhiteBase64 = null; if ($logoPath && file_exists($logoPath)) { $src = @imagecreatefrompng($logoPath); if ($src) { $w = imagesx($src); $h = imagesy($src); $dst = imagecreatetruecolor($w, $h); imagealphablending($dst, false); imagesavealpha($dst, true); // Fill with transparent background $transparent = imagecolorallocatealpha($dst, 0, 0, 0, 127); imagefill($dst, 0, 0, $transparent); // Copy pixel by pixel, inverting RGB but keeping alpha for ($x = 0; $x < $w; $x++) { for ($y = 0; $y < $h; $y++) { $rgba = imagecolorat($src, $x, $y); $alpha = ($rgba >> 24) & 0x7F; $r = 255 - (($rgba >> 16) & 0xFF); $g = 255 - (($rgba >> 8) & 0xFF); $b = 255 - ($rgba & 0xFF); imagesetpixel($dst, $x, $y, imagecolorallocatealpha($dst, $r, $g, $b, $alpha)); } } ob_start(); imagepng($dst); $logoWhiteBase64 = 'data:image/png;base64,' . base64_encode(ob_get_clean()); imagedestroy($src); imagedestroy($dst); } } // QR code as inline SVG (no ImageMagick needed) $postUrl = route('post.show', ['id' => $post->id]); $qrSvg = \SimpleSoftwareIO\QrCode\Facades\QrCode::format('svg') ->size(300)->margin(2)->errorCorrection('M')->color(0, 0, 0)->generate($postUrl); $qrBase64 = 'data:image/svg+xml;base64,' . base64_encode($qrSvg); // Sanitize and truncate content for PDF, preserving HTML formatting. // Truncation is based on plain-text length; the HTML is kept intact up to that point. $rawContent = $translation?->content ?? ''; $plainContent = strip_tags($rawContent); $contentMax = 800; $isTruncated = mb_strlen($plainContent) > $contentMax; $pdfContent = \App\Helpers\StringHelper::sanitizeHtml($rawContent); // Pass the full excerpt — CSS overflow:hidden on .hero-excerpt clips it within the hero. $rawExcerpt = $translation?->excerpt ?? ''; $pdfExcerpt = $rawExcerpt; $meetingCityName = null; if ($isEvent && $post->meeting?->location?->city) { $city = $post->meeting->location->city; $meetingCityName = $city->translations->firstWhere('locale', $locale)?->name ?? $city->translations->first()?->name; } return compact( 'post', 'translation', 'categoryName', 'update', 'categoryType', 'isEvent', 'isArticle', 'isNews', 'isImagePost', 'heroBase64', 'avatarBase64', 'logoBase64', 'logoWhiteBase64', 'qrBase64', 'pdfContent', 'isTruncated', 'pdfExcerpt', 'author', 'brandColor', 'meetingCityName' ); } public function notFound() { return view('posts.not_found'); } }