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');
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue