das marketing frontend wurde auf lokalisierte urls umgestellt.

This commit is contained in:
Codex Agent
2025-11-03 15:50:10 +01:00
parent c0c1d31385
commit 55c606bdd4
47 changed files with 1592 additions and 251 deletions

View File

@@ -150,6 +150,9 @@ class SendAbandonedCheckoutReminders extends Command
{
// Für jetzt: Einfache Package-URL
// Später: Persönliche Resume-Token URLs
return route('buy.packages', $checkout->package_id);
return route('buy.packages', [
'locale' => config('app.locale', 'de'),
'packageId' => $checkout->package_id,
]);
}
}

View File

@@ -39,6 +39,7 @@ class RegisteredUserController extends Controller
{
$fullName = trim($request->first_name.' '.$request->last_name);
$validated = $request->validate([
'username' => ['required', 'string', 'max:255', 'unique:'.User::class],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
@@ -51,7 +52,7 @@ class RegisteredUserController extends Controller
'package_id' => ['nullable', 'exists:packages,id'],
]);
$shouldAutoVerify = App::environment(['local', 'testing']);
$shouldAutoVerify = App::environment('local');
$user = User::create([
'username' => $validated['username'],
@@ -98,12 +99,16 @@ class RegisteredUserController extends Controller
]),
]);
if (! $user->tenant_id) {
$user->forceFill(['tenant_id' => $tenant->id])->save();
}
event(new Registered($user));
// Send Welcome Email
Mail::to($user)
->locale($user->preferred_locale ?? app()->getLocale())
->queue(new \App\Mail\Welcome($user));
->send(new \App\Mail\Welcome($user));
if ($request->filled('package_id')) {
$package = \App\Models\Package::find($request->package_id);
@@ -131,7 +136,10 @@ class RegisteredUserController extends Controller
Auth::login($user);
} elseif ($package) {
// Redirect to buy for paid package
return redirect()->route('marketing.buy', $package->id);
return redirect()->route('buy.packages', [
'locale' => session('preferred_locale', app()->getLocale()),
'packageId' => $package->id,
]);
}
}

View File

@@ -200,7 +200,9 @@ class CheckoutGoogleController extends Controller
return redirect()->route('purchase.wizard', ['package' => $firstPackageId]);
}
return redirect()->route('packages');
return redirect()->route('packages', [
'locale' => app()->getLocale(),
]);
}
private function flashError(Request $request, string $message): void

View File

@@ -3,6 +3,8 @@
namespace App\Http\Controllers;
use App\Models\LegalPage;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
@@ -16,21 +18,42 @@ use League\CommonMark\MarkdownConverter;
class LegalPageController extends Controller
{
public function show(?string $slug = null): Response
public function show(Request $request, string $locale, ?string $slug = null): Response
{
$resolvedSlug = $this->resolveSlug($slug);
$page = null;
$page = LegalPage::query()
->where('slug', $resolvedSlug)
->where('is_published', true)
->orderByDesc('version')
->first();
if (! $page) {
abort(404);
try {
$page = LegalPage::query()
->where('slug', $resolvedSlug)
->where('is_published', true)
->orderByDesc('version')
->first();
} catch (QueryException $exception) {
// Table does not exist or query failed; fallback to filesystem documents
$page = null;
}
$locale = $request->route('locale', app()->getLocale());
if (! $page) {
$fallback = $this->loadFallbackDocument($resolvedSlug, $locale);
if (! $fallback) {
abort(404);
}
return Inertia::render('legal/Show', [
'seoTitle' => $fallback['title'].' - '.config('app.name', 'Fotospiel'),
'title' => $fallback['title'],
'content' => $this->convertMarkdownToHtml($fallback['markdown']),
'effectiveFrom' => null,
'effectiveFromLabel' => null,
'versionLabel' => null,
'slug' => $resolvedSlug,
]);
}
$locale = app()->getLocale();
$title = $page->title[$locale]
?? $page->title[$page->locale_fallback]
?? $page->title['de']
@@ -87,4 +110,56 @@ class LegalPageController extends Controller
return trim((string) $converter->convert($markdown));
}
private function loadFallbackDocument(string $slug, string $locale): ?array
{
$candidates = array_unique([
strtolower($locale),
strtolower(config('app.fallback_locale', 'de')),
'de',
'en',
]);
foreach ($candidates as $candidateLocale) {
$path = base_path("docs/legal/{$slug}-{$candidateLocale}.md");
if (! is_file($path)) {
continue;
}
$markdown = (string) file_get_contents($path);
$title = $this->extractTitleFromMarkdown($markdown) ?? Str::title($slug);
return [
'markdown' => $markdown,
'title' => $title,
];
}
return null;
}
private function extractTitleFromMarkdown(string $markdown): ?string
{
foreach (preg_split('/\r?\n/', $markdown) as $line) {
$trimmed = trim($line);
if ($trimmed === '') {
continue;
}
if (str_starts_with($trimmed, '# ')) {
return trim(substr($trimmed, 2));
}
if (str_starts_with($trimmed, '## ')) {
return trim(substr($trimmed, 3));
}
// First non-empty line can act as fallback title
return $trimmed;
}
return null;
}
}

View File

@@ -11,14 +11,31 @@ class LocaleController extends Controller
public function set(Request $request)
{
$locale = $request->input('locale');
$supportedLocales = ['de', 'en'];
$supportedLocales = array_values(array_unique(array_filter([
config('app.locale'),
config('app.fallback_locale'),
...array_filter(array_map(
static fn ($value) => trim((string) $value),
explode(',', (string) env('APP_SUPPORTED_LOCALES', ''))
)),
])));
if (empty($supportedLocales)) {
$supportedLocales = ['de', 'en'];
}
if (in_array($locale, $supportedLocales)) {
App::setLocale($locale);
Session::put('locale', $locale);
Session::put('preferred_locale', $locale);
}
if ($request->expectsJson()) {
return response()->json([
'locale' => App::getLocale(),
]);
}
// Return JSON response for fetch requests
return back();
}
}
}

View File

@@ -78,17 +78,33 @@ class MarketingController extends Controller
->with('success', __('marketing.contact.success', [], $locale));
}
public function contactView()
public function contactView(Request $request)
{
return Inertia::render('marketing.Kontakt');
$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, $packageId)
public function buyPackages(Request $request, string $locale, $packageId)
{
Log::info('Buy packages called', ['auth' => Auth::check(), 'package_id' => $packageId]);
Log::info('Buy packages called', ['auth' => Auth::check(), 'locale' => $locale, 'package_id' => $packageId]);
$package = Package::findOrFail($packageId);
if (! Auth::check()) {
@@ -138,7 +154,10 @@ class MarketingController extends Controller
if (! $package->paddle_price_id) {
Log::warning('Package missing Paddle price id', ['package_id' => $package->id]);
return redirect()->route('packages', ['highlight' => $package->slug])
return redirect()->route('packages', [
'locale' => app()->getLocale(),
'highlight' => $package->slug,
])
->with('error', __('marketing.packages.paddle_not_configured'));
}
@@ -149,8 +168,14 @@ class MarketingController extends Controller
$this->checkoutSessions->selectProvider($session, CheckoutSession::PROVIDER_PADDLE);
$checkout = $this->paddleCheckout->createCheckout($tenant, $package, [
'success_url' => route('marketing.success', ['packageId' => $package->id]),
'return_url' => route('packages', ['highlight' => $package->slug]),
'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,
],
@@ -314,22 +339,69 @@ class MarketingController extends Controller
]);
}
public function occasionsType($type)
public function occasionsType(Request $request, string $locale, string $type)
{
Log::info('OccasionsType hit', [
'type' => $type,
'locale' => app()->getLocale(),
'locale' => $locale,
'url' => request()->fullUrl(),
'route' => request()->route()->getName(),
'isInertia' => request()->header('X-Inertia'),
]);
$validTypes = ['hochzeit', 'geburtstag', 'firmenevent', 'konfirmation'];
if (! in_array($type, $validTypes)) {
$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');
}
return Inertia::render('marketing/Occasions', ['type' => $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,
]);
}
}

View File

@@ -67,4 +67,4 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'locale' => \App\Http\Middleware\SetLocale::class,
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
protected function redirectTo(Request $request): ?string
{
if ($request->expectsJson()) {
return null;
}
if ($request->routeIs('buy.packages') && $request->route('packageId')) {
return route('register', ['package_id' => $request->route('packageId')]);
}
return route('login');
}
}

View File

@@ -60,6 +60,7 @@ class HandleInertiaRequests extends Middleware
'user' => $request->user(),
],
'supportedLocales' => $supportedLocales,
'appUrl' => rtrim(config('app.url'), '/'),
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'locale' => app()->getLocale(),
'translations' => [

View File

@@ -29,6 +29,7 @@ class SetLocale
App::setLocale($sessionLocale);
Session::put('locale', $sessionLocale);
Session::put('preferred_locale', $sessionLocale);
return $next($request);
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Session;
class SetLocaleFromRequest
{
public function handle(Request $request, Closure $next)
{
$supportedLocales = $this->supportedLocales();
$locale = (string) $request->route('locale');
if (! $locale || ! in_array($locale, $supportedLocales, true)) {
$preferred = Session::get('preferred_locale');
if ($preferred && in_array($preferred, $supportedLocales, true)) {
App::setLocale($preferred);
Session::put('locale', $preferred);
$request->attributes->set('preferred_locale', $preferred);
}
return $next($request);
}
App::setLocale($locale);
Session::put('preferred_locale', $locale);
Session::put('locale', $locale);
$request->attributes->set('preferred_locale', $locale);
return $next($request);
}
/**
* @return array<int, string>
*/
private function supportedLocales(): array
{
$configured = array_filter(array_map(
static fn ($value) => trim((string) $value),
explode(',', (string) env('APP_SUPPORTED_LOCALES', ''))
));
if (empty($configured)) {
$configured = array_filter([
config('app.locale'),
config('app.fallback_locale'),
]);
}
if (empty($configured)) {
$configured = ['de', 'en'];
}
return array_values(array_unique($configured));
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Services\Paddle;
use App\Models\Package;
use App\Models\Tenant;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
class PaddleCheckoutService
@@ -21,8 +22,14 @@ class PaddleCheckoutService
{
$customerId = $this->customers->ensureCustomerId($tenant);
$successUrl = $options['success_url'] ?? route('marketing.success', ['packageId' => $package->id]);
$returnUrl = $options['return_url'] ?? route('packages', ['highlight' => $package->slug]);
$successUrl = $options['success_url'] ?? route('marketing.success', [
'locale' => App::getLocale(),
'packageId' => $package->id,
]);
$returnUrl = $options['return_url'] ?? route('packages', [
'locale' => App::getLocale(),
'highlight' => $package->slug,
]);
$metadata = $this->buildMetadata($tenant, $package, $options['metadata'] ?? []);