Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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";
}
42 changes: 34 additions & 8 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,34 +44,41 @@ 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;
}
}

public virtual bool Equals(UserSearchFilter? other)
public virtual bool Equals(GroupSearchFilter? other)
{
return other is not null &&
Name == other.Name &&
((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.
}

file class RadiusRequiredAttribute : ValidationAttribute
{
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 userSearchFilter.WithinRadiusKilometers is null
return groupSearchFilter.WithinRadiusKilometers is null
? new ValidationResult("Radius skal vælges når et område er valgt.")
: ValidationResult.Success!;
}
Expand All @@ -68,14 +87,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
29 changes: 25 additions & 4 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 All @@ -27,7 +39,10 @@ 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!;
}
Expand All @@ -44,13 +59,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
30 changes: 26 additions & 4 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 All @@ -65,7 +81,10 @@ 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!;
}
Expand All @@ -81,13 +100,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 src="@Assets["js/leaflet-interop.js"]"></script>
</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
Loading