Add slug availability check in listing form
This commit is contained in:
parent
b34c0b6f1a
commit
6306256d4c
3 changed files with 58 additions and 2 deletions
18
app/api/listings/check-slug/route.ts
Normal file
18
app/api/listings/check-slug/route.ts
Normal 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 });
|
||||
}
|
||||
|
|
@ -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 }}>
|
||||
|
|
|
|||
12
lib/i18n.ts
12
lib/i18n.ts
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue