Add slug availability check in listing form

This commit is contained in:
Tero Halla-aho 2025-11-29 20:13:17 +02:00
parent b34c0b6f1a
commit 6306256d4c
3 changed files with 58 additions and 2 deletions

View file

@ -0,0 +1,18 @@
import { NextResponse } from 'next/server';
import { prisma } from '../../../../lib/prisma';
export async function GET(req: Request) {
const url = new URL(req.url);
const slug = url.searchParams.get('slug')?.trim().toLowerCase();
if (!slug) {
return NextResponse.json({ error: 'Missing slug' }, { status: 400 });
}
const existing = await prisma.listingTranslation.findFirst({
where: { slug, listing: { removedAt: null } },
select: { id: true },
});
return NextResponse.json({ available: !existing });
}

View file

@ -60,6 +60,7 @@ export default function NewListingPage() {
const [isAuthed, setIsAuthed] = useState(false);
const [aiResponse, setAiResponse] = useState('');
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'error'>('idle');
const [slugStatus, setSlugStatus] = useState<'idle' | 'checking' | 'available' | 'taken' | 'error'>('idle');
useEffect(() => {
setCurrentLocale(uiLocale as Locale);
@ -214,6 +215,25 @@ export default function NewListingPage() {
}));
}
async function checkSlugAvailability() {
const value = slug.trim().toLowerCase();
if (!value) {
setSlugStatus('idle');
return;
}
setSlugStatus('checking');
try {
const res = await fetch(`/api/listings/check-slug?slug=${encodeURIComponent(value)}`, { cache: 'no-store' });
const data = await res.json();
if (!res.ok || typeof data.available !== 'boolean') {
throw new Error('bad response');
}
setSlugStatus(data.available ? 'available' : 'taken');
} catch (err) {
setSlugStatus('error');
}
}
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setMessage(null);
@ -325,8 +345,14 @@ export default function NewListingPage() {
<form onSubmit={onSubmit} style={{ display: 'grid', gap: 10 }}>
<label>
{t('slugLabel')}
<input value={slug} onChange={(e) => setSlug(e.target.value)} required />
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('slugHelp')}</div>
<input value={slug} onChange={(e) => setSlug(e.target.value)} onBlur={checkSlugAvailability} required />
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4, flexWrap: 'wrap' }}>
<span style={{ color: '#cbd5e1', fontSize: 12 }}>{t('slugHelp')}</span>
{slugStatus === 'checking' ? <span style={{ color: '#cbd5e1', fontSize: 12 }}>{t('slugChecking')}</span> : null}
{slugStatus === 'available' ? <span style={{ color: '#34d399', fontSize: 12 }}>{t('slugAvailable')}</span> : null}
{slugStatus === 'taken' ? <span style={{ color: '#f87171', fontSize: 12 }}>{t('slugTaken')}</span> : null}
{slugStatus === 'error' ? <span style={{ color: '#facc15', fontSize: 12 }}>{t('slugCheckError')}</span> : null}
</div>
</label>
<div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>

View file

@ -155,6 +155,10 @@ const baseMessages = {
loginToCreate: 'Please log in first to create a listing.',
slugLabel: 'Slug',
slugHelp: 'Unique link name, use lowercase letters and hyphens only (e.g. lake-cabin).',
slugChecking: 'Checking availability…',
slugAvailable: 'Slug is available',
slugTaken: 'Slug already in use',
slugCheckError: 'Could not check slug right now',
localeInput: 'Locale',
titleLabel: 'Title',
descriptionLabel: 'Description',
@ -420,6 +424,10 @@ const baseMessages = {
loginToCreate: 'Kirjaudu ensin luodaksesi kohteen.',
slugLabel: 'Osoitepolku',
slugHelp: 'Yksilöllinen linkki, käytä pieniä kirjaimia ja väliviivoja (esim. saimaa-mokki).',
slugChecking: 'Tarkistetaan saatavuutta…',
slugAvailable: 'Slug on vapaa',
slugTaken: 'Slug on jo käytössä',
slugCheckError: 'Slugia ei voitu tarkistaa nyt',
localeInput: 'Kieli',
titleLabel: 'Otsikko',
descriptionLabel: 'Kuvaus',
@ -567,6 +575,10 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
aiApplySuccess: 'Översättningar uppdaterade från AI-svaret.',
aiHelperNote: 'AI:n ska bara returnera JSON med samma nycklar.',
translationMissing: 'Lägg till minst ett språk med titel och beskrivning.',
slugChecking: 'Kontrollerar tillgänglighet…',
slugAvailable: 'Sluggen är ledig',
slugTaken: 'Sluggen används redan',
slugCheckError: 'Kunde inte kontrollera sluggen nu',
};
export const messages = { ...baseMessages, sv: svMessages } as const;