fix(FIX-10): 6 bugs — route ULID, QR modal, bio unique, nav active, copy btn, analytics context
- Workspace.getRouteKeyName()='ulid' so route() generates ULID URLs not ID
- QR modal 4xl→5xl; import QRCode in @script→window.QRCode (no ESM in @script)
- Bio slug unique rule scoped to (workspace_id, domain_id) not global
- updateActiveNav uses URL.pathname not raw getAttribute('href') (absolute URL fix)
- Add resources/views/components/ui/copy-button.blade.php Alpine component
- Analytics Index.render/baseQuery use $workspaceId not require_workspace() — prevents RuntimeException on Livewire action calls without middleware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
parent
2d231fe6b8
commit
43ff6d6407
|
|
@ -43,6 +43,11 @@ class Workspace extends Model
|
||||||
'trial_ends_at' => 'datetime',
|
'trial_ends_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getRouteKeyName(): string
|
||||||
|
{
|
||||||
|
return 'ulid';
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<User, $this> */
|
/** @return BelongsTo<User, $this> */
|
||||||
public function owner(): BelongsTo
|
public function owner(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Domains\Bio\Models\BioPage;
|
||||||
use App\Domains\Workspace\Models\Workspace;
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use App\Livewire\Pages\Bio\Index;
|
use App\Livewire\Pages\Bio\Index;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
|
|
@ -19,12 +20,24 @@ class CreateBioPage extends ModalComponent
|
||||||
|
|
||||||
public string $slug = '';
|
public string $slug = '';
|
||||||
|
|
||||||
/** @return array<string, string> */
|
/** @return array<string, mixed> */
|
||||||
protected function rules(): array
|
protected function rules(): array
|
||||||
{
|
{
|
||||||
|
$wsId = $this->workspaceId;
|
||||||
|
if (! $wsId) {
|
||||||
|
$ws = current_workspace();
|
||||||
|
$wsId = $ws !== null ? $ws->id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => 'required|max:200',
|
'title' => 'required|max:200',
|
||||||
'slug' => 'nullable|alpha_dash|max:80|unique:bio_pages,slug',
|
'slug' => [
|
||||||
|
'nullable', 'alpha_dash', 'max:80',
|
||||||
|
Rule::unique('bio_pages', 'slug')
|
||||||
|
->where('workspace_id', $wsId)
|
||||||
|
->whereNull('domain_id')
|
||||||
|
->whereNull('deleted_at'),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ class CreateQrCode extends ModalComponent
|
||||||
|
|
||||||
public static function modalMaxWidth(): string
|
public static function modalMaxWidth(): string
|
||||||
{
|
{
|
||||||
return '4xl';
|
return '5xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,9 @@ class Index extends Component
|
||||||
/** @return Builder<Click> */
|
/** @return Builder<Click> */
|
||||||
private function baseQuery(): Builder
|
private function baseQuery(): Builder
|
||||||
{
|
{
|
||||||
$workspace = require_workspace();
|
$wsId = $this->workspaceId;
|
||||||
|
|
||||||
$q = Click::where('workspace_id', $workspace->id)
|
$q = Click::where('workspace_id', $wsId)
|
||||||
->where('clicked_at', '>=', $this->rangeStart());
|
->where('clicked_at', '>=', $this->rangeStart());
|
||||||
|
|
||||||
if ($this->country) {
|
if ($this->country) {
|
||||||
|
|
@ -71,7 +71,7 @@ class Index extends Component
|
||||||
$q->where('device', $this->device);
|
$q->where('device', $this->device);
|
||||||
}
|
}
|
||||||
if ($this->linkId) {
|
if ($this->linkId) {
|
||||||
$link = Link::where('workspace_id', $workspace->id)->where('ulid', $this->linkId)->first();
|
$link = Link::where('workspace_id', $wsId)->where('ulid', $this->linkId)->first();
|
||||||
if ($link) {
|
if ($link) {
|
||||||
$q->where('link_id', $link->id);
|
$q->where('link_id', $link->id);
|
||||||
}
|
}
|
||||||
|
|
@ -91,11 +91,11 @@ class Index extends Component
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
$workspace = require_workspace();
|
$wsId = $this->workspaceId;
|
||||||
|
|
||||||
$totalFiltered = $this->baseQuery()->count();
|
$totalFiltered = $this->baseQuery()->count();
|
||||||
|
|
||||||
$allTime = Click::where('workspace_id', $workspace->id)->count();
|
$allTime = Click::where('workspace_id', $wsId)->count();
|
||||||
|
|
||||||
$byCountry = (clone $this->baseQuery())
|
$byCountry = (clone $this->baseQuery())
|
||||||
->whereNotNull('country')
|
->whereNotNull('country')
|
||||||
|
|
@ -111,20 +111,20 @@ class Index extends Component
|
||||||
->orderByDesc('total')
|
->orderByDesc('total')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$countries = Click::where('workspace_id', $workspace->id)
|
$countries = Click::where('workspace_id', $wsId)
|
||||||
->whereNotNull('country')
|
->whereNotNull('country')
|
||||||
->distinct()
|
->distinct()
|
||||||
->pluck('country')
|
->pluck('country')
|
||||||
->sort()
|
->sort()
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
$devices = Click::where('workspace_id', $workspace->id)
|
$devices = Click::where('workspace_id', $wsId)
|
||||||
->whereNotNull('device')
|
->whereNotNull('device')
|
||||||
->distinct()
|
->distinct()
|
||||||
->pluck('device')
|
->pluck('device')
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
$links = Link::where('workspace_id', $workspace->id)
|
$links = Link::where('workspace_id', $wsId)
|
||||||
->orderBy('slug')
|
->orderBy('slug')
|
||||||
->get(['ulid', 'slug', 'title']);
|
->get(['ulid', 'slug', 'title']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ window.Echo = new Echo({
|
||||||
enabledTransports: ['ws', 'wss'],
|
enabledTransports: ['ws', 'wss'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
window.QRCode = QRCode;
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
LineController, LineElement,
|
LineController, LineElement,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
@props(['value', 'label' => 'Kopieren'])
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
x-data="{ copied: false }"
|
||||||
|
@click="navigator.clipboard.writeText(@js($value)).then(() => { copied = true; setTimeout(() => copied = false, 1500); }).catch(() => {
|
||||||
|
var ta = document.createElement('textarea');
|
||||||
|
ta.value = @js($value);
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
copied = true;
|
||||||
|
setTimeout(() => copied = false, 1500);
|
||||||
|
})"
|
||||||
|
:class="copied ? 'text-green-400' : 'text-t2 hover:text-t1'"
|
||||||
|
class="inline-flex items-center gap-1 text-xs transition-colors"
|
||||||
|
>
|
||||||
|
<span x-show="!copied" class="flex items-center gap-1">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="5" y="5" width="8" height="8" rx="1"/><path stroke-linecap="round" stroke-linejoin="round" d="M11 5V3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h2"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ $label }}</span>
|
||||||
|
</span>
|
||||||
|
<span x-show="copied" class="flex items-center gap-1">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8l3 3 7-7"/>
|
||||||
|
</svg>
|
||||||
|
<span>Kopiert</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
function updateActiveNav() {
|
function updateActiveNav() {
|
||||||
var path = window.location.pathname;
|
var path = window.location.pathname;
|
||||||
document.querySelectorAll('[data-nav-link]').forEach(function (el) {
|
document.querySelectorAll('[data-nav-link]').forEach(function (el) {
|
||||||
var href = el.getAttribute('href');
|
var href = new URL(el.href, location.origin).pathname;
|
||||||
var isActive = href && (path === href || path.startsWith(href + '/'));
|
var isActive = href && (path === href || path.startsWith(href + '/'));
|
||||||
el.classList.toggle('bg-blue/10', isActive);
|
el.classList.toggle('bg-blue/10', isActive);
|
||||||
el.classList.toggle('text-blue', isActive);
|
el.classList.toggle('text-blue', isActive);
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
|
|
||||||
@script
|
@script
|
||||||
<script>
|
<script>
|
||||||
import QRCode from 'qrcode';
|
const QRCode = window.QRCode;
|
||||||
|
|
||||||
const canvas = document.getElementById('qr-canvas');
|
const canvas = document.getElementById('qr-canvas');
|
||||||
const placeholder = document.getElementById('qr-placeholder');
|
const placeholder = document.getElementById('qr-placeholder');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue