Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/shared/Jordnaer.Shared/FeatureFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public static class FeatureFlags
public const string Posts = "Posts";
public const string AccountSettings = "AccountSettings";
public const string NotificationSettings = "NotificationSettings";
public const string MapSearch = "MapSearch";
}
31 changes: 27 additions & 4 deletions src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ public record GroupSearchFilter
[RadiusRequired]
public string? Location { get; set; }

/// <summary>
/// Latitude coordinate for map-based location search.
/// When set (along with Longitude), takes precedence over Location string.
/// </summary>
public double? Latitude { get; set; }

/// <summary>
/// Longitude coordinate for map-based location search.
/// When set (along with Latitude), takes precedence over Location string.
/// </summary>
public double? Longitude { get; set; }

public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;

Expand All @@ -32,6 +44,8 @@ public override int GetHashCode()
hash = hash * 23 + (Categories != null ? Categories.Aggregate(0, (current, category) => current + category.GetHashCode()) : 0);
hash = hash * 23 + WithinRadiusKilometers.GetHashCode();
hash = hash * 23 + (Location?.GetHashCode() ?? 0);
hash = hash * 23 + Latitude.GetHashCode();
hash = hash * 23 + Longitude.GetHashCode();

return hash;
}
Expand All @@ -44,7 +58,9 @@ public virtual bool Equals(UserSearchFilter? other)
((Categories == null && other.Categories == null) ||
(Categories != null && other.Categories != null && Categories.SequenceEqual(other.Categories))) &&
WithinRadiusKilometers == other.WithinRadiusKilometers &&
Location == other.Location;
Location == other.Location &&
Latitude == other.Latitude &&
Longitude == other.Longitude;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand All @@ -68,14 +84,21 @@ protected override ValidationResult IsValid(object? value, ValidationContext val
{
protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
{
var userSearchFilter = (GroupSearchFilter)validationContext.ObjectInstance;
var groupSearchFilter = (GroupSearchFilter)validationContext.ObjectInstance;

if (userSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(userSearchFilter.Location))
if (groupSearchFilter.WithinRadiusKilometers is null &&
string.IsNullOrEmpty(groupSearchFilter.Location) &&
!groupSearchFilter.Latitude.HasValue &&
!groupSearchFilter.Longitude.HasValue)
{
return ValidationResult.Success!;
}

return string.IsNullOrEmpty(userSearchFilter.Location)
// Valid if either Location string is set OR lat/long coordinates are set
var hasLocation = !string.IsNullOrEmpty(groupSearchFilter.Location) ||
(groupSearchFilter.Latitude.HasValue && groupSearchFilter.Longitude.HasValue);

return !hasLocation
? new ValidationResult("Område skal vælges når en radius er valgt.")
: ValidationResult.Success!;
}
Expand Down
24 changes: 21 additions & 3 deletions src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ public class PostSearchFilter
[RadiusRequired]
public string? Location { get; set; }

/// <summary>
/// Latitude coordinate for map-based location search.
/// When set (along with Longitude), takes precedence over Location string.
/// </summary>
public double? Latitude { get; set; }

/// <summary>
/// Longitude coordinate for map-based location search.
/// When set (along with Latitude), takes precedence over Location string.
/// </summary>
public double? Longitude { get; set; }

public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
}
Expand Down Expand Up @@ -44,13 +56,19 @@ protected override ValidationResult IsValid(object? value, ValidationContext val
{
var postSearchFilter = (PostSearchFilter)validationContext.ObjectInstance;

if (postSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(postSearchFilter.Location))
if (postSearchFilter.WithinRadiusKilometers is null &&
string.IsNullOrEmpty(postSearchFilter.Location) &&
!postSearchFilter.Latitude.HasValue &&
!postSearchFilter.Longitude.HasValue)
{
return ValidationResult.Success!;

}

return string.IsNullOrEmpty(postSearchFilter.Location)
// Valid if either Location string is set OR lat/long coordinates are set
var hasLocation = !string.IsNullOrEmpty(postSearchFilter.Location) ||
(postSearchFilter.Latitude.HasValue && postSearchFilter.Longitude.HasValue);

return !hasLocation
? new ValidationResult("Område skal vælges når en radius er valgt.")
: ValidationResult.Success!;
}
Expand Down
25 changes: 22 additions & 3 deletions src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ public record UserSearchFilter
[RadiusRequired]
public string? Location { get; set; }

/// <summary>
/// Latitude coordinate for map-based location search.
/// When set (along with Longitude), takes precedence over Location string.
/// </summary>
public double? Latitude { get; set; }

/// <summary>
/// Longitude coordinate for map-based location search.
/// When set (along with Latitude), takes precedence over Location string.
/// </summary>
public double? Longitude { get; set; }

[Range(0, 18, ErrorMessage = "Skal være mellem 0 og 18 år")]
public int? MinimumChildAge { get; set; }
[Range(0, 18, ErrorMessage = "Skal være mellem 0 og 18 år")]
Expand All @@ -37,6 +49,8 @@ public override int GetHashCode()
hash = hash * 23 + (Categories != null ? Categories.Aggregate(0, (current, category) => current + category.GetHashCode()) : 0);
hash = hash * 23 + WithinRadiusKilometers.GetHashCode();
hash = hash * 23 + (Location?.GetHashCode() ?? 0);
hash = hash * 23 + Latitude.GetHashCode();
hash = hash * 23 + Longitude.GetHashCode();
hash = hash * 23 + MinimumChildAge.GetHashCode();
hash = hash * 23 + MaximumChildAge.GetHashCode();
hash = hash * 23 + ChildGender.GetHashCode();
Expand All @@ -53,6 +67,8 @@ public virtual bool Equals(UserSearchFilter? other)
(Categories != null && other.Categories != null && Categories.SequenceEqual(other.Categories))) &&
WithinRadiusKilometers == other.WithinRadiusKilometers &&
Location == other.Location &&
Latitude == other.Latitude &&
Longitude == other.Longitude &&
MinimumChildAge == other.MinimumChildAge &&
MaximumChildAge == other.MaximumChildAge &&
ChildGender == other.ChildGender;
Expand Down Expand Up @@ -81,13 +97,16 @@ protected override ValidationResult IsValid(object? value, ValidationContext val
{
var userSearchFilter = (UserSearchFilter)validationContext.ObjectInstance;

if (userSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(userSearchFilter.Location))
if (userSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(userSearchFilter.Location) && !userSearchFilter.Latitude.HasValue && !userSearchFilter.Longitude.HasValue)
{
return ValidationResult.Success!;

}

return string.IsNullOrEmpty(userSearchFilter.Location)
// Valid if either Location string is set OR lat/long coordinates are set
var hasLocation = !string.IsNullOrEmpty(userSearchFilter.Location) ||
(userSearchFilter.Latitude.HasValue && userSearchFilter.Longitude.HasValue);

return !hasLocation
? new ValidationResult("Område skal vælges når en radius er valgt.")
: ValidationResult.Success!;
}
Expand Down
11 changes: 11 additions & 0 deletions src/web/Jordnaer/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
<link rel="stylesheet" href="@Assets["Jordnaer.styles.css"]" media="print"
onload="this.onload=null;this.removeAttribute('media');">

@* Leaflet.js for map search functionality *@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" />

<ImportMap />

<HeadOutlet @rendermode="RenderModeForPage" />
Expand All @@ -71,6 +76,12 @@
<script async src="@Assets["js/utilities.js"]"></script>
<script async src="@Assets["js/quill-loader.js"]"></script>
<script async src="@Assets["_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"]"></script>

@* Leaflet.js for map search functionality *@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script async src="@Assets["js/leaflet-interop.js"]"></script>
Comment thread
NielsPilgaard marked this conversation as resolved.
Outdated
</body>

</html>
Expand Down
94 changes: 81 additions & 13 deletions src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
@using Jordnaer.Features.Map
@using Microsoft.FeatureManagement
@using MudBlazor
@using Variant = MudBlazor.Variant
@inject NavigationManager Navigation
@inject IJSRuntime JsRuntime
@inject IFeatureManager FeatureManager

<MudContainer MaxWidth="MaxWidth.Small">

Expand All @@ -24,19 +29,33 @@

<MudGrid Justify="Justify.SpaceAround" Spacing="6">

<MudItem xs="8">
<ZipCodeAutoComplete For="() => Filter.Location"
Location="@Filter.Location"
LocationChanged="LocationChanged"
DisableSmartCompletion="_disableSmartCompletionForZipCode"/>
</MudItem>
<MudItem xs="4">
<MudNumericField For="() => Filter.WithinRadiusKilometers"
@bind-Value="Filter.WithinRadiusKilometers"
Label="km"
Placeholder="Radius">
</MudNumericField>
</MudItem>
@if (_mapSearchEnabled)
{
@* New map-based search experience *@
<MudItem xs="12">
<MapSearchFilter @ref="_mapSearchFilter"
RadiusKm="@(Filter.WithinRadiusKilometers ?? 10)"
RadiusKmChanged="OnRadiusChanged"
OnLocationSearchChanged="OnLocationSearchChanged" />
</MudItem>
}
else
{
@* Existing zip code search *@
<MudItem xs="8">
<ZipCodeAutoComplete For="() => Filter.Location"
Location="@Filter.Location"
LocationChanged="LocationChanged"
DisableSmartCompletion="_disableSmartCompletionForZipCode"/>
</MudItem>
<MudItem xs="4">
<MudNumericField For="() => Filter.WithinRadiusKilometers"
@bind-Value="Filter.WithinRadiusKilometers"
Label="km"
Placeholder="Radius">
</MudNumericField>
</MudItem>
}

<MudItem xs="12">
<CategorySelector @bind-Categories="Filter.Categories"/>
Expand Down Expand Up @@ -71,6 +90,9 @@

@code
{
private MapSearchFilter? _mapSearchFilter;
private bool _mapSearchEnabled = false;

[Parameter]
public required GroupSearchFilter Filter { get; set; }

Expand All @@ -85,6 +107,52 @@
private bool _recentlyClearedForm = false;
private bool _disableSmartCompletionForZipCode => _recentlyClearedForm || Filter != DefaultFilter;

protected override async Task OnInitializedAsync()
{
_mapSearchEnabled = await FeatureManager.IsEnabledAsync(FeatureFlags.MapSearch);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && _mapSearchEnabled && _mapSearchFilter is not null)
{
// If we have lat/long in the filter from query string, set the map location
if (Filter.Latitude.HasValue && Filter.Longitude.HasValue && Filter.WithinRadiusKilometers.HasValue)
{
await _mapSearchFilter.SetLocationAsync(
Filter.Latitude.Value,
Filter.Longitude.Value,
13);
}
}
}

private async Task OnLocationSearchChanged((double Latitude, double Longitude, int RadiusKm, string? Address) locationSearch)
{
Filter.Latitude = locationSearch.Latitude;
Filter.Longitude = locationSearch.Longitude;
Filter.WithinRadiusKilometers = locationSearch.RadiusKm;
Filter.Location = locationSearch.Address;

await FilterChanged.InvokeAsync(Filter);
}

private async Task OnRadiusChanged(int newRadius)
{
Filter.WithinRadiusKilometers = newRadius;

// Update the map if we have a location
if (_mapSearchFilter is not null && Filter.Latitude.HasValue && Filter.Longitude.HasValue)
{
await _mapSearchFilter.UpdateSearchRadiusAsync(
Filter.Latitude.Value,
Filter.Longitude.Value,
newRadius);
}

await FilterChanged.InvokeAsync(Filter);
}

private async Task ClearFilter()
{
Filter = new GroupSearchFilter();
Expand Down
26 changes: 21 additions & 5 deletions src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Jordnaer.Features.Profile;
using Jordnaer.Shared;
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;

namespace Jordnaer.Features.GroupSearch;

Expand All @@ -16,6 +17,7 @@ public class GroupSearchService(
ILocationService locationService)
: IGroupSearchService
{
private static readonly GeometryFactory GeometryFactory = new(new PrecisionModel(), 4326);
public async Task<GroupSearchResult> GetGroupsAsync(GroupSearchFilter filter,
CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -63,20 +65,34 @@ public async Task<GroupSearchResult> GetGroupsAsync(GroupSearchFilter filter,
GroupSearchFilter filter,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(filter.Location) || filter.WithinRadiusKilometers is null)
if (filter.WithinRadiusKilometers is null)
{
return (groups, false);
}

// Get location from the search location
var searchLocation = await locationService.GetLocationFromZipCodeAsync(filter.Location, cancellationToken);
Point? location = null;

if (searchLocation is null)
// Prefer lat/long from map search if available
if (filter.Latitude.HasValue && filter.Longitude.HasValue)
{
// NetTopologySuite Point uses (longitude, latitude) order
location = GeometryFactory.CreatePoint(new Coordinate(filter.Longitude.Value, filter.Latitude.Value));
}
// Fall back to zip code/location string lookup for backward compatibility
else if (!string.IsNullOrEmpty(filter.Location))
{
var searchLocation = await locationService.GetLocationFromZipCodeAsync(filter.Location, cancellationToken);
if (searchLocation is null)
{
return (groups, false);
}
location = searchLocation.Location;
}
else
{
return (groups, false);
}

var location = searchLocation.Location;
var radiusMeters = filter.WithinRadiusKilometers.Value * 1000;

// Use SQL Server's built-in distance calculation with geography type
Expand Down
Loading