orderBy('price') ->get() ->map(fn (Package $package) => $this->presentPackage($package)) ->values() ->all(); return Inertia::render('marketing/Home', compact('packages')); } public function contact(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|max:255', 'message' => 'required|string|max:1000', 'nickname' => 'present|size:0', ]); $locale = app()->getLocale(); $contactAddress = config('mail.contact_address', config('mail.from.address')); if (! $contactAddress) { throw ValidationException::withMessages([ 'email' => __('marketing.contact.error_recipient_missing', [], $locale) ?: 'Anfrage derzeit nicht möglich. Bitte später erneut versuchen.', ]); } try { Mail::raw( __('emails.contact.body', [ 'name' => $request->name, 'email' => $request->email, 'message' => $request->message, ], $locale), function ($message) use ($contactAddress, $locale) { $message->to($contactAddress) ->subject(__('emails.contact.subject', [], $locale)); } ); Mail::to($request->email) ->locale($locale) ->queue(new ContactConfirmation($request->name)); } catch (\Throwable $exception) { Log::error('Contact form mail failed', [ 'error' => $exception->getMessage(), 'code' => $exception->getCode(), ]); throw ValidationException::withMessages([ 'email' => __('marketing.contact.error_send_failed', [], $locale) ?: 'Nachricht konnte nicht gesendet werden. Bitte versuche es später erneut.', ]); } return redirect() ->back() ->with('success', __('marketing.contact.success', [], $locale)); } public function contactView(Request $request) { $locale = \App\Support\LocaleConfig::canonicalize((string) ($request->route('locale') ?? app()->getLocale())); $secondSegment = $request->segment(2); $slug = $secondSegment ? '/'.trim((string) $secondSegment, '/') : '/'; if ($locale === 'en' && $slug === '/kontakt') { return redirect()->route('marketing.contact', [ 'locale' => $request->route('locale') ?? $locale, ], 301); } if ($locale === 'de' && $slug === '/contact') { return redirect()->route('kontakt', [ 'locale' => $request->route('locale') ?? $locale, ], 301); } return Inertia::render('marketing/Kontakt'); } /** * Handle package purchase flow. */ public function buyPackages(Request $request, string $locale, $packageId) { Log::info('Buy packages called', ['auth' => Auth::check(), 'locale' => $locale, 'package_id' => $packageId]); $package = Package::findOrFail($packageId); $couponCode = $this->rememberCouponFromRequest($request, $package); if (! Auth::check()) { return redirect()->route('register', ['package_id' => $package->id, 'coupon' => $couponCode]) ->with('message', __('marketing.packages.register_required')); } $user = Auth::user(); if (! $user->email_verified_at) { return redirect()->route('verification.notice') ->with('message', __('auth.verification_required')); } $tenant = $user->tenant; if (! $tenant) { abort(500, 'Tenant not found'); } if ($package->price == 0) { TenantPackage::updateOrCreate( [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ], [ 'price' => $package->price, 'active' => true, 'purchased_at' => now(), 'expires_at' => now()->addYear(), ] ); PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider' => 'free', 'provider_id' => 'free', 'price' => $package->price, 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription', 'purchased_at' => now(), 'refunded' => false, ]); return redirect('/event-admin')->with('success', __('marketing.packages.free_assigned')); } if (! $package->paddle_price_id) { Log::warning('Package missing Paddle price id', ['package_id' => $package->id]); return redirect()->route('packages', [ 'locale' => app()->getLocale(), 'highlight' => $package->slug, ]) ->with('error', __('marketing.packages.paddle_not_configured')); } $session = $this->checkoutSessions->createOrResume($user, $package, [ 'tenant' => $tenant, ]); $this->checkoutSessions->selectProvider($session, CheckoutSession::PROVIDER_PADDLE); $appliedDiscountId = null; if ($couponCode) { try { $preview = $this->coupons->preview($couponCode, $package, $tenant); $this->checkoutSessions->applyCoupon($session, $preview['coupon'], $preview['pricing']); $appliedDiscountId = $preview['coupon']->paddle_discount_id; $request->session()->forget('marketing.checkout.coupon'); } catch (ValidationException $exception) { $request->session()->flash('coupon_error', $exception->errors()['code'][0] ?? __('marketing.coupon.errors.generic')); } } $checkout = $this->paddleCheckout->createCheckout($tenant, $package, [ 'success_url' => route('marketing.success', [ 'locale' => app()->getLocale(), 'packageId' => $package->id, ]), 'return_url' => route('packages', [ 'locale' => app()->getLocale(), 'highlight' => $package->slug, ]), 'metadata' => [ 'checkout_session_id' => $session->id, 'coupon_code' => $couponCode, ], 'discount_id' => $appliedDiscountId, ]); $session->forceFill([ 'paddle_checkout_id' => $checkout['id'] ?? $session->paddle_checkout_id, 'provider_metadata' => array_merge($session->provider_metadata ?? [], array_filter([ 'paddle_checkout_id' => $checkout['id'] ?? null, 'paddle_checkout_url' => $checkout['checkout_url'] ?? null, 'paddle_expires_at' => $checkout['expires_at'] ?? null, ])), ])->save(); $redirectUrl = $checkout['checkout_url'] ?? null; if (! $redirectUrl) { throw ValidationException::withMessages([ 'paddle' => __('marketing.packages.paddle_checkout_failed'), ]); } return redirect()->away($redirectUrl); } public function success(Request $request, $packageId = null) { if (Auth::check() && Auth::user()->email_verified_at) { return redirect('/event-admin')->with('success', __('marketing.success.welcome')); } return Inertia::render('marketing/Success', compact('packageId')); } protected function rememberCouponFromRequest(Request $request, Package $package): ?string { $input = Str::upper(trim((string) $request->input('coupon'))); if ($input !== '') { $request->session()->put('marketing.checkout.coupon', [ 'package_id' => $package->id, 'code' => $input, ]); return $input; } if ($request->has('coupon')) { $request->session()->forget('marketing.checkout.coupon'); return null; } $stored = $request->session()->get('marketing.checkout.coupon'); if ($stored && (int) ($stored['package_id'] ?? 0) === (int) $package->id) { return $stored['code'] ?? null; } return null; } public function blogIndex(Request $request, string $locale) { $locale = $locale ?: app()->getLocale(); Log::info('Blog Index Debug - Initial', [ 'locale' => $locale, 'full_url' => $request->fullUrl(), ]); $query = BlogPost::query() ->with('author') ->whereHas('category', function ($query) { $query->where('slug', 'blog'); }); $totalWithCategory = $query->count(); Log::info('Blog Index Debug - With Category', ['count' => $totalWithCategory]); $query->where('is_published', true) ->whereNotNull('published_at') ->where('published_at', '<=', now()); $totalPublished = $query->count(); Log::info('Blog Index Debug - Published', ['count' => $totalPublished]); // Removed translation filter for now $totalWithTranslation = $query->count(); Log::info('Blog Index Debug - With Translation', ['count' => $totalWithTranslation, 'locale' => $locale]); $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' => $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, ]; }); Log::info('Blog Index Debug - Final Posts', [ 'count' => $posts->count(), 'total' => $posts->total(), 'posts_data' => $posts->toArray(), 'first_post_title' => $posts->count() > 0 ? ($posts->first()['title'] ?? 'No title') : 'No 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) { $locale = $locale ?: app()->getLocale(); $postModel = BlogPost::query() ->with('author') ->whereHas('category', function ($query) { $query->where('slug', 'blog'); }) ->where('slug', $slug) ->where('is_published', true) ->whereNotNull('published_at') ->where('published_at', '<=', now()) // Removed translation filter for now ->firstOrFail(); // Transform to array with translated strings for the current locale $markdown = $postModel->getTranslation('content', $locale) ?? $postModel->getTranslation('content', 'de') ?? ''; $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' => $excerpt, 'excerpt_html' => $this->convertMarkdownToHtml($excerpt), 'content' => $markdown, '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')); } public function howItWorks() { return Inertia::render('marketing/HowItWorks'); } public function demo() { $joinToken = optional(Event::firstWhere('slug', 'demo-wedding-2025')) ?->joinTokens() ->latest('id') ->first(); $demoToken = null; if ($joinToken) { if (! empty($joinToken->token_encrypted)) { try { $demoToken = Crypt::decryptString($joinToken->token_encrypted); } catch (\Throwable $exception) { Log::warning('Failed to decrypt demo join token', [ 'token_id' => $joinToken->id, 'exception' => $exception->getMessage(), ]); } } if (! $demoToken) { $demoToken = $joinToken->metadata['plain_token'] ?? null; } } return Inertia::render('marketing/Demo', [ 'demoToken' => $demoToken, ]); } public function packagesIndex() { $endcustomerPackages = Package::where('type', 'endcustomer') ->orderBy('price') ->get() ->map(fn (Package $package) => $this->presentPackage($package)) ->values() ->all(); $resellerPackages = Package::where('type', 'reseller') ->orderBy('price') ->get() ->map(fn (Package $package) => $this->presentPackage($package)) ->values() ->all(); return Inertia::render('marketing/Packages', [ 'endcustomerPackages' => $endcustomerPackages, 'resellerPackages' => $resellerPackages, ]); } public function occasionsType(Request $request, string $locale, string $type) { Log::info('OccasionsType hit', [ 'type' => $type, 'locale' => $locale, 'url' => request()->fullUrl(), 'route' => request()->route()->getName(), 'isInertia' => request()->header('X-Inertia'), ]); $normalized = strtolower($type); $typeMap = [ 'hochzeit' => 'hochzeit', 'wedding' => 'hochzeit', 'geburtstag' => 'geburtstag', 'birthday' => 'geburtstag', 'firmenevent' => 'firmenevent', 'corporate-event' => 'firmenevent', 'konfirmation' => 'konfirmation', 'confirmation' => 'konfirmation', ]; if (! array_key_exists($normalized, $typeMap)) { Log::warning('Invalid occasion type accessed', ['type' => $type]); abort(404, 'Invalid occasion type'); } $baseSlug = $typeMap[$normalized]; $canonical = [ 'hochzeit' => [ 'de' => 'hochzeit', 'en' => 'wedding', ], 'geburtstag' => [ 'de' => 'geburtstag', 'en' => 'birthday', ], 'firmenevent' => [ 'de' => 'firmenevent', 'en' => 'corporate-event', ], 'konfirmation' => [ 'de' => 'konfirmation', 'en' => 'confirmation', ], ]; $canonicalSlug = $canonical[$baseSlug][$locale] ?? $baseSlug; $currentSlug = strtolower($type); if ($currentSlug !== $canonicalSlug) { $routeName = $locale === 'en' ? 'occasions.type' : 'anlaesse.type'; return redirect()->route($routeName, [ 'locale' => $locale, 'type' => $canonicalSlug, ], 301); } return Inertia::render('marketing/Occasions', [ 'type' => $baseSlug, '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\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('%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), ]; } }