feat(bio): Link-in-Bio pages with translatable fields, public render

main
boban 2026-05-16 08:10:59 +02:00
parent d7ca84dd2e
commit e9623b9b31
6 changed files with 143 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace App\Domains\Bio\Actions;
use App\Domains\Bio\Models\BioPage;
use App\Models\User;
class CreateBioPage
{
public function handle(int $workspaceId, array $data, User $creator): BioPage
{
return BioPage::create([
'workspace_id' => $workspaceId,
'slug' => $data['slug'],
'title' => $data['title'] ?? [],
'description' => $data['description'] ?? [],
'theme' => $data['theme'] ?? [],
'is_published' => $data['is_published'] ?? false,
'domain_id' => $data['domain_id'] ?? null,
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Bio\Models;
use Illuminate\Database\Eloquent\Model;
class BioBlock extends Model
{
protected $guarded = ['id'];
protected $casts = [
'config' => 'array',
];
public function bioPage(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(BioPage::class);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Domains\Bio\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
use Spatie\Translatable\HasTranslations;
class BioPage extends Model
{
use SoftDeletes, HasFactory, HasTranslations;
protected $guarded = ['id'];
public array $translatable = ['title', 'description'];
protected $casts = [
'theme' => 'array',
'is_published' => 'boolean',
'ulid' => 'string',
];
protected static function booted(): void
{
static::creating(function (self $model) {
if (empty($model->ulid)) {
$model->ulid = (string) Str::ulid();
}
});
}
public function blocks(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(BioBlock::class)->orderBy('position');
}
public function workspace(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(\App\Domains\Workspace\Models\Workspace::class);
}
public function domain(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(\App\Domains\Domain\Models\Domain::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers;
use App\Domains\Bio\Models\BioPage;
use App\Domains\Domain\Models\Domain;
class BioPageController extends Controller
{
public function show(string $slug): \Illuminate\View\View|\Illuminate\Http\Response
{
$host = request()->getHost();
$domain = Domain::where('hostname', $host)->first();
$page = BioPage::where('slug', $slug)
->where('domain_id', $domain?->id)
->where('is_published', true)
->with('blocks')
->firstOrFail();
return view('bio.show', compact('page'));
}
}

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $page->getTranslation('title', app()->getLocale()) }}</title>
@vite(['resources/css/app.css'])
</head>
<body class="bg-gray-950 text-gray-100 min-h-screen flex flex-col items-center py-12">
<div class="w-full max-w-md">
<h1 class="text-2xl font-bold text-center mb-2">
{{ $page->getTranslation('title', app()->getLocale()) }}
</h1>
@if($page->getTranslation('description', app()->getLocale()))
<p class="text-gray-400 text-center mb-8">
{{ $page->getTranslation('description', app()->getLocale()) }}
</p>
@endif
<div class="space-y-3">
@foreach($page->blocks as $block)
<div class="bg-gray-900 rounded-xl p-4 border border-gray-800">
<div class="text-sm text-gray-300">{{ $block->config['content'] ?? '' }}</div>
</div>
@endforeach
</div>
</div>
</body>
</html>

View File

@ -30,6 +30,9 @@ Route::middleware(['auth', 'verified', \App\Http\Middleware\ResolveWorkspace::cl
Route::get('/billing', \App\Livewire\Pages\Billing\Index::class)->name('billing.index'); Route::get('/billing', \App\Livewire\Pages\Billing\Index::class)->name('billing.index');
}); });
// Bio page route — must be before the slug catch-all
Route::get('/bio/{slug}', [\App\Http\Controllers\BioPageController::class, 'show'])->name('bio.public');
// Redirect service — handles slugs on main domain and custom domains // Redirect service — handles slugs on main domain and custom domains
// Placed after all named routes so dashboard/profile etc. are not caught here // Placed after all named routes so dashboard/profile etc. are not caught here
Route::get('/{slug}', \App\Http\Controllers\RedirectController::class) Route::get('/{slug}', \App\Http\Controllers\RedirectController::class)