vendor slugs routes instead of ids

This commit is contained in:
2026-01-27 16:56:57 -05:00
parent aa2f30c086
commit c6ff651f21
10 changed files with 247 additions and 120 deletions

View File

@@ -5,15 +5,15 @@ import { VendorDetailPage } from '@/components/vendor/VendorDetailPage'
interface PageProps { interface PageProps {
params: { params: {
id: string slug: string
} }
} }
export default async function Page({ params }: PageProps) { export default async function Page({ params }: PageProps) {
const { id } = params const { slug } = params
const vendor = await prisma.vendor.findUnique({ const vendor = await prisma.vendor.findUnique({
where: { id }, where: { slug },
include: { include: {
address: true, address: true,
events: { events: {

View File

@@ -2,6 +2,7 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { VendorType, VendorStatus } from '@prisma/client' import { VendorType, VendorStatus } from '@prisma/client'
import { generateUniqueSlug, slugify } from '@/lib/utils/slugify'
export async function PUT( export async function PUT(
request: NextRequest, request: NextRequest,
@@ -24,6 +25,20 @@ export async function PUT(
) )
} }
let slug = existingVendor.slug
if (body.name && body.name !== existingVendor.name) {
const baseSlug = slugify(body.name)
slug = await generateUniqueSlug(
baseSlug,
async (testSlug) => {
const existing = await prisma.vendor.findUnique({
where: { slug: testSlug }
})
return !!existing && existing.id !== id
}
)
}
// Parse numeric values // Parse numeric values
const parseFloatOrNull = (value: any): number | null => { const parseFloatOrNull = (value: any): number | null => {
if (value === null || value === undefined || value === '') return null if (value === null || value === undefined || value === '') return null
@@ -75,6 +90,7 @@ export async function PUT(
// Prepare update data // Prepare update data
const updateData: any = { const updateData: any = {
slug,
name: body.name, name: body.name,
type: body.type as VendorType, type: body.type as VendorType,
description: body.description || null, description: body.description || null,

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { VendorType, VendorStatus } from '@prisma/client' import { VendorType, VendorStatus } from '@prisma/client'
import { generateUniqueSlug, slugify } from '@/lib/utils/slugify'
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -16,6 +17,17 @@ export async function POST(request: NextRequest) {
) )
} }
const baseSlug = slugify(body.name)
const uniqueSlug = await generateUniqueSlug(
baseSlug,
async (slug) => {
const existing = await prisma.vendor.findUnique({
where: { slug }
})
return !!existing
}
)
// Check if we have address data // Check if we have address data
const hasAddress = body.street && body.city && body.state && body.postalCode const hasAddress = body.street && body.city && body.state && body.postalCode
@@ -34,6 +46,7 @@ export async function POST(request: NextRequest) {
// Prepare vendor data WITHOUT addressId // Prepare vendor data WITHOUT addressId
const vendorData: any = { const vendorData: any = {
slug: uniqueSlug,
name: body.name, name: body.name,
type: body.type as VendorType, type: body.type as VendorType,
description: body.description || null, description: body.description || null,

View File

@@ -5,7 +5,6 @@ import { Input } from '../ui/input'
import { Button } from '../ui/button' import { Button } from '../ui/button'
import { Label } from '../ui/label' import { Label } from '../ui/label'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Address, Vendor } from '@prisma/client'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { Textarea } from '../ui/textarea' import { Textarea } from '../ui/textarea'
import { Checkbox } from '../ui/checkbox' import { Checkbox } from '../ui/checkbox'

View File

@@ -14,6 +14,7 @@ import { useRouter } from 'next/navigation'
interface VendorRow { interface VendorRow {
id: string id: string
slug: string
name: string name: string
type: VendorType type: VendorType
description?: string | null description?: string | null
@@ -189,7 +190,7 @@ export default function VendorsTable({ initialVendors }: Props) {
// Handle row click // Handle row click
const handleRowClick = (vendor: VendorRow) => { const handleRowClick = (vendor: VendorRow) => {
router.push(`/vendors/${vendor.id}`) router.push(`/vendors/${vendor.slug}`)
} }
return ( return (

30
lib/utils/slugify.ts Normal file
View File

@@ -0,0 +1,30 @@
export function slugify(text: string): string {
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}
export async function generateUniqueSlug(
baseSlug: string,
checkUnique: (slug: string) => Promise<boolean>,
maxAttempts = 10
): Promise<string> {
let slug = baseSlug
let attempt = 1
while (await checkUnique(slug)) {
if (attempt >= maxAttempts) {
throw new Error(`Could not generate unique slug after ${maxAttempts} attempts`)
}
slug = `${baseSlug}-${attempt}`
attempt++
}
return slug
}

View File

@@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[slug]` on the table `Vendor` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Vendor" ADD COLUMN "slug" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Vendor_slug_key" ON "Vendor"("slug");

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `slug` on table `Vendor` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Vendor" ALTER COLUMN "slug" SET NOT NULL;

View File

@@ -0,0 +1,35 @@
-- CreateIndex
CREATE INDEX "Vendor_slug_idx" ON "Vendor"("slug");
-- CreateIndex
CREATE INDEX "Vendor_name_idx" ON "Vendor"("name");
-- CreateIndex
CREATE INDEX "Vendor_type_idx" ON "Vendor"("type");
-- CreateIndex
CREATE INDEX "Vendor_status_idx" ON "Vendor"("status");
-- CreateIndex
CREATE INDEX "Vendor_isBooked_idx" ON "Vendor"("isBooked");
-- CreateIndex
CREATE INDEX "Vendor_bookedDate_idx" ON "Vendor"("bookedDate");
-- CreateIndex
CREATE INDEX "Vendor_finalCost_idx" ON "Vendor"("finalCost");
-- CreateIndex
CREATE INDEX "Vendor_createdAt_idx" ON "Vendor"("createdAt");
-- CreateIndex
CREATE INDEX "Vendor_type_status_idx" ON "Vendor"("type", "status");
-- CreateIndex
CREATE INDEX "Vendor_isBooked_type_idx" ON "Vendor"("isBooked", "type");
-- CreateIndex
CREATE INDEX "Vendor_depositDueDate_idx" ON "Vendor"("depositDueDate");
-- CreateIndex
CREATE INDEX "Vendor_finalPaymentDue_idx" ON "Vendor"("finalPaymentDue");

View File

@@ -148,6 +148,7 @@ model Vendor {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
slug String @unique
name String name String
type VendorType type VendorType
@@ -178,6 +179,19 @@ model Vendor {
events Event[] events Event[]
categories Category[] categories Category[]
@@index([slug])
@@index([name])
@@index([type])
@@index([status])
@@index([isBooked])
@@index([bookedDate])
@@index([finalCost])
@@index([createdAt])
@@index([type, status])
@@index([isBooked, type])
@@index([depositDueDate])
@@index([finalPaymentDue])
} }
model Address { model Address {