Overview
AndanDo’s marketplace is the central hub where travelers discover and book tours, activities, and experiences. Built with Blazor Server, the marketplace provides a dynamic, real-time browsing experience with advanced filtering, search, and pricing transparency.
Real-time Updates Server-side rendering ensures users always see current availability and pricing
Rich Media High-quality images and detailed descriptions for each tour
Smart Filtering Filter by price, duration, location, and availability
Rating System Verified reviews and ratings from real customers
Marketplace Architecture
The marketplace leverages a sophisticated data pipeline to deliver up-to-date tour information:
public async Task < IReadOnlyList < TourMarketplaceItemDto >> GetToursForMarketplaceAsync (
CancellationToken cancellationToken = default )
{
await using var conn = new SqlConnection ( _connectionString );
await conn . OpenAsync ( cancellationToken );
await using var cmd = conn . CreateCommand ();
cmd . CommandText = "sp_Tour_ListMarketplace" ;
cmd . CommandType = CommandType . StoredProcedure ;
// Process results with ticket types and ratings...
}
Data Model
Each marketplace item includes comprehensive tour information:
TourMarketplaceItemDto
TourTicketTypeSummaryDto
public record TourMarketplaceItemDto (
int TourId ,
string Title ,
string LocationLabel ,
string ? FirstImageUrl ,
string ? DurationLabel ,
decimal ? FromPrice ,
decimal ? MaxPrice ,
IReadOnlyList < TourTicketTypeSummaryDto > TicketTypes ,
double ? AvgRating ,
int RatingCount
);
User Interface Features
Tour Cards
The marketplace displays tours in an interactive card layout with hover effects and instant navigation:
< div class = "mp-card" @onclick = "() => { if (!card.DisableNavigation) GoToTourDetails(tour.TourId); }" >
< div class = "mp-card__image-wrap" >
@if ( card . IsUpcoming )
{
< div class = "mp-card__upcoming-badge" >
< span > Próximamente </ span >
</ div >
}
else
{
< img src = " @ imgSrc " alt = " @ tour . Title " />
}
< button type = "button" class = "mp-card__fav" @onclick = "() => ToggleLikeAsync(card)" >
< svg class = "fav-svg" > <!-- Heart icon --> </ svg >
</ button >
</ div >
< div class = "mp-card__body" >
< div class = "mp-card__location" > @ tour . LocationLabel </ div >
< div class = "mp-card__title" > @ tour . Title </ div >
< div class = "mp-card__rating" > @ GetRatingText ( tour ) </ div >
< div class = "mp-card__footer" >
< div class = "mp-card__price-val" > @ card . PriceDisplay </ div >
< button type = "button" class = "mp-go-btn" > Ver </ button >
</ div >
</ div >
</ div >
Filter Chips
Users can quickly filter tours using interactive chips:
Duration Filters
Filter by short duration (hours/minutes) or long duration (days/weeks)
Price Ranges
Economic (under RD3 , 000 ) o r P r e m i u m ( o v e r R D 3,000) or Premium (over RD 3 , 000 ) or P re mi u m ( o v er R D 3,000) options
Availability Status
Show only upcoming tours or currently available experiences
< button class = "mp-chip @( _durationFilter == "short" ? "active" : "" ) "
type = "button"
@onclick = " @( () => { _durationFilter = "short"; _currentPage = 1 ; } ) " >
< svg > <!-- Clock icon --> </ svg >
Corta duración
</ button >
< button class = "mp-chip @( _priceFilter == "low" ? "active" : "" ) "
type = "button"
@onclick = " @( () => { _priceFilter = "low"; _currentPage = 1 ; } ) " >
< svg > <!-- Dollar icon --> </ svg >
Económico
</ button >
Dynamic Pricing Display
The marketplace intelligently displays pricing based on available ticket types:
string ? priceDisplay = null ;
if ( tour . TicketTypes != null && tour . TicketTypes . Any ())
{
var activeTickets = tour . TicketTypes
. Where ( t => t . IsActive && t . Price . HasValue )
. ToList ();
if ( activeTickets . Any ())
{
var availableTickets = activeTickets
. Where ( t => ( ! t . VentaInicioUtc . HasValue || t . VentaInicioUtc <= now )
&& ( ! t . VentaFinUtc . HasValue || t . VentaFinUtc >= now ))
. ToList ();
if ( availableTickets . Any ())
{
var minPrice = availableTickets . Min ( t => t . Price . Value );
priceDisplay = $"Desde: { FormatDop ( minPrice )} " ;
}
else
{
// Check if any future tickets exist
if ( activeTickets . Any ( t => t . VentaInicioUtc . HasValue && t . VentaInicioUtc > now ))
{
priceDisplay = "Próximamente" ;
}
}
}
}
The marketplace automatically hides tours with “Status = I” (Inactive) and respects visibility windows configured by tour operators.
The marketplace implements smart pagination with 12 tours per page:
private const int PageSize = 12 ;
private int _currentPage = 1 ;
private IEnumerable < TourCardViewModel > PagedFilteredTours =>
FilteredTours . Skip (( _currentPage - 1 ) * PageSize ). Take ( PageSize );
private int TotalFilteredPages =>
Math . Max ( 1 , ( int ) Math . Ceiling (( double ) FilteredTours . Count () / PageSize ));
< div class = "mp-pagination" >
< button class = "mp-page-btn" disabled = " @( ! CanGoPrev ) " @onclick = "PrevPage" >
< svg > <!-- Left arrow --> </ svg >
</ button >
@foreach ( var pageNum in GetPageNumbers ())
{
@if ( pageNum == - 1 )
{
< span class = "mp-page-dots" > … </ span >
}
else
{
< button class = "mp-page-btn @( pageNum == _currentPage ? "active" : "" ) "
@ onclick = "() = > GoToPage(pageNum)"> @ pageNum </ button >
}
}
< button class = "mp-page-btn" disabled = " @( ! CanGoNext ) " @onclick = "NextPage" >
< svg > <!-- Right arrow --> </ svg >
</ button >
</ div >
Sorting Options
Users can sort tours by multiple criteria:
private IEnumerable < TourCardViewModel > FilteredTours => Tours
. Where ( c => /* filter logic */ )
. OrderBy ( c => _sortOrder switch
{
"priceLow" => c . Tour . FromPrice ?? decimal . MaxValue ,
"priceHigh" => - ( c . Tour . FromPrice ?? 0m ),
_ => 0m
});
Sorting occurs in-memory on the server. For large datasets (>1000 tours), consider implementing database-level sorting.
Like/Favorite System
Users can save tours to their favorites:
private async Task ToggleLikeAsync ( TourCardViewModel card )
{
if ( ! Session . IsAuthenticated || Session . Current is null )
{
await ShowLikeToastAsync ( "Inicia sesión para dar like." , card . Tour . TourId );
return ;
}
_togglingLikes . Add ( card . Tour . TourId );
try
{
if ( IsLiked ( card . Tour . TourId ))
{
await PostLikeService . RemoveLikeAsync ( Session . Current . UserId , card . Tour . TourId );
_likedTours . Remove ( card . Tour . TourId );
}
else
{
await PostLikeService . AddLikeAsync ( Session . Current . UserId , card . Tour . TourId );
_likedTours . Add ( card . Tour . TourId );
}
}
finally
{
_togglingLikes . Remove ( card . Tour . TourId );
await InvokeAsync ( StateHasChanged );
}
}
Mobile Experience
The marketplace adapts for mobile devices with a simplified card layout:
@media ( max-width : 640 px ) {
.mp-hero { padding : 30 px 0 24 px ; }
.mp-grid {
grid-template-columns : 1 fr 1 fr ;
gap : 14 px ;
}
}
@media ( max-width : 420 px ) {
.mp-grid { grid-template-columns : 1 fr ; }
}
The mobile layout uses a 2-column grid on tablets and single-column on phones for optimal touch interaction.
Tour Visibility Logic
Tours have sophisticated visibility controls:
var vis = await TourService . GetTourVisibilityAsync ( tour . TourId );
start = vis . StartDate ;
end = vis . EndDate ;
muestra = vis . MuestraInstantanea ;
if ( string . Equals ( vis . Status , "I" , StringComparison . OrdinalIgnoreCase ))
{
continue ; // Skip inactive tours
}
if ( start . HasValue )
{
var cutoff = end ?? start ;
if ( now > cutoff )
{
continue ; // Skip expired tours
}
if ( ! muestra && start . Value > now . AddDays ( 3 ))
{
continue ; // Hide tours starting >3 days in future unless "muestra" flag is set
}
}
var isUpcoming = ! muestra && start . HasValue && start . Value > now ;
Best Practices
Performance
Implement stored procedures for complex queries
Use async/await throughout the data pipeline
Cache tour images with CDN
User Experience
Show loading states during data fetches
Preserve filter state in query parameters
Implement optimistic UI for likes
Data Integrity
Validate ticket availability before display
Handle concurrent like/unlike operations
Respect tour visibility windows
Accessibility
Use semantic HTML in card components
Provide alt text for all tour images
Ensure keyboard navigation works
Next Steps
Explore Search & Filters Learn how AndanDo’s advanced search helps users find the perfect tour