Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

Custom Components

Creating and using custom React components in the Jose Madrid Salsa platform

Custom Components

This guide covers how to create custom React components that follow the patterns established in the Jose Madrid Salsa codebase.

Component Organization

Components are organized by domain:

UI Component Library

The project uses shadcn/ui components based on Radix UI primitives, configured via components.json:

# To add a new shadcn/ui component
npx shadcn@latest add [component-name]

Installed primitives include:

  • Accordion, Alert Dialog, Avatar
  • Checkbox, Dialog, Dropdown Menu
  • Label, Navigation Menu, Progress
  • Select, Separator, Switch, Tabs, Toast

Styling Approach

Components use Tailwind CSS with the cn() utility for conditional classes:

import { cn } from '@/lib/utils'

interface BadgeProps {
  variant: 'default' | 'success' | 'warning' | 'danger'
  children: React.ReactNode
}

function Badge({ variant, children }: BadgeProps) {
  return (
    <span
      className={cn(
        'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium',
        {
          'bg-gray-100 text-gray-700': variant === 'default',
          'bg-green-100 text-green-700': variant === 'success',
          'bg-yellow-100 text-yellow-700': variant === 'warning',
          'bg-red-100 text-red-700': variant === 'danger',
        }
      )}
    >
      {children}
    </span>
  )
}

Creating a Server Component

Server Components are the default in the App Router. They can fetch data directly:

// components/products/product-grid.tsx
import { prisma } from '@/lib/prisma'

interface ProductGridProps {
  categorySlug?: string
  limit?: number
}

export async function ProductGrid({ categorySlug, limit }: ProductGridProps) {
  const products = await prisma.product.findMany({
    where: {
      isActive: true,
      ...(categorySlug && {
        category: { slug: categorySlug },
      }),
    },
    take: limit,
    orderBy: [{ isFeatured: 'desc' }, { sortOrder: 'asc' }],
  })

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

Creating a Client Component

Add 'use client' at the top for components that need interactivity:

'use client'

import { useState } from 'react'

interface QuantitySelectorProps {
  initialQuantity?: number
  max: number
  onChange: (quantity: number) => void
}

export function QuantitySelector({
  initialQuantity = 1,
  max,
  onChange,
}: QuantitySelectorProps) {
  const [quantity, setQuantity] = useState(initialQuantity)

  const handleChange = (newQty: number) => {
    const clamped = Math.max(1, Math.min(max, newQty))
    setQuantity(clamped)
    onChange(clamped)
  }

  return (
    <div className="flex items-center gap-2">
      <button
        onClick={() => handleChange(quantity - 1)}
        disabled={quantity <= 1}
        className="px-3 py-1 border rounded disabled:opacity-50"
      >
        -
      </button>
      <span className="w-8 text-center">{quantity}</span>
      <button
        onClick={() => handleChange(quantity + 1)}
        disabled={quantity >= max}
        className="px-3 py-1 border rounded disabled:opacity-50"
      >
        +
      </button>
    </div>
  )
}

Client vs Server Boundary

Only add 'use client' when the component needs browser APIs (useState, useEffect, onClick, etc.). Server Components are more performant because they render on the server and send zero JavaScript to the client.

Component Patterns

Props Interface Pattern

Always define a named interface for component props:

interface HeatLevelBadgeProps {
  level: 'MILD' | 'MEDIUM' | 'HOT' | 'EXTRA_HOT'
  size?: 'sm' | 'md' | 'lg'
}

function HeatLevelBadge({ level, size = 'md' }: HeatLevelBadgeProps) {
  const colors = {
    MILD: 'bg-green-100 text-green-800',
    MEDIUM: 'bg-yellow-100 text-yellow-800',
    HOT: 'bg-orange-100 text-orange-800',
    EXTRA_HOT: 'bg-red-100 text-red-800',
  }

  return (
    <span className={cn('rounded-full px-2 py-0.5', colors[level])}>
      {level.replace('_', ' ')}
    </span>
  )
}

Composition Pattern

Build complex components from smaller pieces:

function ProductCard({ product }: { product: Product }) {
  return (
    <div className="rounded-lg border p-4">
      <ProductImage src={product.featuredImage} alt={product.name} />
      <div className="mt-3">
        <h3 className="font-semibold">{product.name}</h3>
        <HeatLevelBadge level={product.heatLevel} />
        <PriceDisplay
          price={product.price}
          compareAtPrice={product.compareAtPrice}
        />
      </div>
    </div>
  )
}

Loading States

Use Suspense boundaries for async Server Components:

import { Suspense } from 'react'

export default function ProductsPage() {
  return (
    <div>
      <h1>Our Salsas</h1>
      <Suspense fallback={<ProductGridSkeleton />}>
        <ProductGrid />
      </Suspense>
    </div>
  )
}

function ProductGridSkeleton() {
  return (
    <div className="grid grid-cols-3 gap-6">
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="h-64 rounded-lg bg-gray-200 animate-pulse" />
      ))}
    </div>
  )
}

Using Framer Motion

The project includes Framer Motion for animations:

'use client'

import { motion } from 'framer-motion'

function AnimatedCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  )
}

Using Lucide Icons

The project uses Lucide React for icons:

import { ShoppingCart, Heart, Star } from 'lucide-react'

function ActionBar() {
  return (
    <div className="flex gap-2">
      <button><ShoppingCart className="h-5 w-5" /></button>
      <button><Heart className="h-5 w-5" /></button>
    </div>
  )
}

State Management with Zustand

For client-side state that spans multiple components, use Zustand:

// store/cart-store.ts
import { create } from 'zustand'

interface CartState {
  items: CartItem[]
  addItem: (item: CartItem) => void
  removeItem: (productId: string) => void
  clearCart: () => void
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addItem: (item) =>
    set((state) => ({
      items: [...state.items, item],
    })),
  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((i) => i.productId !== productId),
    })),
  clearCart: () => set({ items: [] }),
}))

Form Handling

Use React Hook Form with Zod validation:

'use client'

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { CreateNewFeatureSchema, type CreateNewFeature } from '@/lib/validations/new-feature'

function NewFeatureForm() {
  const form = useForm<CreateNewFeature>({
    resolver: zodResolver(CreateNewFeatureSchema),
    defaultValues: { name: '', description: '' },
  })

  const onSubmit = async (data: CreateNewFeature) => {
    const response = await fetch('/api/new-feature', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    })
    // handle response
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.register('name')} />
      {form.formState.errors.name && (
        <p className="text-red-500">{form.formState.errors.name.message}</p>
      )}
      <button type="submit">Create</button>
    </form>
  )
}

How is this guide?

Edit on GitHub

Last updated on

On this page