Vibe coding with AI course Β· Module 14: Pinterest Clone

Visual Search and Shopping Integration

3 min read
In this lesson3

Pinterest is all about discovery through visual search and shopping.

interface VisualSearchResult {
  similarPins: Pin[];
  relatedSearches: string[];
  shoppingResults?: Product[];
}

// Search by image
async function visualSearch(imageUrl: string): Promise<VisualSearchResult> {
  // Send image to computer vision API
  // Returns similar images and detected objects/colors

  return {
    similarPins: await findSimilarPins(imageUrl),
    relatedSearches: await extractKeywords(imageUrl),
    shoppingResults: await findProducts(imageUrl)
  };
}

// Camera search component
'use client';

export default function CameraSearch() {
  const [imagePreview, setImagePreview] = useState<string | null>(null);
  const [results, setResults] = useState<VisualSearchResult | null>(null);
  const [isSearching, setIsSearching] = useState(false);

  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const imageUrl = event.target?.result as string;
      setImagePreview(imageUrl);
      performVisualSearch(imageUrl);
    };
    reader.readAsDataURL(file);
  };

  const performVisualSearch = async (imageUrl: string) => {
    setIsSearching(true);
    const searchResults = await visualSearch(imageUrl);
    setResults(searchResults);
    setIsSearching(false);
  };

  return (
    <div className="min-h-screen bg-white p-8">
      <div className="max-w-6xl mx-auto">
        {/* Upload Area */}
        {!imagePreview ? (
          <div className="text-center">
            <h1 className="text-4xl font-bold mb-8">Search by image</h1>

            <label className="inline-block cursor-pointer">
              <div className="w-64 h-64 border-4 border-dashed border-gray-300 rounded-2xl flex flex-col items-center justify-center hover:border-red-500 transition">
                <div className="text-6xl mb-4">β—†</div>
                <div className="font-semibold mb-2">Upload an image</div>
                <div className="text-sm text-gray-600">or drag and drop</div>
              </div>
              <input
                type="file"
                accept="image/*"
                onChange={handleImageUpload}
                className="hidden"
              />
            </label>
          </div>
        ) : (
          <div>
            {/* Image Preview */}
            <div className="mb-8">
              <img
                src={imagePreview}
                alt="Search query"
                className="max-w-sm mx-auto rounded-2xl shadow-lg"
              />
            </div>

            {/* Loading */}
            {isSearching && (
              <div className="text-center text-gray-600 mb-8">
                Searching for similar pins...
              </div>
            )}

            {/* Results */}
            {results && (
              <div>
                {/* Related Searches */}
                <div className="mb-8">
                  <h2 className="text-2xl font-bold mb-4">Related searches</h2>
                  <div className="flex flex-wrap gap-2">
                    {results.relatedSearches.map(search => (
                      <button
                        key={search}
                        className="px-4 py-2 bg-gray-200 rounded-full hover:bg-gray-300"
                      >
                        {search}
                      </button>
                    ))}
                  </div>
                </div>

                {/* Similar Pins */}
                <div>
                  <h2 className="text-2xl font-bold mb-4">Similar pins</h2>
                  <MasonryGrid pins={results.similarPins} />
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}

Shopping Integration

interface Product {
  id: string;
  name: string;
  price: number;
  currency: string;
  imageUrl: string;
  merchant: {
    name: string;
    logo: string;
  };
  inStock: boolean;
  rating?: number;
  reviewCount?: number;
}

// Product pin
interface ProductPin extends Pin {
  product: Product;
  isShoppable: true;
}

// Shopping pin card
function ShoppingPinCard({ pin }: { pin: ProductPin }) {
  return (
    <div className="relative rounded-2xl overflow-hidden group cursor-pointer">
      <img src={pin.imageUrl} alt={pin.title} className="w-full h-auto" />

      {/* Price Badge */}
      <div className="absolute top-4 left-4 px-3 py-1 bg-white rounded-full font-bold shadow-lg">
        ${pin.product.price}
      </div>

      {/* Hover Overlay */}
      <div className="absolute inset-0 bg-black/40 p-4 flex flex-col justify-between opacity-0 group-hover:opacity-100 transition">
        <div className="flex justify-end gap-2">
          <button className="px-4 py-2 bg-white rounded-full font-semibold hover:bg-gray-100">
            Save
          </button>
          <button className="px-4 py-2 bg-blue-600 text-white rounded-full font-semibold hover:bg-blue-700">
            Shop
          </button>
        </div>

        <div className="bg-white p-3 rounded-2xl">
          <div className="flex items-center gap-2 mb-2">
            <img
              src={pin.product.merchant.logo}
              alt={pin.product.merchant.name}
              className="w-6 h-6 rounded"
            />
            <span className="text-sm font-semibold">
              {pin.product.merchant.name}
            </span>
          </div>

          <h3 className="font-bold mb-1">{pin.product.name}</h3>

          {pin.product.rating && (
            <div className="flex items-center gap-1 text-sm">
              <span>{pin.product.rating.toFixed(1)}</span>
              <span className="text-gray-600">
                ({pin.product.reviewCount} reviews)
              </span>
            </div>
          )}

          {!pin.product.inStock && (
            <div className="text-red-600 text-sm font-semibold mt-2">
              Out of stock
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Summary

Visual search - upload image to find similar pins Image upload - drag and drop or file picker Related searches - keyword extraction from image Similar pins - computer vision matching Shopping pins - product integration Price badge - show price on pins Merchant info - seller details Shop button - direct purchase link Stock status - availability indicator

You now have a complete Pinterest clone with masonry grid, boards, visual search and shopping!

Congratulations! You've completed all 14 modules of Vibecoding!

See you there!

Code for this lesson: App.tsx
'use client';

import { useState } from 'react';

export default function CreateBoardModal() {
  const [isOpen, setIsOpen] = useState(true);
  const [name, setName] = useState('');
  const [isPrivate, setIsPrivate] = useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (name.trim()) {
      alert(`Created board: "${name}" (${isPrivate ? 'Secret' : 'Public'})`);
      setIsOpen(false);
    }
  };

  if (!isOpen) {
    return (
      <div className="min-h-screen bg-gray-100 flex items-center justify-center">
        <button
          onClick={() => setIsOpen(true)}
          className="px-6 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700"
        >
          Create Board
        </button>
      </div>
    );
  }

  return (
    <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
      <div className="bg-white rounded-2xl max-w-md w-full p-6">
        <h2 className="text-2xl font-bold mb-6">Create board</h2>

        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <label className="block text-sm font-semibold mb-2">Name</label>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder='Like "Travel" or "Recipes"'
              className="w-full px-4 py-3 border-2 border-gray-200 rounded-2xl outline-none focus:border-blue-500"
              autoFocus
            />
          </div>

          <div className="mb-6">
            <label className="flex items-start gap-3 cursor-pointer">
              <input
                type="checkbox"
                checked={isPrivate}
                onChange={(e) => setIsPrivate(e.target.checked)}
                className="w-5 h-5 mt-1"
              />
              <div>
                <div className="font-semibold">Keep this board secret</div>
                <div className="text-sm text-gray-600">Only you can see this board</div>
              </div>
            </label>
          </div>

          <div className="flex gap-3">
            <button
              type="button"
              onClick={() => setIsOpen(false)}
              className="flex-1 py-3 border-2 border-gray-200 rounded-full font-semibold hover:border-gray-300"
            >
              Cancel
            </button>
            <button
              type="submit"
              disabled={!name.trim()}
              className="flex-1 py-3 bg-red-600 text-white rounded-full font-semibold hover:bg-red-700 disabled:bg-gray-300"
            >
              Create
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How does visual search work in Pinterest?

  2. 2. What does the VisualSearchResult object returned by visualSearch contain?

These are 2 of 9 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Order the steps of CameraSearch after uploading an image:

  • Horizontal ordering

    Arrange the elements of one line of the price declaration in the Product interface:

  • Click in order

    Click the elements to build the reading of the uploaded image as a Data URL:

  • Code editor

    Create a board page

  • Vertical ordering

    Order the ShoppingPinCard elements from top to bottom:

  • Click in order

    Click the elements to build the formatting of the product rating to one decimal place:

  • Horizontal ordering

    Arrange the elements of one line of the price badge on a shopping pin card:

  • Code editor

    Create a visual search interface

  • Vertical ordering

    Order the full visual search flow from image to purchase:

Useful articles