Skip to main content

Overview

Horse Trust enables sellers to create comprehensive, professional horse listings with rich multimedia content, detailed specifications, and integrated veterinary records. The platform requires minimum quality standards to maintain marketplace integrity.

Horse Data Model

The Horse model is defined in server/src/models/Horse.ts with comprehensive validation:
const horseSchema = new Schema<IHorse>({
  seller_id: { type: Schema.Types.ObjectId, ref: "User", required: true, index: true },
  name: { type: String, required: [true, "Horse name is required"], trim: true },
  age: { type: Number, required: true, min: 0, max: 40 },
  breed: { type: String, required: [true, "Breed is required"], trim: true },
  discipline: { type: String, required: [true, "Discipline is required"], trim: true },
  pedigree: { type: String },
  location: { type: locationSchema, required: true },
  price: { type: Number, min: 0 },
  currency: { type: String, enum: ["USD", "EUR", "ARS", "BRL", "MXN"], default: "USD" },
  photos: {
    type: [photoSchema],
    validate: {
      validator: (photos: unknown[]) => photos.length >= 3,
      message: "At least 3 photos are required",
    },
  },
  videos: { type: [videoSchema], default: [] },
  status: { type: String, enum: ["active", "sold", "paused", "draft"], default: "draft" },
  views_count: { type: Number, default: 0 },
});
Minimum 3 photos are required for every listing to ensure quality presentation.

Creating a Horse Listing

The listing creation form is implemented in client/app/registro-caballo/RegistroCaballoForm.tsx:

1. Basic Information

name
string
required
Horse’s registered name (e.g., “Tornado Express”)
age
number
required
Age in years (0-40)
breed
string
required
Horse breed selection:
  • Pura Sangre (Thoroughbred)
  • Árabe (Arabian)
  • Cuarto de Milla (Quarter Horse)
  • Criollo
  • Todas las Razas (All Breeds)
discipline
string
required
Primary discipline:
  • Salto (Jumping)
  • Adiestramiento (Dressage)
  • Polo
  • Carreras (Racing)
pedigree
string
Genealogy information (e.g., “Padre: Viento Norte | Madre: Luna Roja”)

2. Location and Pricing

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
  <input name="country" placeholder="Ej. Argentina" required />
  <input name="region" placeholder="Ej. Posadas, Misiones" required />
  
  <div className="md:col-span-2 grid grid-cols-3 gap-4">
    <input name="price" type="number" placeholder="35000" required />
    <select name="currency">
      <option value="USD">USD</option>
      <option value="EUR">EUR</option>
      <option value="ARS">ARS</option>
    </select>
  </div>
</div>

3. Photo Upload

Photos are uploaded to Cloudinary with automatic processing:
const uploadToCloudinary = async (file: File) => {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('upload_preset', 'horse_trust_uploads');
  formData.append('cloud_name', 'di2agiylz');

  try {
    const res = await fetch('https://api.cloudinary.com/v1_1/di2agiylz/image/upload', {
      method: 'POST',
      body: formData,
    });
    const data = await res.json();
    return data.secure_url;
  } catch (error) {
    console.error('Error subiendo imagen:', error);
    return null;
  }
};
The first photo uploaded is automatically set as the cover photo for marketplace display.

4. Video Integration

The platform supports YouTube and Vimeo videos with automatic embed URL conversion:
function toEmbedUrl(url: string): string {
  // YouTube
  const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/);
  if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
  
  // Vimeo
  const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
  if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
  
  return url;
}

Video Schema

const videoSchema = new Schema({
  url: { type: String, required: true },
  embed_url: { type: String },
  video_type: { type: String, enum: ["training", "competition", "other"], required: true },
  title: { type: String },
  description: { type: String },
  recorded_at: { type: Date, required: true },
  uploaded_at: { type: Date, default: () => new Date() },
});
1

Paste Video URL

Enter a YouTube or Vimeo URL in the video field
2

Auto-conversion

The system automatically converts it to an embeddable format
3

Select Type

Choose video type: Training, Competition, or Other
4

Add Metadata

Provide title and recording date for context

Veterinary Records

Listings can include initial veterinary records to build buyer confidence:

Record Structure

const vetRecordSchema = new Schema<IVetRecord>({
  horse_id: { type: Schema.Types.ObjectId, ref: "Horse", required: true, index: true },
  vet_id: { type: Schema.Types.ObjectId, ref: "User" },
  review_date: { type: Date, required: true },
  health_status: { type: String, required: true },
  certificates: { type: [certificateSchema], default: [] },
  vaccines: { type: [vaccineSchema], default: [] },
  validation_status: { type: String, enum: ["pending", "validated", "rejected"], default: "pending" },
  notes: { type: String },
});

Vaccine Tracking

Sellers can add multiple vaccine records:
const addVaccineRow = () => {
  setVaccines([...vaccines, { 
    vaccine_name: '', 
    applied_date: '', 
    next_dose_date: '' 
  }]);
};

// Dynamic vaccine form fields
vaccines.map((v, index) => (
  <div key={index} className="flex gap-3">
    <input 
      placeholder="Ej. Tétanos" 
      value={v.vaccine_name} 
      onChange={(e) => updateVaccine(index, 'vaccine_name', e.target.value)} 
    />
    <input 
      type="date" 
      value={v.applied_date}
      onChange={(e) => updateVaccine(index, 'applied_date', e.target.value)} 
    />
    <input 
      type="date" 
      value={v.next_dose_date}
      onChange={(e) => updateVaccine(index, 'next_dose_date', e.target.value)} 
    />
    <button onClick={() => removeVaccine(index)}>Remove</button>
  </div>
))
Listings with complete veterinary records receive 3x more inquiries on average.
The platform implements full-text search with Spanish language support:
horseSchema.index(
  { name: "text", breed: "text", discipline: "text", pedigree: "text" },
  { default_language: "spanish" }
);
This enables buyers to search across:
  • Horse names
  • Breed information
  • Disciplines
  • Pedigree details

Listing Submission

The complete submission flow with database wake-up:
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  if (photos.length < 3) {
    alert("Se requieren al menos 3 fotos.");
    return;
  }

  setIsSubmitting(true);
  
  try {
    // Auto-reconnect database if sleeping
    console.log("Despertando base de datos...");
    await reconnectDatabase();

    // 1. Upload images to Cloudinary
    const uploadedUrls = [];
    for (const file of photos) {
      const url = await uploadToCloudinary(file);
      if (url) uploadedUrls.push(url);
    }

    // 2. Build horse payload
    const horsePayload = {
      name: formData.name,
      age: Number(formData.age),
      breed: formData.breed,
      discipline: formData.discipline,
      pedigree: formData.pedigree,
      price: Number(formData.price),
      currency: formData.currency,
      location: `${formData.region}, ${formData.country}`,
      photos: uploadedUrls.map((url, i) => ({ url, is_cover: i === 0 })),
      videos: formData.videoUrl ? [{
        video_url: formData.videoUrl,
        video_type: formData.videoType,
        recording_date: formData.videoDate || new Date().toISOString().split('T')[0]
      }] : []
    };

    const result = await createHorse(horsePayload);

    if (!result.success) throw new Error(result.error);

    const horseId = result.data?.ID || result.data?.id || result.data?._id;

    // 3. Add veterinary record if provided
    if (horseId && formData.reviewDate) {
      const vetPayload = {
        review_date: formData.reviewDate,
        certificate_url: formData.certificateUrl || "",
        notes: formData.vetNotes || "",
        vaccines: vaccines.filter(v => v.vaccine_name.trim() !== "")
      };
      await addVetRecord(horseId, vetPayload);
    }

    alert("¡Ejemplar y ficha médica publicados con éxito!");
    router.push('/dashboard');

  } catch (error: any) {
    alert(error.message || "Hubo un error en el servidor");
  } finally {
    setIsSubmitting(false);
  }
};

Live Preview

The form includes a real-time preview of how the listing will appear:
const previewHorse = {
  ID: 999,
  NAME: formData.name || 'Nombre del Ejemplar',
  AGE: Number(formData.age) || 0,
  BREED: formData.breed,
  DISCIPLINE: formData.discipline,
  LOCATION: formData.region && formData.country 
    ? `${formData.region}, ${formData.country}` 
    : 'Ubicación',
  PRICE: Number(formData.price) || 0,
  SELLER_VERIFIED: 'verified',
  MAIN_PHOTO: previewUrls.length > 0 
    ? previewUrls[0] 
    : 'https://images.unsplash.com/photo-1598974357801-cbca100e65d3?q=80&w=800'
};

<div className="sticky top-24">
  <h4>Vista Previa en Vivo</h4>
  <HorseCard horse={previewHorse} />
</div>

Listing Status

Listings progress through multiple states:
1

Draft

Initial state when creating a listing
2

Active

Published and visible in marketplace
3

Paused

Temporarily hidden by seller
4

Sold

Horse has been sold, listing archived

Best Practices

High-Quality Photos

Use professional photos in good lighting. First photo is the cover image.

Complete Pedigree

Detailed genealogy information increases buyer confidence

Add Videos

Showcase training or competition footage for better engagement

Medical Records

Including vet records triples inquiry rates

Quality Indicators

The platform highlights listing quality to encourage complete profiles:
<div className="bg-equestrian-navy/5 rounded-xl p-4">
  <ShieldCheck className="text-equestrian-navy w-5 h-5" />
  <h5>Calidad del Anuncio</h5>
  <p>Los anuncios con un historial médico inicial, pedigree detallado 
     y video reciben el triple de consultas.</p>
</div>

Next Steps

Browse Marketplace

See how listings appear to buyers

Real-Time Chat

Communicate with interested buyers

Build docs developers (and LLMs) love