feat(bio): live slug availability check while typing

- Edit view: debounced $wire.checkSlug() call on input (350ms)
- Shows red error text + red border immediately when slug taken
- Hides help text when error shown
- Edit component: checkSlug() queries DB excluding current page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-17 23:25:19 +02:00
parent 308778b99d
commit f742b40921
2 changed files with 63 additions and 14 deletions

View File

@ -90,6 +90,17 @@ class Edit extends Component
$this->dispatch('toast', message: 'Gespeichert', type: 'success');
}
public function checkSlug(string $slug): bool
{
$page = BioPage::findOrFail($this->pageId);
return BioPage::where('workspace_id', $page->workspace_id)
->where('slug', $slug)
->where('id', '!=', $page->id)
->whereNull('deleted_at')
->exists();
}
public function publish(array $state): void
{
$this->save($state);

View File

@ -528,10 +528,23 @@
<span class="prefix">{{ $displayUrlBase }}/</span>
<input class="input" type="text"
x-model="state.slug"
@input="state.slug = $event.target.value.replace(/[^a-z0-9\-_]/gi, '').toLowerCase(); $event.target.value = state.slug"
:class="slugTaken ? 'border-red/60 focus:ring-red/40 focus:border-red/60' : ''"
@input="
state.slug = $event.target.value.replace(/[^a-z0-9\-_]/gi, '').toLowerCase();
$event.target.value = state.slug;
clearTimeout(_slugTimer);
if (state.slug) {
_slugTimer = setTimeout(async () => {
slugTaken = await $wire.checkSlug(state.slug);
}, 350);
} else {
slugTaken = false;
}
"
placeholder="yourslug">
</div>
<div class="help">Lowercase, no spaces. Connect a custom domain in Domains.</div>
<div x-show="slugTaken" x-cloak class="mt-1 text-xs text-red">Dieser Slug ist bereits vergeben.</div>
<div class="help" x-show="!slugTaken">Lowercase, no spaces. Connect a custom domain in Domains.</div>
</div>
<div class="toggle-row" style="cursor:pointer"
@click="state.isPublished = !state.isPublished">
@ -674,6 +687,8 @@ function bioEditor(init) {
device: 'mobile',
dragSrcId: null,
dragOverId: null,
slugTaken: false,
_slugTimer: null,
cropState: { open: false, src: null, type: null, x: 0, y: 0, scale: 1, minScale: 1, _drag: null },
state: {
@ -1012,12 +1027,24 @@ function bioEditor(init) {
},
async save() {
await this.$wire.save(JSON.parse(JSON.stringify(this.state)));
const snap = JSON.parse(JSON.stringify(this.state));
await this.$wire.save(snap);
this.state.profile = snap.profile;
this.state.theme = snap.theme;
this.state.socials = snap.socials;
this.state.blocks = snap.blocks;
this.state.slug = snap.slug;
this.state.isPublished = false;
},
async publish() {
await this.$wire.publish(JSON.parse(JSON.stringify(this.state)));
const snap = JSON.parse(JSON.stringify(this.state));
await this.$wire.publish(snap);
this.state.profile = snap.profile;
this.state.theme = snap.theme;
this.state.socials = snap.socials;
this.state.blocks = snap.blocks;
this.state.slug = snap.slug;
this.state.isPublished = true;
},
@ -1030,20 +1057,31 @@ function bioEditor(init) {
(function () {
var mainEl = document.querySelector('main.flex-1');
var contentEl = mainEl && mainEl.parentElement;
if (mainEl && contentEl && window.innerWidth > 1180) {
contentEl.style.height = '100dvh';
mainEl.style.overflow = 'hidden';
mainEl.style.padding = '0';
mainEl.style.minHeight = '0';
var rootEl = mainEl && mainEl.querySelector('.bio-editor-root');
function applyLayout() {
if (window.innerWidth > 1180 && mainEl && contentEl && rootEl) {
contentEl.style.height = '100dvh';
mainEl.style.overflow = 'hidden';
mainEl.style.padding = '0';
mainEl.style.minHeight = '0';
rootEl.style.height = '100%';
rootEl.style.overflow = 'hidden';
}
}
applyLayout();
Livewire.hook('commit', function (ref) {
ref.succeed(function () {
requestAnimationFrame(applyLayout);
});
});
document.addEventListener('livewire:navigating', function restore() {
if (contentEl) contentEl.style.height = '';
if (mainEl) {
mainEl.style.overflow = '';
mainEl.style.padding = '';
mainEl.style.minHeight = '';
}
if (mainEl) { mainEl.style.overflow = ''; mainEl.style.padding = ''; mainEl.style.minHeight = ''; }
if (rootEl) { rootEl.style.height = ''; rootEl.style.overflow = ''; }
document.removeEventListener('livewire:navigating', restore);
});
})();