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
parent
308778b99d
commit
f742b40921
|
|
@ -90,6 +90,17 @@ class Edit extends Component
|
||||||
$this->dispatch('toast', message: 'Gespeichert', type: 'success');
|
$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
|
public function publish(array $state): void
|
||||||
{
|
{
|
||||||
$this->save($state);
|
$this->save($state);
|
||||||
|
|
|
||||||
|
|
@ -528,10 +528,23 @@
|
||||||
<span class="prefix">{{ $displayUrlBase }}/</span>
|
<span class="prefix">{{ $displayUrlBase }}/</span>
|
||||||
<input class="input" type="text"
|
<input class="input" type="text"
|
||||||
x-model="state.slug"
|
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">
|
placeholder="yourslug">
|
||||||
</div>
|
</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>
|
||||||
<div class="toggle-row" style="cursor:pointer"
|
<div class="toggle-row" style="cursor:pointer"
|
||||||
@click="state.isPublished = !state.isPublished">
|
@click="state.isPublished = !state.isPublished">
|
||||||
|
|
@ -674,6 +687,8 @@ function bioEditor(init) {
|
||||||
device: 'mobile',
|
device: 'mobile',
|
||||||
dragSrcId: null,
|
dragSrcId: null,
|
||||||
dragOverId: null,
|
dragOverId: null,
|
||||||
|
slugTaken: false,
|
||||||
|
_slugTimer: null,
|
||||||
cropState: { open: false, src: null, type: null, x: 0, y: 0, scale: 1, minScale: 1, _drag: null },
|
cropState: { open: false, src: null, type: null, x: 0, y: 0, scale: 1, minScale: 1, _drag: null },
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
|
|
@ -1012,12 +1027,24 @@ function bioEditor(init) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async save() {
|
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;
|
this.state.isPublished = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async publish() {
|
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;
|
this.state.isPublished = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1030,20 +1057,31 @@ function bioEditor(init) {
|
||||||
(function () {
|
(function () {
|
||||||
var mainEl = document.querySelector('main.flex-1');
|
var mainEl = document.querySelector('main.flex-1');
|
||||||
var contentEl = mainEl && mainEl.parentElement;
|
var contentEl = mainEl && mainEl.parentElement;
|
||||||
if (mainEl && contentEl && window.innerWidth > 1180) {
|
var rootEl = mainEl && mainEl.querySelector('.bio-editor-root');
|
||||||
|
|
||||||
|
function applyLayout() {
|
||||||
|
if (window.innerWidth > 1180 && mainEl && contentEl && rootEl) {
|
||||||
contentEl.style.height = '100dvh';
|
contentEl.style.height = '100dvh';
|
||||||
mainEl.style.overflow = 'hidden';
|
mainEl.style.overflow = 'hidden';
|
||||||
mainEl.style.padding = '0';
|
mainEl.style.padding = '0';
|
||||||
mainEl.style.minHeight = '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() {
|
document.addEventListener('livewire:navigating', function restore() {
|
||||||
if (contentEl) contentEl.style.height = '';
|
if (contentEl) contentEl.style.height = '';
|
||||||
if (mainEl) {
|
if (mainEl) { mainEl.style.overflow = ''; mainEl.style.padding = ''; mainEl.style.minHeight = ''; }
|
||||||
mainEl.style.overflow = '';
|
if (rootEl) { rootEl.style.height = ''; rootEl.style.overflow = ''; }
|
||||||
mainEl.style.padding = '';
|
|
||||||
mainEl.style.minHeight = '';
|
|
||||||
}
|
|
||||||
document.removeEventListener('livewire:navigating', restore);
|
document.removeEventListener('livewire:navigating', restore);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue