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 [isAuthed, setIsAuthed] = useState(false);
|
||||||
const [aiResponse, setAiResponse] = useState('');
|
const [aiResponse, setAiResponse] = useState('');
|
||||||
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'error'>('idle');
|
const [copyStatus, setCopyStatus] = useState<'idle' | 'copied' | 'error'>('idle');
|
||||||
|
const [slugStatus, setSlugStatus] = useState<'idle' | 'checking' | 'available' | 'taken' | 'error'>('idle');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentLocale(uiLocale as Locale);
|
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) {
|
async function onSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
@ -325,8 +345,14 @@ export default function NewListingPage() {
|
||||||
<form onSubmit={onSubmit} style={{ display: 'grid', gap: 10 }}>
|
<form onSubmit={onSubmit} style={{ display: 'grid', gap: 10 }}>
|
||||||
<label>
|
<label>
|
||||||
{t('slugLabel')}
|
{t('slugLabel')}
|
||||||
<input value={slug} onChange={(e) => setSlug(e.target.value)} required />
|
<input value={slug} onChange={(e) => setSlug(e.target.value)} onBlur={checkSlugAvailability} required />
|
||||||
<div style={{ color: '#cbd5e1', fontSize: 12 }}>{t('slugHelp')}</div>
|
<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>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>
|
<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.',
|
loginToCreate: 'Please log in first to create a listing.',
|
||||||
slugLabel: 'Slug',
|
slugLabel: 'Slug',
|
||||||
slugHelp: 'Unique link name, use lowercase letters and hyphens only (e.g. lake-cabin).',
|
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',
|
localeInput: 'Locale',
|
||||||
titleLabel: 'Title',
|
titleLabel: 'Title',
|
||||||
descriptionLabel: 'Description',
|
descriptionLabel: 'Description',
|
||||||
|
|
@ -420,6 +424,10 @@ const baseMessages = {
|
||||||
loginToCreate: 'Kirjaudu ensin luodaksesi kohteen.',
|
loginToCreate: 'Kirjaudu ensin luodaksesi kohteen.',
|
||||||
slugLabel: 'Osoitepolku',
|
slugLabel: 'Osoitepolku',
|
||||||
slugHelp: 'Yksilöllinen linkki, käytä pieniä kirjaimia ja väliviivoja (esim. saimaa-mokki).',
|
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',
|
localeInput: 'Kieli',
|
||||||
titleLabel: 'Otsikko',
|
titleLabel: 'Otsikko',
|
||||||
descriptionLabel: 'Kuvaus',
|
descriptionLabel: 'Kuvaus',
|
||||||
|
|
@ -567,6 +575,10 @@ const svMessages: Record<keyof typeof baseMessages.en, string> = {
|
||||||
aiApplySuccess: 'Översättningar uppdaterade från AI-svaret.',
|
aiApplySuccess: 'Översättningar uppdaterade från AI-svaret.',
|
||||||
aiHelperNote: 'AI:n ska bara returnera JSON med samma nycklar.',
|
aiHelperNote: 'AI:n ska bara returnera JSON med samma nycklar.',
|
||||||
translationMissing: 'Lägg till minst ett språk med titel och beskrivning.',
|
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;
|
export const messages = { ...baseMessages, sv: svMessages } as const;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue