fix(modals): make create/edit/delete modals actually save + auto-refresh lists
- All create modals now validate→create→dispatch(kebab-case event)→toast→close - All edit/delete modals dispatch kebab-case events to Index classes - Index components listen via #[On] and call resetPage() to refresh - QR Code save() validates url field conditionally per type - Tests updated to assert kebab-case event names Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
c67bde8fc5
commit
2510620f67
|
|
@ -3,12 +3,17 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\Bio\Models\BioPage;
|
use App\Domains\Bio\Models\BioPage;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class CreateBioPage extends ModalComponent
|
class CreateBioPage extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public string $workspaceUlid = '';
|
||||||
|
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $title = '';
|
public string $title = '';
|
||||||
|
|
||||||
public string $slug = '';
|
public string $slug = '';
|
||||||
|
|
@ -21,24 +26,39 @@ class CreateBioPage extends ModalComponent
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mount(string $workspaceUlid = ''): void
|
||||||
|
{
|
||||||
|
if ($workspaceUlid) {
|
||||||
|
$workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail();
|
||||||
|
abort_unless(
|
||||||
|
$workspace->owner_id === auth()->id() ||
|
||||||
|
$workspace->members()->where('user_id', auth()->id())->exists(),
|
||||||
|
404
|
||||||
|
);
|
||||||
|
$this->workspaceUlid = $workspaceUlid;
|
||||||
|
$this->workspaceId = $workspace->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
$wsId = $this->workspaceId ?: require_workspace()->id;
|
||||||
|
|
||||||
if (empty($this->slug)) {
|
if (empty($this->slug)) {
|
||||||
$this->slug = Str::slug($this->title).'-'.Str::random(4);
|
$this->slug = Str::slug($this->title).'-'.Str::random(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
BioPage::create([
|
BioPage::create([
|
||||||
'workspace_id' => $workspace->id,
|
'workspace_id' => $wsId,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
'title' => ['en' => $this->title, 'de' => $this->title],
|
'title' => ['en' => $this->title, 'de' => $this->title],
|
||||||
'theme' => [],
|
'theme' => [],
|
||||||
'is_published' => false,
|
'is_published' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->dispatch('bioPageCreated');
|
$this->dispatch('bio-created')->to(\App\Livewire\Pages\Bio\Index::class);
|
||||||
$this->dispatch('toast', message: 'Bio Page erstellt', type: 'success');
|
$this->dispatch('toast', message: 'Bio Page erstellt', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,67 @@
|
||||||
|
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
|
use App\Domains\Ai\Services\SlugSuggester;
|
||||||
use App\Domains\Link\Models\Link;
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class CreateLink extends ModalComponent
|
class CreateLink extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public string $workspaceUlid = '';
|
||||||
|
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
|
#[Validate('required|url|max:2048')]
|
||||||
public string $targetUrl = '';
|
public string $targetUrl = '';
|
||||||
|
|
||||||
|
#[Validate('nullable|alpha_dash|min:3|max:64')]
|
||||||
public string $slug = '';
|
public string $slug = '';
|
||||||
|
|
||||||
|
#[Validate('nullable|string|max:200')]
|
||||||
public string $title = '';
|
public string $title = '';
|
||||||
|
|
||||||
protected $rules = [
|
public bool $autoSlug = true;
|
||||||
'targetUrl' => 'required|url|max:2048',
|
|
||||||
'slug' => 'nullable|alpha_dash|max:50|unique:links,slug',
|
/** @var string[] */
|
||||||
'title' => 'nullable|max:200',
|
public array $slugSuggestions = [];
|
||||||
];
|
|
||||||
|
public bool $aiLoading = false;
|
||||||
|
|
||||||
|
public function mount(string $workspaceUlid = ''): void
|
||||||
|
{
|
||||||
|
if ($workspaceUlid) {
|
||||||
|
$workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail();
|
||||||
|
abort_unless(
|
||||||
|
$workspace->owner_id === auth()->id() ||
|
||||||
|
$workspace->members()->where('user_id', auth()->id())->exists(),
|
||||||
|
404
|
||||||
|
);
|
||||||
|
$this->workspaceUlid = $workspaceUlid;
|
||||||
|
$this->workspaceId = $workspace->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedTargetUrl(string $value): void
|
||||||
|
{
|
||||||
|
if ($this->autoSlug && $value) {
|
||||||
|
$this->slug = Str::random(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSlug(): void
|
||||||
|
{
|
||||||
|
$this->autoSlug = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateSlug(): void
|
||||||
|
{
|
||||||
|
$this->slug = Str::random(6);
|
||||||
|
$this->autoSlug = true;
|
||||||
|
}
|
||||||
|
|
||||||
public function generateSlug(): void
|
public function generateSlug(): void
|
||||||
{
|
{
|
||||||
|
|
@ -28,24 +71,49 @@ class CreateLink extends ModalComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function suggestSlugs(): void
|
||||||
|
{
|
||||||
|
if (! $this->targetUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->aiLoading = true;
|
||||||
|
$this->slugSuggestions = app(SlugSuggester::class)->suggest($this->targetUrl);
|
||||||
|
$this->aiLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pickSuggestion(string $slug): void
|
||||||
|
{
|
||||||
|
$this->slug = $slug;
|
||||||
|
$this->autoSlug = false;
|
||||||
|
$this->slugSuggestions = [];
|
||||||
|
}
|
||||||
|
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
if (empty($this->slug)) {
|
$wsId = $this->workspaceId;
|
||||||
$this->slug = Str::random(6);
|
if (! $wsId) {
|
||||||
|
$ws = current_workspace();
|
||||||
|
if (! $ws) {
|
||||||
|
$this->addError('targetUrl', 'Workspace nicht gefunden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$wsId = $ws->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Link::create([
|
$slug = $this->slug ?: Str::random(6);
|
||||||
'workspace_id' => $workspace->id,
|
|
||||||
|
$link = Link::create([
|
||||||
|
'workspace_id' => $wsId,
|
||||||
'target_url' => $this->targetUrl,
|
'target_url' => $this->targetUrl,
|
||||||
'slug' => $this->slug,
|
'slug' => $slug,
|
||||||
'title' => $this->title ?: null,
|
'title' => $this->title ?: null,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
|
'created_by' => auth()->id(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->dispatch('linkCreated');
|
$this->dispatch('link-created', linkId: $link->ulid)->to(\App\Livewire\Pages\Links\Index::class);
|
||||||
$this->dispatch('toast', message: 'Link erstellt', type: 'success');
|
$this->dispatch('toast', message: 'Link erstellt', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,176 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\QrCode\Models\QrCode;
|
use App\Domains\QrCode\Models\QrCode;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class CreateQrCode extends ModalComponent
|
class CreateQrCode extends ModalComponent
|
||||||
{
|
{
|
||||||
public string $label = '';
|
public string $workspaceUlid = '';
|
||||||
|
|
||||||
public string $url = '';
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $type = 'url';
|
public string $type = 'url';
|
||||||
|
|
||||||
protected function rules(): array
|
public string $label = '';
|
||||||
|
|
||||||
|
// Generic payload (url, text, email, phone, sms, location)
|
||||||
|
public string $url = '';
|
||||||
|
|
||||||
|
// vCard
|
||||||
|
public string $vcardName = '';
|
||||||
|
public string $vcardPhone = '';
|
||||||
|
public string $vcardEmail = '';
|
||||||
|
public string $vcardCompany = '';
|
||||||
|
public string $vcardWebsite = '';
|
||||||
|
|
||||||
|
// WiFi
|
||||||
|
public string $wifiSsid = '';
|
||||||
|
public string $wifiPassword = '';
|
||||||
|
public string $wifiEncryption = 'WPA';
|
||||||
|
public bool $wifiHidden = false;
|
||||||
|
|
||||||
|
// Style
|
||||||
|
public string $fgColor = '#000000';
|
||||||
|
public string $bgColor = '#ffffff';
|
||||||
|
public string $logoUrl = '';
|
||||||
|
public string $dotStyle = 'square';
|
||||||
|
public int $size = 512;
|
||||||
|
public int $margin = 4;
|
||||||
|
public string $errorCorrection = 'M';
|
||||||
|
public string $frame = 'none';
|
||||||
|
public string $frameText = '';
|
||||||
|
public string $frameColor = '#7c3aed';
|
||||||
|
|
||||||
|
// Advanced
|
||||||
|
public bool $isDynamic = true;
|
||||||
|
public ?int $scanLimit = null;
|
||||||
|
public string $expiresAt = '';
|
||||||
|
|
||||||
|
public function mount(string $workspaceUlid = ''): void
|
||||||
{
|
{
|
||||||
return [
|
if ($workspaceUlid) {
|
||||||
'url' => 'required|url|max:2048',
|
$workspace = Workspace::where('ulid', $workspaceUlid)->firstOrFail();
|
||||||
'label' => 'nullable|max:200',
|
abort_unless(
|
||||||
'type' => 'required|in:url,vcard,wifi,pdf,mp3,text',
|
$workspace->owner_id === auth()->id() ||
|
||||||
];
|
$workspace->members()->where('user_id', auth()->id())->exists(),
|
||||||
|
404
|
||||||
|
);
|
||||||
|
$this->workspaceUlid = $workspaceUlid;
|
||||||
|
$this->workspaceId = $workspace->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated(string $prop): void
|
||||||
|
{
|
||||||
|
$this->dispatch('qr-preview-update', payload: $this->buildPayload(), options: [
|
||||||
|
'size' => $this->size,
|
||||||
|
'margin' => $this->margin,
|
||||||
|
'errorCorrection' => $this->errorCorrection,
|
||||||
|
'fgColor' => $this->fgColor,
|
||||||
|
'bgColor' => $this->bgColor,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildPayload(): string
|
||||||
|
{
|
||||||
|
return match ($this->type) {
|
||||||
|
'email' => 'mailto:' . $this->url,
|
||||||
|
'phone' => 'tel:' . $this->url,
|
||||||
|
'sms' => 'sms:' . $this->url,
|
||||||
|
'location' => 'geo:' . $this->url,
|
||||||
|
'vcard' => $this->buildVcard(),
|
||||||
|
'wifi' => $this->buildWifi(),
|
||||||
|
default => $this->url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildVcard(): string
|
||||||
|
{
|
||||||
|
return implode("\n", array_filter([
|
||||||
|
'BEGIN:VCARD',
|
||||||
|
'VERSION:3.0',
|
||||||
|
$this->vcardName ? "FN:{$this->vcardName}" : null,
|
||||||
|
$this->vcardPhone ? "TEL:{$this->vcardPhone}" : null,
|
||||||
|
$this->vcardEmail ? "EMAIL:{$this->vcardEmail}" : null,
|
||||||
|
$this->vcardCompany ? "ORG:{$this->vcardCompany}" : null,
|
||||||
|
$this->vcardWebsite ? "URL:{$this->vcardWebsite}" : null,
|
||||||
|
'END:VCARD',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildWifi(): string
|
||||||
|
{
|
||||||
|
$enc = $this->wifiEncryption ?: 'nopass';
|
||||||
|
$hidden = $this->wifiHidden ? 'true' : 'false';
|
||||||
|
|
||||||
|
return "WIFI:T:{$enc};S:{$this->wifiSsid};P:{$this->wifiPassword};H:{$hidden};;";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->validate();
|
$urlRule = match ($this->type) {
|
||||||
$workspace = app('current_workspace');
|
'url' => 'required|url|max:2048',
|
||||||
|
'email', 'phone', 'sms', 'location', 'text' => 'required|max:2048',
|
||||||
|
default => 'nullable',
|
||||||
|
};
|
||||||
|
|
||||||
QrCode::create([
|
$this->validate([
|
||||||
'workspace_id' => $workspace->id,
|
'type' => 'required|in:url,vcard,wifi,text,email,phone,sms,location,pdf,mp3',
|
||||||
'type' => $this->type,
|
'label' => 'nullable|max:200',
|
||||||
'payload' => ['url' => $this->url],
|
'url' => $urlRule,
|
||||||
'is_dynamic' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->dispatch('qrCreated');
|
$wsId = $this->workspaceId;
|
||||||
$this->dispatch('toast', message: 'QR Code erstellt', type: 'success');
|
if (! $wsId) {
|
||||||
|
$ws = current_workspace();
|
||||||
|
if (! $ws) {
|
||||||
|
$this->addError('url', 'Workspace nicht gefunden.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$wsId = $ws->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbType = match ($this->type) {
|
||||||
|
'email', 'phone', 'sms', 'location' => 'url',
|
||||||
|
default => $this->type,
|
||||||
|
};
|
||||||
|
|
||||||
|
QrCode::create([
|
||||||
|
'workspace_id' => $wsId,
|
||||||
|
'type' => $dbType,
|
||||||
|
'payload' => [
|
||||||
|
'data' => $this->buildPayload(),
|
||||||
|
'label' => $this->label,
|
||||||
|
'display_type' => $this->type,
|
||||||
|
],
|
||||||
|
'style' => [
|
||||||
|
'fg_color' => $this->fgColor,
|
||||||
|
'bg_color' => $this->bgColor,
|
||||||
|
'logo_url' => $this->logoUrl ?: null,
|
||||||
|
'dot_style' => $this->dotStyle,
|
||||||
|
'size' => $this->size,
|
||||||
|
'margin' => $this->margin,
|
||||||
|
'error_correction' => $this->errorCorrection,
|
||||||
|
'frame' => $this->frame,
|
||||||
|
'frame_text' => $this->frameText ?: null,
|
||||||
|
'frame_color' => $this->frameColor,
|
||||||
|
],
|
||||||
|
'is_dynamic' => $this->isDynamic,
|
||||||
|
'scan_limit' => $this->scanLimit ?: null,
|
||||||
|
'expires_at' => $this->expiresAt ?: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->dispatch('qr-created')->to(\App\Livewire\Pages\QrCodes\Index::class);
|
||||||
|
$this->dispatch('toast', message: 'QR-Code erstellt', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function modalMaxWidth(): string
|
public static function modalMaxWidth(): string
|
||||||
{
|
{
|
||||||
return 'md';
|
return '4xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,37 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\Bio\Models\BioPage;
|
use App\Domains\Bio\Models\BioPage;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class DeleteBioPage extends ModalComponent
|
class DeleteBioPage extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $bioUlid = '';
|
public string $bioUlid = '';
|
||||||
|
|
||||||
public string $bioTitle = '';
|
public string $bioTitle = '';
|
||||||
|
|
||||||
public function mount(string $bioUlid): void
|
public function mount(string $bioUlid): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$bio = BioPage::where('ulid', $bioUlid)->firstOrFail();
|
||||||
$bio = BioPage::where('ulid', $bioUlid)
|
|
||||||
->where('workspace_id', $workspace->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
|
$this->authorizeWorkspace($bio->workspace_id);
|
||||||
|
|
||||||
|
$this->workspaceId = $bio->workspace_id;
|
||||||
$this->bioUlid = $bioUlid;
|
$this->bioUlid = $bioUlid;
|
||||||
$this->bioTitle = $bio->getTranslation('title', 'en') ?? $bio->slug;
|
$this->bioTitle = $bio->getTranslation('title', 'en') ?? $bio->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(): void
|
public function delete(): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
BioPage::where('ulid', $this->bioUlid)
|
BioPage::where('ulid', $this->bioUlid)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $this->workspaceId)
|
||||||
->firstOrFail()
|
->firstOrFail()
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
$this->dispatch('bioPageDeleted', bioUlid: $this->bioUlid);
|
$this->dispatch('bio-deleted')->to(\App\Livewire\Pages\Bio\Index::class);
|
||||||
$this->dispatch('toast', message: 'Bio Page gelöscht', type: 'success');
|
$this->dispatch('toast', message: 'Bio Page gelöscht', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
@ -46,4 +47,13 @@ class DeleteBioPage extends ModalComponent
|
||||||
{
|
{
|
||||||
return view('livewire.modals.delete-bio-page');
|
return view('livewire.modals.delete-bio-page');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeWorkspace(int $workspaceId): void
|
||||||
|
{
|
||||||
|
Workspace::where("id", $workspaceId)
|
||||||
|
->where(fn ($q) => $q
|
||||||
|
->where("owner_id", auth()->id())
|
||||||
|
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
|
||||||
|
)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,37 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\Link\Models\Link;
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class DeleteLink extends ModalComponent
|
class DeleteLink extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $linkUlid = '';
|
public string $linkUlid = '';
|
||||||
|
|
||||||
public string $linkTitle = '';
|
public string $linkTitle = '';
|
||||||
|
|
||||||
public function mount(string $linkUlid): void
|
public function mount(string $linkUlid): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$link = Link::where('ulid', $linkUlid)->firstOrFail();
|
||||||
$link = Link::where('ulid', $linkUlid)
|
|
||||||
->where('workspace_id', $workspace->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
|
$this->authorizeWorkspace($link->workspace_id);
|
||||||
|
|
||||||
|
$this->workspaceId = $link->workspace_id;
|
||||||
$this->linkUlid = $linkUlid;
|
$this->linkUlid = $linkUlid;
|
||||||
$this->linkTitle = $link->title ?? $link->slug;
|
$this->linkTitle = $link->title ?? $link->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(): void
|
public function delete(): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
Link::where('ulid', $this->linkUlid)
|
Link::where('ulid', $this->linkUlid)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $this->workspaceId)
|
||||||
->firstOrFail()
|
->firstOrFail()
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
$this->dispatch('linkDeleted', linkUlid: $this->linkUlid);
|
$this->dispatch('link-deleted', linkUlid: $this->linkUlid)->to(\App\Livewire\Pages\Links\Index::class);
|
||||||
$this->dispatch('toast', message: 'Link gelöscht', type: 'success');
|
$this->dispatch('toast', message: 'Link gelöscht', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
@ -46,4 +47,13 @@ class DeleteLink extends ModalComponent
|
||||||
{
|
{
|
||||||
return view('livewire.modals.delete-link');
|
return view('livewire.modals.delete-link');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeWorkspace(int $workspaceId): void
|
||||||
|
{
|
||||||
|
Workspace::where("id", $workspaceId)
|
||||||
|
->where(fn ($q) => $q
|
||||||
|
->where("owner_id", auth()->id())
|
||||||
|
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
|
||||||
|
)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,37 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\QrCode\Models\QrCode;
|
use App\Domains\QrCode\Models\QrCode;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class DeleteQrCode extends ModalComponent
|
class DeleteQrCode extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $qrUlid = '';
|
public string $qrUlid = '';
|
||||||
|
|
||||||
public string $qrLabel = '';
|
public string $qrLabel = '';
|
||||||
|
|
||||||
public function mount(string $qrUlid): void
|
public function mount(string $qrUlid): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$qr = QrCode::where('ulid', $qrUlid)->firstOrFail();
|
||||||
$qr = QrCode::where('ulid', $qrUlid)
|
|
||||||
->where('workspace_id', $workspace->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
|
$this->authorizeWorkspace($qr->workspace_id);
|
||||||
|
|
||||||
|
$this->workspaceId = $qr->workspace_id;
|
||||||
$this->qrUlid = $qrUlid;
|
$this->qrUlid = $qrUlid;
|
||||||
$this->qrLabel = $qr->label ?? $qr->ulid;
|
$this->qrLabel = $qr->label ?? $qr->ulid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(): void
|
public function delete(): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
QrCode::where('ulid', $this->qrUlid)
|
QrCode::where('ulid', $this->qrUlid)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $this->workspaceId)
|
||||||
->firstOrFail()
|
->firstOrFail()
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
$this->dispatch('qrDeleted', qrUlid: $this->qrUlid);
|
$this->dispatch('qr-deleted')->to(\App\Livewire\Pages\QrCodes\Index::class);
|
||||||
$this->dispatch('toast', message: 'QR Code gelöscht', type: 'success');
|
$this->dispatch('toast', message: 'QR Code gelöscht', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
@ -46,4 +47,13 @@ class DeleteQrCode extends ModalComponent
|
||||||
{
|
{
|
||||||
return view('livewire.modals.delete-qr-code');
|
return view('livewire.modals.delete-qr-code');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeWorkspace(int $workspaceId): void
|
||||||
|
{
|
||||||
|
Workspace::where("id", $workspaceId)
|
||||||
|
->where(fn ($q) => $q
|
||||||
|
->where("owner_id", auth()->id())
|
||||||
|
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
|
||||||
|
)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\Bio\Models\BioPage;
|
use App\Domains\Bio\Models\BioPage;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class EditBioPage extends ModalComponent
|
class EditBioPage extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $bioUlid = '';
|
public string $bioUlid = '';
|
||||||
|
|
||||||
public string $title = '';
|
public string $title = '';
|
||||||
|
|
@ -18,11 +21,11 @@ class EditBioPage extends ModalComponent
|
||||||
|
|
||||||
public function mount(string $bioUlid): void
|
public function mount(string $bioUlid): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$bio = BioPage::where('ulid', $bioUlid)->firstOrFail();
|
||||||
$bio = BioPage::where('ulid', $bioUlid)
|
|
||||||
->where('workspace_id', $workspace->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
|
$this->authorizeWorkspace($bio->workspace_id);
|
||||||
|
|
||||||
|
$this->workspaceId = $bio->workspace_id;
|
||||||
$this->bioUlid = $bioUlid;
|
$this->bioUlid = $bioUlid;
|
||||||
$this->title = $bio->getTranslation('title', 'en') ?? '';
|
$this->title = $bio->getTranslation('title', 'en') ?? '';
|
||||||
$this->slug = $bio->slug;
|
$this->slug = $bio->slug;
|
||||||
|
|
@ -41,10 +44,9 @@ class EditBioPage extends ModalComponent
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
$bio = BioPage::where('ulid', $this->bioUlid)
|
$bio = BioPage::where('ulid', $this->bioUlid)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $this->workspaceId)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$bio->update([
|
$bio->update([
|
||||||
|
|
@ -53,7 +55,7 @@ class EditBioPage extends ModalComponent
|
||||||
'is_published' => $this->isPublished,
|
'is_published' => $this->isPublished,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->dispatch('bioPageUpdated', bioUlid: $this->bioUlid);
|
$this->dispatch('bio-updated')->to(\App\Livewire\Pages\Bio\Index::class);
|
||||||
$this->dispatch('toast', message: 'Bio Page aktualisiert', type: 'success');
|
$this->dispatch('toast', message: 'Bio Page aktualisiert', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
@ -67,4 +69,13 @@ class EditBioPage extends ModalComponent
|
||||||
{
|
{
|
||||||
return view('livewire.modals.edit-bio-page');
|
return view('livewire.modals.edit-bio-page');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeWorkspace(int $workspaceId): void
|
||||||
|
{
|
||||||
|
Workspace::where("id", $workspaceId)
|
||||||
|
->where(fn ($q) => $q
|
||||||
|
->where("owner_id", auth()->id())
|
||||||
|
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
|
||||||
|
)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\Link\Models\Link;
|
use App\Domains\Link\Models\Link;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class EditLink extends ModalComponent
|
class EditLink extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $linkUlid = '';
|
public string $linkUlid = '';
|
||||||
|
|
||||||
public int $linkId = 0;
|
public int $linkId = 0;
|
||||||
|
|
@ -22,11 +25,11 @@ class EditLink extends ModalComponent
|
||||||
|
|
||||||
public function mount(string $linkUlid): void
|
public function mount(string $linkUlid): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$link = Link::where('ulid', $linkUlid)->firstOrFail();
|
||||||
$link = Link::where('ulid', $linkUlid)
|
|
||||||
->where('workspace_id', $workspace->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
|
$this->authorizeWorkspace($link->workspace_id);
|
||||||
|
|
||||||
|
$this->workspaceId = $link->workspace_id;
|
||||||
$this->linkUlid = $linkUlid;
|
$this->linkUlid = $linkUlid;
|
||||||
$this->linkId = $link->id;
|
$this->linkId = $link->id;
|
||||||
$this->targetUrl = $link->target_url;
|
$this->targetUrl = $link->target_url;
|
||||||
|
|
@ -48,10 +51,9 @@ class EditLink extends ModalComponent
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
$link = Link::where('ulid', $this->linkUlid)
|
$link = Link::where('ulid', $this->linkUlid)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $this->workspaceId)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$link->update([
|
$link->update([
|
||||||
|
|
@ -61,7 +63,7 @@ class EditLink extends ModalComponent
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->dispatch('linkUpdated', linkUlid: $this->linkUlid);
|
$this->dispatch('link-updated', linkUlid: $this->linkUlid)->to(\App\Livewire\Pages\Links\Index::class);
|
||||||
$this->dispatch('toast', message: 'Link aktualisiert', type: 'success');
|
$this->dispatch('toast', message: 'Link aktualisiert', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
@ -70,4 +72,13 @@ class EditLink extends ModalComponent
|
||||||
{
|
{
|
||||||
return view('livewire.modals.edit-link');
|
return view('livewire.modals.edit-link');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeWorkspace(int $workspaceId): void
|
||||||
|
{
|
||||||
|
Workspace::where("id", $workspaceId)
|
||||||
|
->where(fn ($q) => $q
|
||||||
|
->where("owner_id", auth()->id())
|
||||||
|
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
|
||||||
|
)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@
|
||||||
namespace App\Livewire\Modals;
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
use App\Domains\QrCode\Models\QrCode;
|
use App\Domains\QrCode\Models\QrCode;
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
class EditQrCode extends ModalComponent
|
class EditQrCode extends ModalComponent
|
||||||
{
|
{
|
||||||
|
public int $workspaceId = 0;
|
||||||
|
|
||||||
public string $qrUlid = '';
|
public string $qrUlid = '';
|
||||||
|
|
||||||
public string $url = '';
|
public string $url = '';
|
||||||
|
|
@ -18,11 +21,11 @@ class EditQrCode extends ModalComponent
|
||||||
|
|
||||||
public function mount(string $qrUlid): void
|
public function mount(string $qrUlid): void
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$qr = QrCode::where('ulid', $qrUlid)->firstOrFail();
|
||||||
$qr = QrCode::where('ulid', $qrUlid)
|
|
||||||
->where('workspace_id', $workspace->id)
|
|
||||||
->firstOrFail();
|
|
||||||
|
|
||||||
|
$this->authorizeWorkspace($qr->workspace_id);
|
||||||
|
|
||||||
|
$this->workspaceId = $qr->workspace_id;
|
||||||
$this->qrUlid = $qrUlid;
|
$this->qrUlid = $qrUlid;
|
||||||
$this->url = $qr->payload['url'] ?? '';
|
$this->url = $qr->payload['url'] ?? '';
|
||||||
$this->label = $qr->label ?? '';
|
$this->label = $qr->label ?? '';
|
||||||
|
|
@ -41,10 +44,9 @@ class EditQrCode extends ModalComponent
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$workspace = app('current_workspace');
|
|
||||||
|
|
||||||
$qr = QrCode::where('ulid', $this->qrUlid)
|
$qr = QrCode::where('ulid', $this->qrUlid)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $this->workspaceId)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$qr->update([
|
$qr->update([
|
||||||
|
|
@ -52,7 +54,7 @@ class EditQrCode extends ModalComponent
|
||||||
'payload' => ['url' => $this->url],
|
'payload' => ['url' => $this->url],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->dispatch('qrUpdated', qrUlid: $this->qrUlid);
|
$this->dispatch('qr-updated')->to(\App\Livewire\Pages\QrCodes\Index::class);
|
||||||
$this->dispatch('toast', message: 'QR Code aktualisiert', type: 'success');
|
$this->dispatch('toast', message: 'QR Code aktualisiert', type: 'success');
|
||||||
$this->closeModal();
|
$this->closeModal();
|
||||||
}
|
}
|
||||||
|
|
@ -66,4 +68,13 @@ class EditQrCode extends ModalComponent
|
||||||
{
|
{
|
||||||
return view('livewire.modals.edit-qr-code');
|
return view('livewire.modals.edit-qr-code');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeWorkspace(int $workspaceId): void
|
||||||
|
{
|
||||||
|
Workspace::where("id", $workspaceId)
|
||||||
|
->where(fn ($q) => $q
|
||||||
|
->where("owner_id", auth()->id())
|
||||||
|
->orWhereHas("members", fn ($q) => $q->where("user_id", auth()->id()))
|
||||||
|
)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,41 @@
|
||||||
|
|
||||||
namespace App\Livewire\Pages\Bio;
|
namespace App\Livewire\Pages\Bio;
|
||||||
|
|
||||||
|
use App\Domains\Bio\Models\BioPage;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
class Index extends Component
|
class Index extends Component
|
||||||
{
|
{
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
public function updatingSearch(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('bio-created')]
|
||||||
|
#[On('bio-updated')]
|
||||||
|
#[On('bio-deleted')]
|
||||||
|
public function refresh(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.pages.bio.index')
|
$workspace = require_workspace();
|
||||||
|
|
||||||
|
$bioPages = BioPage::where('workspace_id', $workspace->id)
|
||||||
|
->when($this->search, fn ($q) => $q->where('slug', 'like', "%{$this->search}%"))
|
||||||
|
->latest()
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('livewire.pages.bio.index', compact('bioPages'))
|
||||||
->layout('layouts.nimuli-app', ['title' => 'Link-in-Bio']);
|
->layout('layouts.nimuli-app', ['title' => 'Link-in-Bio']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ class Index extends Component
|
||||||
$this->resetPage();
|
$this->resetPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[On('linkCreated')]
|
#[On('link-created')]
|
||||||
#[On('linkUpdated')]
|
#[On('link-updated')]
|
||||||
#[On('linkDeleted')]
|
#[On('link-deleted')]
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
$this->resetPage();
|
$this->resetPage();
|
||||||
|
|
@ -36,7 +36,7 @@ class Index extends Component
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$workspace = require_workspace();
|
||||||
|
|
||||||
$links = Link::where('workspace_id', $workspace->id)
|
$links = Link::where('workspace_id', $workspace->id)
|
||||||
->when($this->search, fn ($q) => $q->where(function ($q) {
|
->when($this->search, fn ($q) => $q->where(function ($q) {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,41 @@
|
||||||
|
|
||||||
namespace App\Livewire\Pages\QrCodes;
|
namespace App\Livewire\Pages\QrCodes;
|
||||||
|
|
||||||
|
use App\Domains\QrCode\Models\QrCode;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
class Index extends Component
|
class Index extends Component
|
||||||
{
|
{
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
public function updatingSearch(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('qr-created')]
|
||||||
|
#[On('qr-updated')]
|
||||||
|
#[On('qr-deleted')]
|
||||||
|
public function refresh(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.pages.qr-codes.index')
|
$workspace = require_workspace();
|
||||||
|
|
||||||
|
$qrCodes = QrCode::where('workspace_id', $workspace->id)
|
||||||
|
->when($this->search, fn ($q) => $q->where('label', 'like', "%{$this->search}%"))
|
||||||
|
->latest()
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('livewire.pages.qr-codes.index', compact('qrCodes'))
|
||||||
->layout('layouts.nimuli-app', ['title' => 'QR Codes']);
|
->layout('layouts.nimuli-app', ['title' => 'QR Codes']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,42 @@
|
||||||
<div>
|
<div>
|
||||||
|
@php $ws = current_workspace(); @endphp
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">Link-in-Bio</h1>
|
<h1 class="text-2xl font-semibold text-t1">Link-in-Bio</h1>
|
||||||
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.create-bio-page', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
|
||||||
|
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
||||||
+ New Bio Page
|
+ New Bio Page
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center">
|
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
|
||||||
|
@forelse($bioPages as $page)
|
||||||
|
<div class="flex items-center gap-4 px-5 py-4 border-b border-white/[.04] group hover:bg-white/[.02] transition-colors">
|
||||||
|
<div class="w-10 h-10 bg-s2 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg class="w-5 h-5 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-medium text-t1">{{ is_array($page->title) ? ($page->title['en'] ?? $page->slug) : ($page->title ?? $page->slug) }}</div>
|
||||||
|
<div class="text-xs text-t2 font-mono">nimu.li/{{ $page->slug }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded-full {{ $page->is_published ? 'bg-green/10 text-green' : 'bg-white/[.06] text-t3' }}">
|
||||||
|
{{ $page->is_published ? 'Published' : 'Draft' }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.edit-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
|
||||||
|
class="text-t2 text-xs hover:text-t1">Edit</button>
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.delete-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
|
||||||
|
class="text-red text-xs hover:opacity-80">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="p-12 text-center">
|
||||||
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
|
||||||
|
|
@ -14,8 +44,18 @@
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-t1 mb-2">No bio pages yet</h3>
|
<h3 class="text-lg font-medium text-t1 mb-2">No bio pages yet</h3>
|
||||||
<p class="text-sm text-t2 mb-6">Create a beautiful link-in-bio page for Instagram, TikTok and more.</p>
|
<p class="text-sm text-t2 mb-6">Create a beautiful link-in-bio page for Instagram, TikTok and more.</p>
|
||||||
<button class="px-4 py-2.5 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.create-bio-page', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
|
||||||
|
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
||||||
Create your first Bio Page
|
Create your first Bio Page
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($bioPages->hasPages())
|
||||||
|
<div class="mt-4">{{ $bioPages->links() }}</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
<div>
|
<div>
|
||||||
|
@php $ws = current_workspace(); @endphp
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-bold text-t1">Links</h1>
|
<h1 class="text-2xl font-bold text-t1">Links</h1>
|
||||||
<button @click="$dispatch('openModal', {component: 'modals.create-link'})"
|
@if($ws)
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="$dispatch('openModal', {component: 'modals.utm-builder'})"
|
||||||
|
class="px-3 py-2 bg-s2 border border-white/[.06] text-t1 rounded-lg text-sm font-medium hover:bg-s3 transition-colors flex items-center gap-1.5">
|
||||||
|
<x-heroicon-o-beaker class="w-4 h-4" />
|
||||||
|
UTM Builder
|
||||||
|
</button>
|
||||||
|
<button @click="$dispatch('openModal', {component: 'modals.create-link', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
|
||||||
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
||||||
+ New Link
|
+ New Link
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Filter Bar --}}
|
{{-- Filter Bar --}}
|
||||||
<div class="flex gap-3 mb-4">
|
<div class="flex gap-3 mb-4">
|
||||||
|
|
@ -87,6 +97,12 @@
|
||||||
{{-- Actions --}}
|
{{-- Actions --}}
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.link-variants', arguments: {linkUlid: '{{ $link->ulid }}'}})"
|
||||||
|
class="text-t2 text-xs hover:text-purple transition-colors flex items-center gap-1">
|
||||||
|
<x-heroicon-o-chart-bar class="w-3 h-3" />
|
||||||
|
A/B
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="$dispatch('openModal', {component: 'modals.edit-link', arguments: {linkUlid: '{{ $link->ulid }}'}})"
|
@click="$dispatch('openModal', {component: 'modals.edit-link', arguments: {linkUlid: '{{ $link->ulid }}'}})"
|
||||||
class="text-t2 text-xs hover:text-t1 transition-colors">
|
class="text-t2 text-xs hover:text-t1 transition-colors">
|
||||||
|
|
@ -108,8 +124,10 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-t3 text-sm">No links yet.</p>
|
<p class="text-t3 text-sm">No links yet.</p>
|
||||||
<button @click="$dispatch('openModal', {component: 'modals.create-link'})"
|
@if($ws)
|
||||||
|
<button @click="$dispatch('openModal', {component: 'modals.create-link', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
|
||||||
class="text-blue text-sm hover:underline">Create your first short link</button>
|
class="text-blue text-sm hover:underline">Create your first short link</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,61 @@
|
||||||
<div>
|
<div>
|
||||||
|
@php $ws = current_workspace(); @endphp
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">QR Codes</h1>
|
<h1 class="text-2xl font-semibold text-t1">QR Codes</h1>
|
||||||
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.create-qr-code', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
|
||||||
|
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
||||||
+ Create QR Code
|
+ Create QR Code
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center">
|
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
|
||||||
|
@forelse($qrCodes as $qr)
|
||||||
|
<div class="flex items-center gap-4 px-5 py-4 border-b border-white/[.04] group hover:bg-white/[.02] transition-colors">
|
||||||
|
<div class="w-10 h-10 bg-s2 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<svg class="w-5 h-5 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm font-medium text-t1">{{ $qr->label ?: $qr->ulid }}</div>
|
||||||
|
<div class="text-xs text-t2 truncate">{{ $qr->type }} · {{ $qr->payload['url'] ?? '' }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded-full {{ $qr->is_dynamic ? 'bg-blue/10 text-blue' : 'bg-white/[.06] text-t3' }}">
|
||||||
|
{{ $qr->is_dynamic ? 'Dynamic' : 'Static' }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.edit-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
|
||||||
|
class="text-t2 text-xs hover:text-t1">Edit</button>
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.delete-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
|
||||||
|
class="text-red text-xs hover:opacity-80">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="p-12 text-center">
|
||||||
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-t1 mb-2">No QR Codes yet</h3>
|
<h3 class="text-lg font-medium text-t1 mb-2">No QR Codes yet</h3>
|
||||||
<p class="text-sm text-t2 mb-6">Create QR codes for URLs, vCards, WiFi and more.</p>
|
<p class="text-sm text-t2 mb-6">Create QR codes for URLs, vCards, WiFi and more.</p>
|
||||||
<button class="px-4 py-2.5 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.create-qr-code', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
|
||||||
|
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
||||||
Create your first QR Code
|
Create your first QR Code
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($qrCodes->hasPages())
|
||||||
|
<div class="mt-4">{{ $qrCodes->links() }}</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ it('creates bio page and dispatches event', function () {
|
||||||
Livewire::actingAs($user)->test(CreateBioPage::class)
|
Livewire::actingAs($user)->test(CreateBioPage::class)
|
||||||
->set('title', 'My Bio Page')
|
->set('title', 'My Bio Page')
|
||||||
->call('save')
|
->call('save')
|
||||||
->assertDispatched('bioPageCreated')
|
->assertDispatched('bio-created')
|
||||||
->assertDispatched('toast');
|
->assertDispatched('toast');
|
||||||
|
|
||||||
$this->assertDatabaseHas('bio_pages', [
|
$this->assertDatabaseHas('bio_pages', [
|
||||||
|
|
@ -77,7 +77,7 @@ it('updates bio page and dispatches event', function () {
|
||||||
Livewire::actingAs($user)->test(EditBioPage::class, ['bioUlid' => $bio->ulid])
|
Livewire::actingAs($user)->test(EditBioPage::class, ['bioUlid' => $bio->ulid])
|
||||||
->set('title', 'Updated Title')
|
->set('title', 'Updated Title')
|
||||||
->call('save')
|
->call('save')
|
||||||
->assertDispatched('bioPageUpdated')
|
->assertDispatched('bio-updated')
|
||||||
->assertDispatched('toast');
|
->assertDispatched('toast');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ it('deletes bio page and dispatches event', function () {
|
||||||
|
|
||||||
Livewire::actingAs($user)->test(DeleteBioPage::class, ['bioUlid' => $bio->ulid])
|
Livewire::actingAs($user)->test(DeleteBioPage::class, ['bioUlid' => $bio->ulid])
|
||||||
->call('delete')
|
->call('delete')
|
||||||
->assertDispatched('bioPageDeleted')
|
->assertDispatched('bio-deleted')
|
||||||
->assertDispatched('toast');
|
->assertDispatched('toast');
|
||||||
|
|
||||||
$this->assertSoftDeleted('bio_pages', ['id' => $bio->id]);
|
$this->assertSoftDeleted('bio_pages', ['id' => $bio->id]);
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ it('creates link and dispatches linkCreated', function () {
|
||||||
Livewire::actingAs($user)->test(CreateLink::class)
|
Livewire::actingAs($user)->test(CreateLink::class)
|
||||||
->set('targetUrl', 'https://example.com/page')
|
->set('targetUrl', 'https://example.com/page')
|
||||||
->call('save')
|
->call('save')
|
||||||
->assertDispatched('linkCreated');
|
->assertDispatched('link-created');
|
||||||
|
|
||||||
expect(Link::where('workspace_id', $ws->id)->where('target_url', 'https://example.com/page')->exists())->toBeTrue();
|
expect(Link::where('workspace_id', $ws->id)->where('target_url', 'https://example.com/page')->exists())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
@ -109,7 +109,7 @@ it('updates link on save', function () {
|
||||||
->set('targetUrl', 'https://new.example.com')
|
->set('targetUrl', 'https://new.example.com')
|
||||||
->set('slug', 'newslug')
|
->set('slug', 'newslug')
|
||||||
->call('save')
|
->call('save')
|
||||||
->assertDispatched('linkUpdated');
|
->assertDispatched('link-updated');
|
||||||
|
|
||||||
expect($link->fresh()->target_url)->toBe('https://new.example.com');
|
expect($link->fresh()->target_url)->toBe('https://new.example.com');
|
||||||
expect($link->fresh()->slug)->toBe('newslug');
|
expect($link->fresh()->slug)->toBe('newslug');
|
||||||
|
|
@ -151,7 +151,7 @@ it('soft-deletes link and dispatches linkDeleted', function () {
|
||||||
|
|
||||||
Livewire::actingAs($user)->test(DeleteLink::class, ['linkUlid' => $link->ulid])
|
Livewire::actingAs($user)->test(DeleteLink::class, ['linkUlid' => $link->ulid])
|
||||||
->call('delete')
|
->call('delete')
|
||||||
->assertDispatched('linkDeleted');
|
->assertDispatched('link-deleted');
|
||||||
|
|
||||||
expect(Link::withTrashed()->find($link->id)->deleted_at)->not->toBeNull();
|
expect(Link::withTrashed()->find($link->id)->deleted_at)->not->toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ it('creates qr code and dispatches event', function () {
|
||||||
->set('url', 'https://example.com')
|
->set('url', 'https://example.com')
|
||||||
->set('label', 'My QR')
|
->set('label', 'My QR')
|
||||||
->call('save')
|
->call('save')
|
||||||
->assertDispatched('qrCreated')
|
->assertDispatched('qr-created')
|
||||||
->assertDispatched('toast');
|
->assertDispatched('toast');
|
||||||
|
|
||||||
$this->assertDatabaseHas('qr_codes', [
|
$this->assertDatabaseHas('qr_codes', [
|
||||||
|
|
@ -90,7 +90,7 @@ it('updates qr code and dispatches event', function () {
|
||||||
Livewire::actingAs($user)->test(EditQrCode::class, ['qrUlid' => $qr->ulid])
|
Livewire::actingAs($user)->test(EditQrCode::class, ['qrUlid' => $qr->ulid])
|
||||||
->set('url', 'https://new.com')
|
->set('url', 'https://new.com')
|
||||||
->call('save')
|
->call('save')
|
||||||
->assertDispatched('qrUpdated')
|
->assertDispatched('qr-updated')
|
||||||
->assertDispatched('toast');
|
->assertDispatched('toast');
|
||||||
|
|
||||||
expect($qr->fresh()->payload['url'])->toBe('https://new.com');
|
expect($qr->fresh()->payload['url'])->toBe('https://new.com');
|
||||||
|
|
@ -114,7 +114,7 @@ it('deletes qr code and dispatches event', function () {
|
||||||
|
|
||||||
Livewire::actingAs($user)->test(DeleteQrCode::class, ['qrUlid' => $qr->ulid])
|
Livewire::actingAs($user)->test(DeleteQrCode::class, ['qrUlid' => $qr->ulid])
|
||||||
->call('delete')
|
->call('delete')
|
||||||
->assertDispatched('qrDeleted')
|
->assertDispatched('qr-deleted')
|
||||||
->assertDispatched('toast');
|
->assertDispatched('toast');
|
||||||
|
|
||||||
$this->assertSoftDeleted('qr_codes', ['id' => $qr->id]);
|
$this->assertSoftDeleted('qr_codes', ['id' => $qr->id]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue