From 6306256d4cf508cb4eec5429e5190407f5f3b75c Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Sat, 29 Nov 2025 20:13:17 +0200 Subject: [PATCH] Add slug availability check in listing form --- app/api/listings/check-slug/route.ts | 18 +++++++++++++++++ app/listings/new/page.tsx | 30 ++++++++++++++++++++++++++-- lib/i18n.ts | 12 +++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 app/api/listings/check-slug/route.ts diff --git a/app/api/listings/check-slug/route.ts b/app/api/listings/check-slug/route.ts new file mode 100644 index 0000000..ee5e290 --- /dev/null +++ b/app/api/listings/check-slug/route.ts @@ -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 }); +} diff --git a/app/listings/new/page.tsx b/app/listings/new/page.tsx index 1db8286..a6566fc 100644 --- a/app/listings/new/page.tsx +++ b/app/listings/new/page.tsx @@ -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() {
diff --git a/lib/i18n.ts b/lib/i18n.ts index a531caf..9e4e3b7 100644 --- a/lib/i18n.ts +++ b/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 = { 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;