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:
Horse’s registered name (e.g., “Tornado Express”)
Horse breed selection:
Pura Sangre (Thoroughbred)
Árabe (Arabian)
Cuarto de Milla (Quarter Horse)
Criollo
Todas las Razas (All Breeds)
Primary discipline:
Salto (Jumping)
Adiestramiento (Dressage)
Polo
Carreras (Racing)
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 () },
});
Paste Video URL
Enter a YouTube or Vimeo URL in the video field
Auto-conversion
The system automatically converts it to an embeddable format
Select Type
Choose video type: Training, Competition, or Other
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.
Full-Text Search
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:
Draft
Initial state when creating a listing
Active
Published and visible in marketplace
Paused
Temporarily hidden by seller
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