admin widget zu dokploy geswitched, viele übersetzungen im Frontend vervollständigt und Anlässe-Seiten mit ChatGPT ausgebaut

This commit is contained in:
Codex Agent
2025-11-19 13:12:35 +01:00
parent 125c624588
commit d8f365ddd6
30 changed files with 2820 additions and 293 deletions

View File

@@ -4,8 +4,8 @@ namespace App\Http\Controllers;
use App\Mail\ContactConfirmation;
use App\Models\BlogPost;
use App\Models\Event;
use App\Models\CheckoutSession;
use App\Models\Event;
use App\Models\Package;
use App\Models\PackagePurchase;
use App\Models\TenantPackage;
@@ -33,6 +33,8 @@ class MarketingController extends Controller
{
use PresentsPackages;
private ?MarkdownConverter $markdownConverter = null;
public function __construct(
private readonly CheckoutSessionService $checkoutSessions,
private readonly PaddleCheckoutService $paddleCheckout,
@@ -292,11 +294,14 @@ class MarketingController extends Controller
$posts = $query->orderBy('published_at', 'desc')
->paginate(4)
->through(function (BlogPost $post) use ($locale) {
$excerpt = $post->getTranslation('excerpt', $locale) ?? $post->getTranslation('excerpt', 'de') ?? '';
return [
'id' => $post->id,
'slug' => $post->slug,
'title' => $post->getTranslation('title', $locale) ?? $post->getTranslation('title', 'de') ?? '',
'excerpt' => $post->getTranslation('excerpt', $locale) ?? $post->getTranslation('excerpt', 'de') ?? '',
'excerpt' => $excerpt,
'excerpt_html' => $this->convertMarkdownToHtml($excerpt),
'featured_image' => $post->featured_image ?? $post->banner_url ?? null,
'published_at' => optional($post->published_at)->toDateString(),
'author' => $post->author ? ['name' => $post->author->name] : null,
@@ -310,7 +315,18 @@ class MarketingController extends Controller
'first_post_title' => $posts->count() > 0 ? ($posts->first()['title'] ?? 'No title') : 'No posts',
]);
return Inertia::render('marketing/Blog', compact('posts'));
$postsArray = $posts->toArray();
$postsArray['links'] = array_map(function (array $link) use ($locale) {
return [
'url' => $link['url'],
'label' => $this->localizePaginationLabel($link['label'] ?? '', $locale),
'active' => (bool) ($link['active'] ?? false),
];
}, $postsArray['links'] ?? []);
return Inertia::render('marketing/Blog', [
'posts' => $postsArray,
]);
}
public function blogShow(string $locale, string $slug)
@@ -330,29 +346,27 @@ class MarketingController extends Controller
// Transform to array with translated strings for the current locale
$markdown = $postModel->getTranslation('content', $locale) ?? $postModel->getTranslation('content', 'de') ?? '';
$environment = new Environment;
$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);
$environment->addExtension(new AutolinkExtension);
$environment->addExtension(new StrikethroughExtension);
$environment->addExtension(new TaskListExtension);
$converter = new MarkdownConverter($environment);
$contentHtml = (string) $converter->convert($markdown);
$excerpt = $postModel->getTranslation('excerpt', $locale) ?? $postModel->getTranslation('excerpt', 'de') ?? '';
$contentHtml = $this->convertMarkdownToHtml($markdown);
[$contentHtmlWithIds, $headings] = $this->decorateHeadings($contentHtml);
$post = [
'id' => $postModel->id,
'title' => $postModel->getTranslation('title', $locale) ?? $postModel->getTranslation('title', 'de') ?? '',
'excerpt' => $postModel->getTranslation('excerpt', $locale) ?? $postModel->getTranslation('excerpt', 'de') ?? '',
'excerpt' => $excerpt,
'excerpt_html' => $this->convertMarkdownToHtml($excerpt),
'content' => $markdown,
'content_html' => $contentHtml,
'content_html' => $contentHtmlWithIds,
'headings' => $headings,
'featured_image' => $postModel->featured_image ?? $postModel->banner_url ?? null,
'published_at' => $postModel->published_at->toDateString(),
'slug' => $postModel->slug,
'url' => route('blog.show', ['locale' => $locale, 'slug' => $postModel->slug], absolute: true),
'author' => $postModel->author ? [
'name' => $postModel->author->name,
] : null,
'previous_post' => $this->presentAdjacentPost($postModel, $locale, direction: 'previous'),
'next_post' => $this->presentAdjacentPost($postModel, $locale, direction: 'next'),
];
return Inertia::render('marketing/BlogShow', compact('post'));
@@ -481,4 +495,119 @@ class MarketingController extends Controller
'requestedType' => $normalized,
]);
}
private function localizePaginationLabel(?string $label, string $locale): string
{
$decoded = trim(html_entity_decode(strip_tags($label ?? '')));
if ($decoded === '') {
return '';
}
if ($decoded === '...') {
return '…';
}
$normalized = Str::lower($decoded);
if (Str::contains($normalized, ['previous', 'vorherige', 'zurück'])) {
return __('marketing.blog.pagination.previous', [], $locale);
}
if (Str::contains($normalized, ['next', 'weiter', 'nächste'])) {
return __('marketing.blog.pagination.next', [], $locale);
}
return $decoded;
}
private function markdownConverter(): MarkdownConverter
{
if (! $this->markdownConverter instanceof MarkdownConverter) {
$environment = new Environment;
$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);
$environment->addExtension(new AutolinkExtension);
$environment->addExtension(new StrikethroughExtension);
$environment->addExtension(new TaskListExtension);
$this->markdownConverter = new MarkdownConverter($environment);
}
return $this->markdownConverter;
}
private function convertMarkdownToHtml(?string $markdown): string
{
if ($markdown === null || trim((string) $markdown) === '') {
return '';
}
return (string) $this->markdownConverter()->convert($markdown);
}
private function decorateHeadings(string $html): array
{
$headings = [];
$usedSlugs = [];
$updatedHtml = preg_replace_callback('/<h([2-3])>(.*?)<\/h\1>/', function ($matches) use (&$headings, &$usedSlugs) {
$level = (int) $matches[1];
$text = trim(strip_tags($matches[2]));
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5);
if ($text === '') {
return $matches[0];
}
$baseSlug = Str::slug($text) ?: 'section';
$slug = $baseSlug;
$iterator = 1;
while (in_array($slug, $usedSlugs, true)) {
$slug = $baseSlug.'-'.(++$iterator);
}
$usedSlugs[] = $slug;
$headings[] = [
'text' => $text,
'slug' => $slug,
'level' => $level,
];
return sprintf('<h%s id="%s">%s</h%s>', $level, $slug, $matches[2], $level);
}, $html) ?: $html;
return [$updatedHtml, $headings];
}
private function presentAdjacentPost(BlogPost $current, string $locale, string $direction): ?array
{
$operator = $direction === 'previous' ? '<' : '>';
$orderDirection = $direction === 'previous' ? 'desc' : 'asc';
$neighbor = BlogPost::query()
->with('author')
->whereHas('category', function ($query) {
$query->where('slug', 'blog');
})
->where('is_published', true)
->whereNotNull('published_at')
->where('published_at', $operator, $current->published_at)
->orderBy('published_at', $orderDirection)
->first();
if (! $neighbor) {
return null;
}
$excerpt = $neighbor->getTranslation('excerpt', $locale) ?? $neighbor->getTranslation('excerpt', 'de') ?? '';
return [
'slug' => $neighbor->slug,
'title' => $neighbor->getTranslation('title', $locale) ?? $neighbor->getTranslation('title', 'de') ?? '',
'excerpt' => $excerpt,
'excerpt_html' => $this->convertMarkdownToHtml($excerpt),
];
}
}