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',
|
||||
];
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'ulid';
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use App\Domains\Bio\Models\BioPage;
|
|||
use App\Domains\Workspace\Models\Workspace;
|
||||
use App\Livewire\Pages\Bio\Index;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
use LivewireUI\Modal\ModalComponent;
|
||||
|
||||
|
|
@ -19,12 +20,24 @@ class CreateBioPage extends ModalComponent
|
|||
|
||||
public string $slug = '';
|
||||
|
||||
/** @return array<string, string> */
|
||||
/** @return array<string, mixed> */
|
||||
protected function rules(): array
|
||||
{
|
||||
$wsId = $this->workspaceId;
|
||||
if (! $wsId) {
|
||||
$ws = current_workspace();
|
||||
$wsId = $ws !== null ? $ws->id : 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'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
|
||||
{
|
||||
return '4xl';
|
||||
return '5xl';
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ class Index extends Component
|
|||
/** @return Builder<Click> */
|
||||
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());
|
||||
|
||||
if ($this->country) {
|
||||
|
|
@ -71,7 +71,7 @@ class Index extends Component
|
|||
$q->where('device', $this->device);
|
||||
}
|
||||
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) {
|
||||
$q->where('link_id', $link->id);
|
||||
}
|
||||
|
|
@ -91,11 +91,11 @@ class Index extends Component
|
|||
|
||||
public function render(): View
|
||||
{
|
||||
$workspace = require_workspace();
|
||||
$wsId = $this->workspaceId;
|
||||
|
||||
$totalFiltered = $this->baseQuery()->count();
|
||||
|
||||
$allTime = Click::where('workspace_id', $workspace->id)->count();
|
||||
$allTime = Click::where('workspace_id', $wsId)->count();
|
||||
|
||||
$byCountry = (clone $this->baseQuery())
|
||||
->whereNotNull('country')
|
||||
|
|
@ -111,20 +111,20 @@ class Index extends Component
|
|||
->orderByDesc('total')
|
||||
->get();
|
||||
|
||||
$countries = Click::where('workspace_id', $workspace->id)
|
||||
$countries = Click::where('workspace_id', $wsId)
|
||||
->whereNotNull('country')
|
||||
->distinct()
|
||||
->pluck('country')
|
||||
->sort()
|
||||
->values();
|
||||
|
||||
$devices = Click::where('workspace_id', $workspace->id)
|
||||
$devices = Click::where('workspace_id', $wsId)
|
||||
->whereNotNull('device')
|
||||
->distinct()
|
||||
->pluck('device')
|
||||
->values();
|
||||
|
||||
$links = Link::where('workspace_id', $workspace->id)
|
||||
$links = Link::where('workspace_id', $wsId)
|
||||
->orderBy('slug')
|
||||
->get(['ulid', 'slug', 'title']);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ window.Echo = new Echo({
|
|||
enabledTransports: ['ws', 'wss'],
|
||||
});
|
||||
|
||||
import QRCode from 'qrcode';
|
||||
window.QRCode = QRCode;
|
||||
|
||||
import {
|
||||
Chart,
|
||||
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() {
|
||||
var path = window.location.pathname;
|
||||
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 + '/'));
|
||||
el.classList.toggle('bg-blue/10', isActive);
|
||||
el.classList.toggle('text-blue', isActive);
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
|
||||
@script
|
||||
<script>
|
||||
import QRCode from 'qrcode';
|
||||
const QRCode = window.QRCode;
|
||||
|
||||
const canvas = document.getElementById('qr-canvas');
|
||||
const placeholder = document.getElementById('qr-placeholder');
|
||||
|
|
|
|||
Loading…
Reference in New Issue