Skip to content

Feature/map search#475

Merged
NielsPilgaard merged 8 commits into
mainfrom
feature/map-search
Dec 23, 2025
Merged

Feature/map search#475
NielsPilgaard merged 8 commits into
mainfrom
feature/map-search

Conversation

@NielsPilgaard

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a feature-flagged map-based search: integrates Leaflet (JS/C# interop and Blazor components), adds nullable Latitude/Longitude to search filters and query-string parameters, updates backend search services to prefer coordinates, adds a FeatureFlags constant, and removes a startup LocationMigrationService.

Changes

Cohort / File(s) Summary
Feature flags & shared models
src/shared/Jordnaer.Shared/FeatureFlags.cs, src/shared/Jordnaer.Shared/.../GroupSearchFilter.cs, src/shared/Jordnaer.Shared/.../PostSearchFilter.cs, src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs
Added MapSearch constant; added nullable Latitude/Longitude to search filters; updated Equals/GetHashCode and validation attributes to accept coordinates as a valid location.
Map UI components
src/web/Jordnaer/Features/Map/LeafletMap.razor, src/web/Jordnaer/Features/Map/MapSearchFilter.razor, src/web/Jordnaer/Features/Map/LeafletMapInterop.cs
New LeafletMap component, MapSearchFilter UI, and ILeafletMapInterop + implementation for C#↔JS map operations and click callbacks.
JS interop
src/web/Jordnaer/wwwroot/js/leaflet-interop.js
New window.leafletInterop module implementing map init, centering, marker & radius management, click callbacks and cleanup with per-map state and error handling.
Search services / location resolution
src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs, src/web/Jordnaer/Features/PostSearch/PostSearchService.cs, src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs
Location resolution now prefers Latitude/Longitude (NetTopologySuite Point SRID 4326) and falls back to text lookup; removed requirement that Location string be present when radius specified.
Search forms & pages (UI wiring)
src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor, src/web/Jordnaer/Features/Posts/PostSearchForm.razor, src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor, src/web/Jordnaer/Pages/.../GroupSearch.razor, src/web/Jordnaer/Pages/.../UserSearch.razor, src/web/Jordnaer/Pages/.../Posts.razor
Feature-flag conditional rendering between MapSearchFilter and legacy zipcode UI; lifecycle hooks to initialize flag and map state; query-string support for Latitude/Longitude; handlers to sync filter model with map interactions.
DI, configuration, app assets
src/web/Jordnaer/Program.cs, src/web/Jordnaer/appsettings.json, src/web/Jordnaer/Components/App.razor
Registered ILeafletMapInterop in DI, added FeatureManagement:MapSearch: true, and injected Leaflet CSS/JS + leaflet-interop.js into the app shell.
Migration service removal
src/web/Jordnaer/Features/Profile/LocationMigrationService.cs, src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
Deleted LocationMigrationService and removed its hosted-service registration; startup migration logic removed.
Docs / tasks removal
tasks/05-map-search.md, tasks/06-improve-user-search-display.md
Removed/updated planning/task docs related to map rollout and user-search display changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SearchForm as Search Form (Blazor)
    participant MapFilter as MapSearchFilter (Blazor)
    participant LeafletMap as LeafletMap (Razor)
    participant Interop as LeafletMapInterop (C#)
    participant JS as leaflet-interop.js
    participant LocationSvc as LocationService
    participant SearchSvc as Search Service

    User->>SearchForm: Choose map search (feature flag)
    SearchForm->>MapFilter: Render / provide initial coords

    User->>MapFilter: Type address OR click map
    alt Address selected
        MapFilter->>LocationSvc: Resolve address -> lat/lng
        LocationSvc-->>MapFilter: lat/lng
        MapFilter->>LeafletMap: CenterMapAsync + UpdateMarkerAsync
    else Map clicked
        LeafletMap->>Interop: forward click event
        Interop->>JS: (registered callback)
        JS-->>Interop: return lat,lng
        Interop-->>LeafletMap: invoke .NET OnMapClicked(lat,lng)
        LeafletMap->>MapFilter: OnLocationSelected(lat,lng)
    end

    MapFilter->>LeafletMap: UpdateSearchRadiusAsync(lat,lng,radius)
    LeafletMap->>Interop: UpdateSearchRadiusAsync(...)
    Interop->>JS: updateSearchRadius(...)
    JS-->>Interop: success

    MapFilter->>SearchForm: Emit OnLocationSearchChanged(lat,lng,radius,address?)
    SearchForm->>SearchSvc: Execute search (coords preferred)
    SearchSvc->>SearchSvc: Build NetTopologySuite Point if coords
    SearchSvc->>SearchSvc: Apply distance filter and return results
    SearchSvc-->>SearchForm: Results
    SearchForm-->>User: Display results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feature/map search' is partially related to the changeset but lacks specificity; it describes the general feature area rather than the main change. Consider using a more descriptive title that highlights the primary change, such as 'Add map-based location search with Leaflet integration' or 'Implement map search feature for user and group search'.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to evaluate whether the description relates to the changeset. Add a pull request description that explains the purpose, scope, and key changes of this feature implementation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/map-search

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 342b6a8 and 695f81f.

📒 Files selected for processing (1)
  • src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to src/web/**/*.cs : Use IFeatureManager for feature flags
🧬 Code graph analysis (1)
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (2)
src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (2)
  • ValidationResult (80-95)
  • ValidationResult (99-115)
src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (2)
  • ValidationResult (38-53)
  • ValidationResult (58-77)
🔇 Additional comments (5)
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (5)

21-31: LGTM! Well-documented coordinate properties.

The nullable coordinate properties are appropriately typed, and the XML documentation clearly communicates their purpose and precedence over the Location string.


47-48: LGTM! Proper hash integration.

The coordinate properties are correctly incorporated into the hash calculation using the established pattern.


54-63: LGTM! Equality contract is now consistent.

The Equals method properly compares the coordinate properties, and the previous equality contract violation (PageNumber/PageSize in Equals but not in GetHashCode) has been resolved—both methods now correctly exclude pagination properties.


71-76: LGTM! Validation properly extended for coordinates.

The validation logic correctly checks for coordinate presence alongside the Location string, and the variable naming has been appropriately corrected to groupSearchFilter.


90-106: LGTM! Validation logic is consistent across filter types.

The location validation correctly requires both Latitude and Longitude to be set for coordinate-based location, and properly allows either the Location string or complete coordinates. This implementation mirrors the patterns in PostSearchFilter and UserSearchFilter, maintaining consistency across the codebase.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (1)

78-93: RadiusRequiredAttribute should consider lat/long coordinates.

The validation skips radius enforcement when Location is empty (line 84), but doesn't account for the new lat/long coordinates. If a user selects a location on the map (setting Latitude/Longitude), but doesn't set a radius, the validation incorrectly passes.

🔎 Proposed fix
 file class RadiusRequiredAttribute : ValidationAttribute
 {
 	protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
 	{
 		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 userSearchFilter.WithinRadiusKilometers is null
 			? new ValidationResult("Radius skal vælges når et område er valgt.")
 			: ValidationResult.Success!;
 	}
 }
src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (1)

36-51: RadiusRequiredAttribute should consider lat/long coordinates.

Same issue as in UserSearchFilter - the validation skips radius enforcement when Location is empty (line 42), but doesn't account for the new lat/long coordinates.

🔎 Proposed fix
 file class RadiusRequiredAttribute : ValidationAttribute
 {
 	protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
 	{
 		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 postSearchFilter.WithinRadiusKilometers is null
 				   ? new ValidationResult("Radius skal vælges når et område er valgt.")
 				   : ValidationResult.Success!;
 	}
 }
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (2)

54-64: Bug: Equals method has wrong parameter type.

The Equals method accepts UserSearchFilter? instead of GroupSearchFilter?. This breaks the equality contract for the GroupSearchFilter record type and will cause incorrect behavior when comparing GroupSearchFilter instances.

🔎 Proposed fix
-	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 &&
 			   Latitude == other.Latitude &&
-			   Longitude == other.Longitude;
+			   Longitude == other.Longitude &&
+			   PageNumber == other.PageNumber &&
+			   PageSize == other.PageSize;
 	}

67-81: RadiusRequiredAttribute should consider lat/long coordinates.

Same issue as in UserSearchFilter and PostSearchFilter - the validation at line 73 doesn't account for lat/long coordinates.

🔎 Proposed fix
 file class RadiusRequiredAttribute : ValidationAttribute
 {
 	protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
 	{
 		var userSearchFilter = (GroupSearchFilter)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 userSearchFilter.WithinRadiusKilometers is null
 			? new ValidationResult("Radius skal vælges når et område er valgt.")
 			: ValidationResult.Success!;
 	}
 }
🧹 Nitpick comments (9)
src/web/Jordnaer/Program.cs (1)

95-96: Consider extracting map service registration to a dedicated extension method.

Per project conventions, DI setup should be in WebApplicationBuilderExtensions (e.g., AddMapServices()) called from Program.cs, similar to other features like AddSearchServices(), AddGroupServices(), etc. This keeps Program.cs focused on orchestration.

Based on learnings, DI setup must be in WebApplicationBuilderExtensions.

🔎 Suggested approach

Create a new extension method in src/web/Jordnaer/Features/Map/WebApplicationBuilderExtensions.cs:

namespace Jordnaer.Features.Map;

public static class WebApplicationBuilderExtensions
{
    public static WebApplicationBuilder AddMapServices(this WebApplicationBuilder builder)
    {
        builder.Services.AddScoped<ILeafletMapInterop, LeafletMapInterop>();
        return builder;
    }
}

Then in Program.cs:

-builder.Services.AddScoped<ILeafletMapInterop, LeafletMapInterop>();
+builder.AddMapServices();
src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs (1)

10-54: LGTM! Implementation aligns with the group search pattern.

The location filtering logic correctly prioritizes map-based coordinates over text location lookup, and the coordinate ordering (Longitude, Latitude) is correct for NetTopologySuite's Point constructor.

Consider extracting the common location resolution logic (lines 24-45) into a shared helper, as noted in the TODO on line 12. The same pattern exists in QueryableGroupExtensions.ApplyLocationFilterAsync (see relevant snippet at lines 62-102), which could reduce duplication.

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor (1)

156-167: Consider resetting map state when clearing the filter.

When the map search is enabled, ClearFilter resets the filter but doesn't clear the map's visual state (marker and radius circle). This could leave stale visuals on the map.

🔎 Proposed enhancement
     private async Task ClearFilter()
     {
         Filter = new GroupSearchFilter();
         await FilterChanged.InvokeAsync(Filter);
 
         var uriWithQuery = new Uri(Navigation.Uri);
         var uriWithoutQuery = uriWithQuery.GetLeftPart(UriPartial.Path);
 
         _recentlyClearedForm = true;
 
+        // Clear map visuals if map search is enabled
+        if (_mapSearchEnabled && _mapSearchFilter is not null)
+        {
+            // Could add a ClearMapAsync method to MapSearchFilter
+        }
+
         await JsRuntime.NavigateTo(uriWithoutQuery);
     }
src/web/Jordnaer/Features/Map/MapSearchFilter.razor (1)

139-145: Avoid directly mutating [Parameter] properties.

Directly setting RadiusKm = newRadius (line 141) mutates a parameter, which can interfere with Blazor's change detection when the parent re-renders. The parent owns this value and passes it via the parameter.

🔎 Proposed fix - rely on callback only
     private async Task OnRadiusChanged(int newRadius)
     {
-        RadiusKm = newRadius;
         await RadiusKmChanged.InvokeAsync(newRadius);
 
         // The parent will handle updating the map through the public method
     }

The parent component should update RadiusKm when it receives the callback, which will flow back down through the parameter binding.

src/web/Jordnaer/wwwroot/js/leaflet-interop.js (1)

50-67: Consider storing dotNetHelper reference for explicit disposal.

The dotNetHelper object reference passed from C# isn't stored, which could be fine since the map click handler is cleaned up when disposeMap removes the map. However, for explicit cleanup of the .NET reference, consider storing and disposing it.

🔎 Proposed enhancement for explicit cleanup
     setupClickHandler: function (mapId, dotNetHelper) {
         try {
             const mapInstance = this.maps[mapId];
             if (!mapInstance) {
                 console.error('Map not found:', mapId);
                 return false;
             }
 
+            // Store reference for cleanup
+            mapInstance.dotNetHelper = dotNetHelper;
+
             mapInstance.map.on('click', function (e) {
                 dotNetHelper.invokeMethodAsync('OnMapClicked', e.latlng.lat, e.latlng.lng);
             });
 
             return true;
         } catch (error) {
             console.error('Error setting up click handler:', error);
             return false;
         }
     },

Then in disposeMap:

+            if (mapInstance.dotNetHelper) {
+                mapInstance.dotNetHelper.dispose();
+            }
             if (mapInstance.map) {
                 mapInstance.map.remove();
             }
src/web/Jordnaer/Features/Map/LeafletMap.razor (2)

29-49: Consider logging or notifying when map initialization fails.

When InitializeMapAsync returns false (Line 42), the component silently fails without any feedback. This could make debugging difficult if the map doesn't appear.

🔎 Suggested improvement
 		if (firstRender)
 		{
 			_dotNetHelper = DotNetObjectReference.Create(this);

 			// Initialize the map
 			var success = await LeafletMapInterop.InitializeMapAsync(
 				MapId,
 				InitialLatitude,
 				InitialLongitude,
 				InitialZoom);

 			if (success)
 			{
 				// Setup click handler
 				await LeafletMapInterop.SetupClickHandlerAsync(MapId, _dotNetHelper!);
 				_isInitialized = true;
 			}
+			else
+			{
+				// Consider logging or exposing initialization failure
+				Console.Error.WriteLine($"Failed to initialize Leaflet map: {MapId}");
+			}
 		}

110-118: Ensure _dotNetHelper is disposed even if JS interop fails.

If DisposeMapAsync throws (e.g., during circuit disconnection in Blazor Server), _dotNetHelper won't be disposed, causing a memory leak.

🔎 Proposed fix
 	public async ValueTask DisposeAsync()
 	{
-		if (_isInitialized)
-		{
-			await LeafletMapInterop.DisposeMapAsync(MapId);
-		}
-
-		_dotNetHelper?.Dispose();
+		try
+		{
+			if (_isInitialized)
+			{
+				await LeafletMapInterop.DisposeMapAsync(MapId);
+			}
+		}
+		finally
+		{
+			_dotNetHelper?.Dispose();
+		}
 	}
src/web/Jordnaer/Features/Map/LeafletMapInterop.cs (2)

1-2: Unused import: MudBlazor.

The MudBlazor namespace is imported but not used in this file. Consider removing it to keep imports clean.

🔎 Proposed fix
 using Microsoft.JSInterop;
-using MudBlazor;

74-81: Simplify by passing zoom directly.

The ternary can be avoided since the JS interop can handle null values directly.

🔎 Proposed fix
 	public async Task<bool> CenterMapAsync(string mapId, double lat, double lng, int? zoom = null)
 	{
-		return zoom.HasValue
-			? await _jsRuntime.InvokeVoidAsyncWithErrorHandling(
-				"leafletInterop.centerMap", mapId, lat, lng, zoom.Value)
-			: await _jsRuntime.InvokeVoidAsyncWithErrorHandling(
-			"leafletInterop.centerMap", mapId, lat, lng, null);
+		return await _jsRuntime.InvokeVoidAsyncWithErrorHandling(
+			"leafletInterop.centerMap", mapId, lat, lng, zoom);
 	}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f324b76 and f99d1d0.

📒 Files selected for processing (22)
  • src/shared/Jordnaer.Shared/FeatureFlags.cs
  • src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs
  • src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs
  • src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs
  • src/web/Jordnaer/Components/App.razor
  • src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
  • src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
  • src/web/Jordnaer/Features/Map/LeafletMap.razor
  • src/web/Jordnaer/Features/Map/LeafletMapInterop.cs
  • src/web/Jordnaer/Features/Map/MapSearchFilter.razor
  • src/web/Jordnaer/Features/PostSearch/PostSearchService.cs
  • src/web/Jordnaer/Features/Posts/PostSearchForm.razor
  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
  • src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs
  • src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor
  • src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor
  • src/web/Jordnaer/Pages/Posts/Posts.razor
  • src/web/Jordnaer/Pages/UserSearch/UserSearch.razor
  • src/web/Jordnaer/Program.cs
  • src/web/Jordnaer/appsettings.json
  • src/web/Jordnaer/wwwroot/js/leaflet-interop.js
💤 Files with no reviewable changes (2)
  • src/web/Jordnaer/Features/Profile/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Profile/LocationMigrationService.cs
🧰 Additional context used
📓 Path-based instructions (3)
src/web/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/web/**/*.cs: Always use 'await using var context = await contextFactory.CreateDbContextAsync()' for scoped DbContext access
Use AsNoTracking() for read-only queries in EF Core
Use OneOf<T, TError> for explicit error handling instead of exceptions or null returns
Use IHubContext for broadcasting SignalR messages to specific user groups
Use custom logger extensions like logger.LogFunctionBegan() and include diagnostic context with diagnosticContext.Set()
Publish messages using 'await publishEndpoint.Publish(/* ... */, cancellationToken)'
Use FluentValidation for input validation
Use IFeatureManager for feature flags
Use custom RateLimitExtensions for rate limiting
Apply NetEscapades.AspNetCore.SecurityHeaders for HTTP security headers
Use SixLabors.ImageSharp for image processing
Use Markdig for Markdown parsing
Use Azure Communication Email service for email sending
Use Azure Blob Storage for storing profile pictures and attachments
Use DSFAPI for Danish civil registry search functionality
Support OAuth authentication with Google, Microsoft, and Facebook providers

Files:

  • src/web/Jordnaer/Features/PostSearch/PostSearchService.cs
  • src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
  • src/web/Jordnaer/Program.cs
  • src/web/Jordnaer/Features/Map/LeafletMapInterop.cs
  • src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs
src/web/**/Program.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Expose health check endpoint at /health

Files:

  • src/web/Jordnaer/Program.cs
src/web/**/Components/**/*.razor

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Razor components should use MudBlazor v8.11.0 components for UI

Files:

  • src/web/Jordnaer/Components/App.razor
🧠 Learnings (5)
📚 Learning: 2025-12-20T19:25:14.348Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to src/web/**/*.cs : Use IFeatureManager for feature flags

Applied to files:

  • src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
  • src/web/Jordnaer/appsettings.json
  • src/web/Jordnaer/Program.cs
  • src/shared/Jordnaer.Shared/FeatureFlags.cs
  • src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor
📚 Learning: 2025-12-21T20:09:14.428Z
Learnt from: NielsPilgaard
Repo: NielsPilgaard/Jordnaer PR: 471
File: src/web/Jordnaer/wwwroot/css/app.css:396-487
Timestamp: 2025-12-21T20:09:14.428Z
Learning: In the Jordnaer project, custom JavaScript files are not available because the application uses Blazor Server (InteractiveServer) for most pages and Blazor Static SSR for Account pages. Any interactive behavior must use Blazor's built-in onclick event handlers or pure CSS solutions.

Applied to files:

  • src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
  • src/web/Jordnaer/Components/App.razor
  • src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor
📚 Learning: 2025-12-20T19:25:14.348Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to src/web/**/Components/**/*.razor : Razor components should use MudBlazor v8.11.0 components for UI

Applied to files:

  • src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
  • src/web/Jordnaer/Features/Map/MapSearchFilter.razor
  • src/web/Jordnaer/Components/App.razor
  • src/web/Jordnaer/Features/Map/LeafletMap.razor
  • src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor
  • src/web/Jordnaer/Features/Posts/PostSearchForm.razor
📚 Learning: 2025-12-20T19:25:14.348Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to **/Features/**/WebApplicationBuilderExtensions.cs : DI setup must be in WebApplicationBuilderExtensions (named AddYourFeatureServices()) called from Program.cs

Applied to files:

  • src/web/Jordnaer/Program.cs
📚 Learning: 2025-12-20T19:25:14.348Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to src/web/**/*.cs : Use DSFAPI for Danish civil registry search functionality

Applied to files:

  • src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor
🧬 Code graph analysis (4)
src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (2)
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (2)
  • ValidationResult (69-81)
  • ValidationResult (85-104)
src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (2)
  • ValidationResult (38-50)
  • ValidationResult (55-74)
src/web/Jordnaer/Program.cs (1)
src/web/Jordnaer/Features/Map/LeafletMapInterop.cs (1)
  • LeafletMapInterop (52-106)
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (2)
src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (2)
  • ValidationResult (38-50)
  • ValidationResult (55-74)
src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (2)
  • ValidationResult (80-92)
  • ValidationResult (96-112)
src/web/Jordnaer/Features/UserSearch/QueryableUserProfileExtensions.cs (1)
src/web/Jordnaer/Features/Profile/LocationService.cs (4)
  • Task (16-18)
  • Task (26-28)
  • Task (37-76)
  • Task (78-117)
🔇 Additional comments (23)
src/shared/Jordnaer.Shared/FeatureFlags.cs (1)

10-10: LGTM!

The new MapSearch feature flag follows the existing naming convention and pattern. It's correctly defined as a constant string for use with IFeatureManager.

src/web/Jordnaer/Components/App.razor (1)

51-53: Verify CDN availability and consider fallback strategy.

Loading Leaflet from unpkg.com introduces a third-party dependency. If the CDN is unavailable, the map functionality will fail silently.

Consider whether a local fallback or self-hosted option is warranted for production reliability. This is optional but worth documenting the trade-off.

src/web/Jordnaer/appsettings.json (1)

11-12: Verify intentional default-enabled state for MapSearch.

The MapSearch feature flag is set to true by default, which differs from other feature flags that are false. This means all environments will have map-based search enabled unless overridden.

If this is intentional for full rollout, this is fine. Otherwise, consider starting with false and enabling via environment-specific configuration (e.g., appsettings.Development.json).

src/web/Jordnaer/Pages/UserSearch/UserSearch.razor (2)

53-56: LGTM!

The Latitude and Longitude query parameters follow the existing pattern and are correctly typed as nullable doubles for optional map-based coordinates.


126-127: Query string handling is consistent.

The latitude/longitude values are correctly propagated to/from the query string, maintaining consistency with the existing filter properties.

Also applies to: 153-154

src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor (1)

54-57: LGTM!

The Latitude/Longitude query parameter handling is consistent with UserSearch.razor and follows the established patterns for filter property propagation.

Also applies to: 121-122, 145-146

src/web/Jordnaer/Features/PostSearch/PostSearchService.cs (2)

18-18: LGTM - static GeometryFactory is appropriate.

The GeometryFactory with SRID 4326 (WGS84) is correctly configured and thread-safe for static usage.


70-96: Well-structured location resolution with clear priority.

The implementation correctly:

  • Prioritizes explicit lat/long coordinates from map search
  • Falls back to zip code lookup for backward compatibility
  • Uses correct NTS coordinate order (longitude, latitude) with a helpful comment
  • Handles null cases gracefully

The early return pattern keeps the code readable.

src/web/Jordnaer/Features/Posts/PostSearchForm.razor (3)

103-116: Potential timing issue with component reference on firstRender.

On firstRender, the _mapSearchFilter component reference may not yet be populated because Blazor sets @ref values after the component tree is rendered. The await _mapSearchFilter.SetLocationAsync(...) call at line 110 might work due to the sequence of lifecycle events, but this timing can be fragile.

Consider moving this initialization to a later lifecycle point or using a flag to trigger it after the component is confirmed rendered:

🔎 Alternative approach
 protected override async Task OnAfterRenderAsync(bool firstRender)
 {
-    if (firstRender && _mapSearchEnabled && _mapSearchFilter is not null)
+    if (_mapSearchEnabled && _mapSearchFilter is not null && !_mapLocationInitialized)
     {
         if (Filter.Latitude.HasValue && Filter.Longitude.HasValue && Filter.WithinRadiusKilometers.HasValue)
         {
             await _mapSearchFilter.SetLocationAsync(
                 Filter.Latitude.Value,
                 Filter.Longitude.Value,
                 13);
+            _mapLocationInitialized = true;
         }
     }
 }

Add a field: private bool _mapLocationInitialized = false;


23-50: Feature-flagged UI toggle is well-implemented.

The conditional rendering between MapSearchFilter and the legacy ZipCodeAutoComplete is cleanly implemented using IFeatureManager, following the project's feature flag patterns.


118-142: Event handlers correctly propagate map changes to filter.

The OnLocationSearchChanged and OnRadiusChanged handlers properly update the filter properties and invoke FilterChanged to notify parent components.

src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (1)

20-31: LGTM! New coordinate properties properly integrated.

The Latitude/Longitude properties are well-documented and correctly integrated into GetHashCode and Equals.

src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs (1)

20-103: LGTM! Correctly follows coding guidelines and patterns.

The implementation properly uses await using for scoped DbContext access, AsNoTracking() for read-only queries, and maintains consistency with the user search location filtering pattern.

src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (1)

20-31: LGTM! Coordinate properties added consistently.

The new Latitude/Longitude properties follow the same pattern as UserSearchFilter and GroupSearchFilter.

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor (1)

110-128: LGTM! Feature flag integration follows guidelines.

The component correctly uses IFeatureManager to check the MapSearch feature flag and conditionally initializes the map location from query parameters.

src/web/Jordnaer/Features/Map/MapSearchFilter.razor (2)

34-61: LGTM! Component parameters are well-defined.

The public API with parameters for map configuration, radius, and event callbacks provides good flexibility for parent components.


66-78: LGTM! Address search handles errors gracefully.

The method returns an empty collection on failure rather than throwing, providing good UX with the autocomplete component.

src/web/Jordnaer/wwwroot/js/leaflet-interop.js (2)

17-42: LGTM! Map initialization properly handles existing instances.

The cleanup logic (lines 19-21) prevents memory leaks when re-initializing a map with the same ID.


224-247: LGTM! Cleanup logic is thorough.

The disposeMap function properly removes all map elements (circle, marker, map) and cleans up the internal state.

src/web/Jordnaer/Features/UserSearch/UserSearchForm.razor (2)

169-182: Potential timing issue: _mapSearchFilter may not be initialized on first render.

On firstRender, the MapSearchFilter component reference (_mapSearchFilter) is assigned during the render phase, but OnAfterRenderAsync runs after render completes. However, the @ref assignment happens during render, so this should work. The null check provides safety.

The logic looks correct, but consider that the map component itself may not be fully initialized yet since LeafletMap.OnAfterRenderAsync also runs on first render.

Verify that the map is ready to accept SetLocationAsync calls by checking if there's any async initialization race between this component and the nested LeafletMap.


22-44: LGTM! Feature flag integration is well-implemented.

The conditional rendering based on _mapSearchEnabled cleanly separates the new map-based search experience from the existing zip code search. This follows the coding guideline to use IFeatureManager for feature flags.

src/web/Jordnaer/Pages/Posts/Posts.razor (1)

126-157: LGTM! Query string parsing is well-structured.

The LoadFromQueryString method correctly leverages Blazor's [SupplyParameterFromQuery] binding and properly handles optional PageNumber and PageSize values.

src/web/Jordnaer/Features/Map/LeafletMapInterop.cs (1)

6-50: LGTM! Well-designed interface with clear documentation.

The interface is cleanly structured with comprehensive XML documentation. The Task<bool> return type provides a consistent pattern for callers to handle initialization/operation failures.

Comment thread src/web/Jordnaer/Components/App.razor Outdated
Comment thread src/web/Jordnaer/Pages/Posts/Posts.razor
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Jordnaer Community Website Dec 23, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between face8f1 and 342b6a8.

📒 Files selected for processing (5)
  • src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs
  • src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs
  • src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs
  • src/web/Jordnaer/Components/App.razor
  • tasks/06-improve-user-search-display.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/web/Jordnaer/Components/App.razor
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T19:25:14.348Z
Learning: Applies to src/web/**/*.cs : Use IFeatureManager for feature flags
🧬 Code graph analysis (2)
src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (2)
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (2)
  • ValidationResult (71-86)
  • ValidationResult (90-109)
src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (2)
  • ValidationResult (80-95)
  • ValidationResult (99-115)
src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (2)
src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (4)
  • Equals (62-75)
  • RadiusRequiredAttribute (78-96)
  • ValidationResult (80-95)
  • ValidationResult (99-115)
src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (3)
  • RadiusRequiredAttribute (36-54)
  • ValidationResult (38-53)
  • ValidationResult (58-77)
🔇 Additional comments (9)
tasks/06-improve-user-search-display.md (1)

8-8: > Likely an incorrect or invalid review comment.

src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs (2)

20-31: LGTM! Well-documented coordinate properties.

The XML documentation clearly explains the precedence behavior. The implementation is consistent with the corresponding properties in UserSearchFilter and GroupSearchFilter.


42-48: Validation logic correctly handles coordinates as an alternative location source.

The early return condition properly requires all location-related fields to be empty, and the hasLocation check correctly validates that either a Location string or both coordinates are provided. This is consistent across all search filter types.

Also applies to: 62-76

src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs (3)

20-31: LGTM! Consistent coordinate property additions.

Properties are well-documented and follow the same pattern as other filter types.


52-53: Hash and equality correctly updated for new properties.

The GetHashCode and Equals implementations properly include Latitude and Longitude, maintaining consistent behavior for the record type.

Also applies to: 70-71


84-95: Validation logic is correct.

The attribute implementations properly handle the new coordinate-based location input as an alternative to the Location string.

Also applies to: 103-114

src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs (3)

21-32: LGTM! Consistent coordinate property additions.

The properties are well-documented and follow the established pattern.


47-48: Hash correctly updated for new properties.

The GetHashCode implementation properly includes Latitude and Longitude.


73-86: Validation logic is correct and consistent.

The attribute implementations properly handle coordinate-based location input and match the pattern used in the other search filter types.

Also applies to: 92-109

Comment thread src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs
Comment thread tasks/06-improve-user-search-display.md
Comment thread tasks/06-improve-user-search-display.md
@NielsPilgaard NielsPilgaard merged commit ad790cf into main Dec 23, 2025
1 check passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Jordnaer Community Website Dec 23, 2025
@NielsPilgaard NielsPilgaard deleted the feature/map-search branch December 23, 2025 22:07
This was referenced Dec 24, 2025
This was referenced Feb 4, 2026
This was referenced Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant