Skip to content

Feature/group locations#498

Merged
NielsPilgaard merged 9 commits into
mainfrom
feature/group-locations
Feb 6, 2026
Merged

Feature/group locations#498
NielsPilgaard merged 9 commits into
mainfrom
feature/group-locations

Conversation

@NielsPilgaard

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@NielsPilgaard has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Adds Leaflet MarkerCluster integration: new cluster assets and CSS, JS interop for clustered group markers and map state, C# interop and Blazor APIs to update/clear/fit group markers and persist map view, and extends GroupSlim and result cache with geolocation and map-state fields.

Changes

Cohort / File(s) Summary
App assets
src/web/Jordnaer/Components/App.razor
Includes MarkerCluster CSS/JS and local wwwroot/css/marker-cluster.css.
Client JS & styling
src/web/Jordnaer/wwwroot/js/leaflet-interop.js, src/web/Jordnaer/wwwroot/css/marker-cluster.css
Adds marker-cluster state and APIs (updateGroupMarkers, clearGroupMarkers, fitBoundsToMarkers, getMapState), cluster icon/popup builders and sanitizers, primaryColor usage, fitBounds flag, and disposes cluster on map teardown.
Map interop (C#)
src/web/Jordnaer/Features/Map/LeafletMapInterop.cs
Adds GroupMarkerData and MapViewState DTOs; new interop methods: UpdateGroupMarkersAsync, ClearGroupMarkersAsync, FitBoundsToMarkersAsync, GetMapStateAsync; UpdateSearchRadiusAsync gains fitBounds flag; adds logger.
Blazor map component
src/web/Jordnaer/Features/Map/LeafletMap.razor
Exposes async wrappers for new interop calls, GetMapStateAsync, FitBoundsToMarkersAsync, UpdateGroupMarkersAsync, ClearGroupMarkersAsync; forwards fitBounds in UpdateSearchRadiusAsync; logs warnings on failures.
Map UI/filter
src/web/Jordnaer/Features/Map/MapSearchFilter.razor
Adds Groups and SkipGeolocation parameters, caches previous groups, synchronizes markers on parameter changes, adds Update/Clear/Get/Restore map state methods, adjusts initial geolocation and zoom behavior, and forwards fitBounds.
Group search form
src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
Adds Groups parameter and FilterChanged callback; transforms GroupSlimGroupMarkerData; initializes/persists/restores map state across navigation; implements disposal and JS error guards.
Search page wiring
src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor
Injects IJSRuntime, binds _searchResult.Groups into GroupSearchForm, clears scroll position after searches, and resets cache state when empty.
Search projection & cache
src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs, src/web/Jordnaer/Features/GroupSearch/GroupSearchResultCache.cs
Extends GroupSlim with website/address/latitude/longitude and zipcode coords; adds MapState class and MapState property on GroupSearchResultCache.
Scroll helper
src/web/Jordnaer/wwwroot/js/scroll.js
Adds clearScrollPosition() to clear session scroll state and scroll to top.

Sequence Diagram

sequenceDiagram
    participant Search as GroupSearch.razor
    participant Form as GroupSearchForm
    participant Filter as MapSearchFilter
    participant LeafletMap as LeafletMap
    participant Interop as LeafletMapInterop
    participant JS as leaflet-interop.js

    Search->>Form: Bind _searchResult.Groups
    Form->>Form: Convert GroupSlim → GroupMarkerData[]
    Form->>Filter: Pass Groups parameter
    Filter->>LeafletMap: Call UpdateGroupMarkersAsync(groups)
    LeafletMap->>Interop: Call UpdateGroupMarkersAsync(mapId, groups)
    Interop->>JS: Call updateGroupMarkers(mapId, groups)
    JS->>JS: createGroupIcon & createGroupPopupContent for each group
    JS->>JS: Create/replace markerClusterGroup and add markers
    Filter->>LeafletMap: Call FitBoundsToMarkersAsync()
    LeafletMap->>Interop: Call FitBoundsToMarkersAsync(mapId, padding)
    Interop->>JS: Call fitBoundsToMarkers(mapId, padding)
    JS->>JS: Fit map bounds to cluster layer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the purpose, scope, and key changes of this feature implementation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feature/group locations' is directly related to the main changeset, which adds marker clustering and group location visualization to the map-based search feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/group-locations

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.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Implement group location markers with clustering on map

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add group marker clustering and visualization on map
• Implement custom marker icons with profile pictures
• Create interactive popups displaying group information
• Add marker cluster styling with size-based color coding
Diagram
flowchart LR
  A["GroupSearchForm"] -->|Groups parameter| B["MapSearchFilter"]
  B -->|UpdateGroupMarkersAsync| C["LeafletMap"]
  C -->|JS interop| D["leaflet-interop.js"]
  D -->|Creates markers| E["Leaflet.markercluster"]
  E -->|Renders with styling| F["marker-cluster.css"]
  D -->|Displays popups| G["Group Popups"]
Loading

Grey Divider

File Changes

1. src/web/Jordnaer/Features/Map/LeafletMapInterop.cs ✨ Enhancement +49/-0

Add group marker interop methods and data model

• Add GroupMarkerData record for transferring group information to JavaScript
• Add three new interop methods: UpdateGroupMarkersAsync, ClearGroupMarkersAsync,
 FitBoundsToMarkersAsync
• Implement methods to manage marker clustering and bounds fitting

src/web/Jordnaer/Features/Map/LeafletMapInterop.cs


2. src/web/Jordnaer/wwwroot/css/marker-cluster.css ✨ Enhancement +238/-0

Add marker cluster and popup styling

• Create comprehensive CSS styling for marker clusters with three size categories
• Define custom properties for Jordnaer theme colors and shadows
• Style group marker icons with profile pictures and initials
• Design popup content with avatar, title, location, description, and action link

src/web/Jordnaer/wwwroot/css/marker-cluster.css


3. src/web/Jordnaer/wwwroot/js/leaflet-interop.js ✨ Enhancement +270/-1

Implement group marker clustering and popup logic

• Add updateGroupMarkers function to create and manage marker clusters with custom icons
• Implement createGroupIcon and createGroupPopupContent for custom marker rendering
• Add clearGroupMarkers and fitBoundsToMarkers utility functions
• Include XSS prevention with escapeHtml and escapeAttribute helper methods
• Add marker cluster group initialization with configurable clustering options

src/web/Jordnaer/wwwroot/js/leaflet-interop.js


View more (5)
4. src/web/Jordnaer/Components/App.razor Dependencies +7/-0

Add marker cluster library dependencies

• Add Leaflet.markercluster CSS and JavaScript library references
• Include custom marker-cluster.css stylesheet for styling
• Load libraries with integrity checks for security

src/web/Jordnaer/Components/App.razor


5. src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor ✨ Enhancement +38/-1

Add group data parameter and conversion logic

• Add Groups parameter to receive group data for map display
• Implement OnParametersSet lifecycle to convert GroupSlim to GroupMarkerData
• Pass converted group markers to MapSearchFilter component
• Filter groups to only include those with valid coordinates

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor


6. src/web/Jordnaer/Features/Map/LeafletMap.razor ✨ Enhancement +39/-0

Add group marker management methods

• Add UpdateGroupMarkersAsync method to update markers on map
• Add ClearGroupMarkersAsync method to remove all group markers
• Add FitBoundsToMarkersAsync method to auto-fit map view to markers
• Include initialization checks before executing operations

src/web/Jordnaer/Features/Map/LeafletMap.razor


7. src/web/Jordnaer/Features/Map/MapSearchFilter.razor ✨ Enhancement +62/-0

Add group marker display and update handling

• Add Groups parameter to receive group marker data
• Implement OnParametersSetAsync to handle group updates and auto-fit map bounds
• Add public methods UpdateGroupMarkersAsync and ClearGroupMarkersAsync
• Push initial group markers after map initialization
• Track previous groups to detect changes and avoid unnecessary updates

src/web/Jordnaer/Features/Map/MapSearchFilter.razor


8. src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor ✨ Enhancement +1/-1

Pass groups to search form for map display

• Pass search result groups to GroupSearchForm component
• Enable group markers to display on map during search

src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor


Grey Divider

Qodo Logo

@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: 1

🤖 Fix all issues with AI agents
In `@src/web/Jordnaer/wwwroot/css/marker-cluster.css`:
- Around line 80-87: The inner div for large clusters is offset because the JS
creates an iconSize of 44×44 while .marker-cluster-large div is 42×42 but uses
negative margins; update the .marker-cluster-large div CSS (selector
.marker-cluster-large div) to use margin-left: 1px and margin-top: 1px (i.e.,
(44−42)/2) so the 42×42 inner circle is centered within the 44×44 icon;
alternatively ensure the inner div size matches iconSize or adjust both
consistently if you prefer a different final visual.
🧹 Nitpick comments (7)
src/web/Jordnaer/wwwroot/js/leaflet-interop.js (3)

9-10: primaryColor is defined but never referenced; color is still hardcoded.

primaryColor on Line 10 is unused — updateSearchRadius (Lines 97-98) still hardcodes '#594F8D'. Either use this.primaryColor in updateSearchRadius or remove the property.

♻️ Suggested fix
             mapInstance.circle = L.circle([lat, lng], {
-                color: '#594F8D',      // Primary color from Jordnaer theme
-                fillColor: '#594F8D',
+                color: this.primaryColor,
+                fillColor: this.primaryColor,
                 fillOpacity: 0.15,
                 radius: radiusMeters
             }).addTo(mapInstance.map);

Also applies to: 97-98


321-334: groupUrl is not escaped for HTML attribute context.

encodeURIComponent encodes " as %22 so this is safe in practice, but for defense-in-depth consistency with the rest of the file, consider running it through escapeAttribute:

-        const groupUrl = `/groups/${encodeURIComponent(group.name)}`;
+        const groupUrl = this.escapeAttribute(`/groups/${encodeURIComponent(group.name)}`);

386-450: Empty groups array still allocates a cluster group and adds it to the map.

When groups is null/empty, the code still creates a new L.markerClusterGroup, adds zero layers, and attaches it to the map (Line 450). This is harmless but wasteful — consider returning early after clearing the old cluster.

src/web/Jordnaer/wwwroot/css/marker-cluster.css (1)

6-28: CSS custom properties duplicate the primary color defined elsewhere.

The --jordnaer-primary: #594F8D`` and other theme colors here may already exist in app.css or other global stylesheets. If the primary color changes, multiple `:root` blocks must be updated.

Not blocking — just a maintenance note to consider centralizing these variables if they aren't already shared.

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

319-343: Groups (IEnumerable) is enumerated multiple times: .Any() + UpdateGroupMarkersAsync.

Line 325 calls Groups.Any() (first enumeration), and Line 337 passes Groups to UpdateGroupMarkersAsync (second enumeration for JSON serialization). This is safe today because the upstream GroupSearchForm materializes it as a List<GroupMarkerData>, but the IEnumerable<GroupMarkerData>? parameter type doesn't enforce that.

Consider accepting IReadOnlyList<GroupMarkerData>? instead, or materializing locally, to make the contract explicit and prevent future callers from passing deferred queries.


296-317: Public UpdateGroupMarkersAsync and ClearGroupMarkersAsync — note these bypass _previousGroups tracking.

Callers using these public methods directly won't update _previousGroups, so a subsequent OnParametersSetAsync cycle could re-push or clear markers unexpectedly if the Groups parameter state drifts from what was imperatively set. This is fine if the public methods are only used for initial setup (as in OnAfterRenderAsync), but worth being aware of if they're called from parent components.

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

137-141: Null-guard is inconsistent with the non-nullable parameter type.

The interface declares groups as IEnumerable<GroupMarkerData> (non-nullable), yet the implementation applies groups?.ToList() ?? [], silently converting a null into an empty list. This masks potential caller bugs. Consider either:

  • Making the parameter nullable (IEnumerable<GroupMarkerData>?) if null is a valid input, or
  • Dropping the null-conditional and just calling groups.ToList() to let NRE surface misuse.
Option: trust the non-nullable contract
 public async Task<bool> UpdateGroupMarkersAsync(string mapId, IEnumerable<GroupMarkerData> groups)
 {
-    var materialized = groups?.ToList() ?? [];
+    var materialized = groups.ToList();
     return await _jsRuntime.InvokeVoidAsyncWithErrorHandling(
         "leafletInterop.updateGroupMarkers", mapId, materialized);
 }

Comment thread src/web/Jordnaer/wwwroot/css/marker-cluster.css
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Jordnaer Community Website Feb 6, 2026
@qodo-code-review

qodo-code-review Bot commented Feb 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. LeafletMap ignores interop result 📘 Rule violation ⛯ Reliability
Suggestion Impact:LeafletMap wrapper methods were changed to return bool, capture the interop success flag, and log warnings when calls fail (and return false when not initialized), addressing the previously ignored Task results.

code diff:

# File: src/web/Jordnaer/Features/Map/LeafletMap.razor
@@ -1,5 +1,6 @@
 @implements IAsyncDisposable
 @inject ILeafletMapInterop LeafletMapInterop
+@inject ILogger<LeafletMap> Logger
 
 <div id="@MapId" style="@MapStyle"></div>
 
@@ -71,11 +72,11 @@
 	/// <summary>
 	/// Updates the search radius visualization on the map
 	/// </summary>
-	public async Task UpdateSearchRadiusAsync(double lat, double lng, int radiusKm)
+	public async Task UpdateSearchRadiusAsync(double lat, double lng, int radiusKm, bool fitBounds = false)
 	{
 		if (!_isInitialized) return;
 
-		await LeafletMapInterop.UpdateSearchRadiusAsync(MapId, lat, lng, radiusKm);
+		await LeafletMapInterop.UpdateSearchRadiusAsync(MapId, lat, lng, radiusKm, fitBounds);
 	}
 
 	/// <summary>
@@ -93,77 +94,102 @@
 	/// </summary>
 	public async Task UpdateMarkerAsync(double lat, double lng)
 	{
+		if (!_isInitialized) return;
+
+		await LeafletMapInterop.UpdateMarkerAsync(MapId, lat, lng);
+	}
+
+	/// <summary>
+	/// Removes the marker from the map
+	/// </summary>
+	public async Task RemoveMarkerAsync()
+	{
 		if (!_isInitialized)
 		{
 			return;
 		}
 
-		await LeafletMapInterop.UpdateMarkerAsync(MapId, lat, lng);
-	}
-
-	/// <summary>
-	/// Removes the marker from the map
-	/// </summary>
-	public async Task RemoveMarkerAsync()
+		await LeafletMapInterop.RemoveMarkerAsync(MapId);
+	}
+
+	/// <summary>
+	/// Removes the search radius circle from the map
+	/// </summary>
+	public async Task RemoveSearchRadiusAsync()
 	{
 		if (!_isInitialized)
 		{
 			return;
 		}
 
-		await LeafletMapInterop.RemoveMarkerAsync(MapId);
-	}
-
-	/// <summary>
-	/// Removes the search radius circle from the map
-	/// </summary>
-	public async Task RemoveSearchRadiusAsync()
-	{
-		if (!_isInitialized)
-		{
-			return;
-		}
-
 		await LeafletMapInterop.RemoveSearchRadiusAsync(MapId);
 	}
 
 	/// <summary>
 	/// Updates group markers on the map with clustering
 	/// </summary>
-	public async Task UpdateGroupMarkersAsync(IEnumerable<GroupMarkerData> groups)
-	{
-		if (!_isInitialized)
-		{
-			return;
-		}
-
-		await LeafletMapInterop.UpdateGroupMarkersAsync(MapId, groups);
+	public async Task<bool> UpdateGroupMarkersAsync(IEnumerable<GroupMarkerData> groups)
+	{
+		if (!_isInitialized)
+		{
+			return false;
+		}
+
+		var success = await LeafletMapInterop.UpdateGroupMarkersAsync(MapId, groups);
+		if (!success)
+		{
+			Logger.LogWarning("Failed to update group markers on map {MapId}", MapId);
+		}
+		return success;
 	}
 
 	/// <summary>
 	/// Clears all group markers from the map
 	/// </summary>
-	public async Task ClearGroupMarkersAsync()
-	{
-		if (!_isInitialized)
-		{
-			return;
-		}
-
-		await LeafletMapInterop.ClearGroupMarkersAsync(MapId);
+	public async Task<bool> ClearGroupMarkersAsync()
+	{
+		if (!_isInitialized)
+		{
+			return false;
+		}
+
+		var success = await LeafletMapInterop.ClearGroupMarkersAsync(MapId);
+		if (!success)
+		{
+			Logger.LogWarning("Failed to clear group markers on map {MapId}", MapId);
+		}
+		return success;
 	}
 
 	/// <summary>
 	/// Fits the map view to show all group markers
 	/// </summary>
-	public async Task FitBoundsToMarkersAsync(int padding = 50)
-	{
-		if (!_isInitialized)
-		{
-			return;
-		}
-
-		await LeafletMapInterop.FitBoundsToMarkersAsync(MapId, padding);
+	public async Task<bool> FitBoundsToMarkersAsync(int padding = 50)
+	{
+		if (!_isInitialized)
+		{
+			return false;
+		}
+
+		var success = await LeafletMapInterop.FitBoundsToMarkersAsync(MapId, padding);
+		if (!success)
+		{
+			Logger.LogWarning("Failed to fit bounds to markers on map {MapId}", MapId);
+		}
+		return success;
+	}
+
+	/// <summary>
+	/// Gets the current map view state (center and zoom)
+	/// </summary>
+	public async Task<MapViewState?> GetMapStateAsync()
+	{
+		if (!_isInitialized)
+		{
+			return null;
+		}
+
+		return await LeafletMapInterop.GetMapStateAsync(MapId);
 	}

Description
• The new LeafletMap methods await interop calls that return Task<bool> but discard the
  success/failure result.
• This creates a silent-failure path where marker updates/clears/bounds-fit can fail without any
  actionable context, making production debugging and graceful degradation harder.
• Callers currently have no way to react (retry, show fallback UI, or log with context) when the JS
  interop returns false.
Code

src/web/Jordnaer/Features/Map/LeafletMap.razor[R133-167]

+	public async Task UpdateGroupMarkersAsync(IEnumerable<GroupMarkerData> groups)
+	{
+		if (!_isInitialized)
+		{
+			return;
+		}
+
+		await LeafletMapInterop.UpdateGroupMarkersAsync(MapId, groups);
+	}
+
+	/// <summary>
+	/// Clears all group markers from the map
+	/// </summary>
+	public async Task ClearGroupMarkersAsync()
+	{
+		if (!_isInitialized)
+		{
+			return;
+		}
+
+		await LeafletMapInterop.ClearGroupMarkersAsync(MapId);
+	}
+
+	/// <summary>
+	/// Fits the map view to show all group markers
+	/// </summary>
+	public async Task FitBoundsToMarkersAsync(int padding = 50)
+	{
+		if (!_isInitialized)
+		{
+			return;
+		}
+
+		await LeafletMapInterop.FitBoundsToMarkersAsync(MapId, padding);
+	}
Evidence
Compliance requires handling failure points with meaningful context. The interop contract explicitly
returns Task<bool> (a failure signal), but the new Blazor wrapper methods await and ignore the
returned bool, which can lead to silent failures without any handling/logging.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/web/Jordnaer/Features/Map/LeafletMapInterop.cs[51-65]
src/web/Jordnaer/Features/Map/LeafletMap.razor[133-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`LeafletMap` awaits JS interop calls that return `Task&lt;bool&gt;` but discards the returned success flag. This can create silent failures (no retry/log/fallback) when the JS side returns `false`.

## Issue Context
`ILeafletMapInterop` methods (`UpdateGroupMarkersAsync`, `ClearGroupMarkersAsync`, `FitBoundsToMarkersAsync`) are designed to signal success/failure via `bool`. The wrapper methods in `LeafletMap.razor` should either:
- Return a `bool` to the caller, or
- Throw/handle failures with actionable context (and ideally log internally), and/or
- Provide a UI-safe fallback behavior.

## Fix Focus Areas
- src/web/Jordnaer/Features/Map/LeafletMapInterop.cs[51-65]
- src/web/Jordnaer/Features/Map/LeafletMap.razor[133-167]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Markers always filtered 🐞 Bug ✓ Correctness
Suggestion Impact:GroupSearchService now projects Latitude/Longitude and ZipCodeLatitude/ZipCodeLongitude from group.Location and group.ZipCodeLocation into GroupSlim, enabling GroupSearchForm’s marker filtering to keep groups with coordinates. The form’s marker mapping was also extended to include WebsiteUrl.

code diff:

# File: src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
@@ -46,11 +46,17 @@
 							  .Select(group => new GroupSlim
 							  {
 								  ProfilePictureUrl = group.ProfilePictureUrl,
+								  WebsiteUrl = group.WebsiteUrl,
 								  Name = group.Name,
 								  ShortDescription = group.ShortDescription,
 								  Description = group.Description,
+								  Address = group.Address,
 								  ZipCode = group.ZipCode,
 								  City = group.City,
+								  Latitude = group.Location != null ? group.Location.Y : null,
+								  Longitude = group.Location != null ? group.Location.X : null,
+								  ZipCodeLatitude = group.ZipCodeLocation != null ? group.ZipCodeLocation.Y : null,
+								  ZipCodeLongitude = group.ZipCodeLocation != null ? group.ZipCodeLocation.X : null,
 								  Categories = group.Categories.Select(category => category.Name).ToArray(),
 								  MemberCount = group.Memberships.Count(e => e.MembershipStatus == MembershipStatus.Active),
 								  Id = group.Id

# File: src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor
@@ -113,6 +113,7 @@
 				Id = g.Id,
 				Name = g.Name,
 				ProfilePictureUrl = g.ProfilePictureUrl,
+				WebsiteUrl = g.WebsiteUrl,
 				ShortDescription = g.ShortDescription,
 				ZipCode = g.ZipCode,
 				City = g.City,

Description
GroupSearchForm only creates marker DTOs for groups with Latitude/Longitude, otherwise they
  are dropped.
• GroupSearchService’s GroupSlim projection never sets Latitude/Longitude (or zip-center
  coords), so map markers will never appear even when searches return groups.
• This makes the new clustering feature effectively non-functional for the group discovery page.
Code

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[R108-122]

+		// Convert GroupSlim to GroupMarkerData for the map and materialize
+		_groupMarkers = Groups?
+			.Where(g => g.Latitude.HasValue && g.Longitude.HasValue)
+			.Select(g => new GroupMarkerData
+			{
+				Id = g.Id,
+				Name = g.Name,
+				ProfilePictureUrl = g.ProfilePictureUrl,
+				ShortDescription = g.ShortDescription,
+				ZipCode = g.ZipCode,
+				City = g.City,
+				Latitude = g.Latitude!.Value,
+				Longitude = g.Longitude!.Value
+			})
+			.ToList();
Evidence
The PR’s marker generation filters on GroupSlim.Latitude/Longitude, but the group search query
that produces GroupSlim objects doesn’t populate those fields, leaving them null and thus
filtering out all groups.

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[100-125]
src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs[40-61]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GroupSearchForm` builds map markers only when `GroupSlim.Latitude/Longitude` are present. `GroupSearchService` currently doesn’t project any of the coordinate fields into `GroupSlim`, so all groups are filtered out and the new map marker/clustering feature won’t show anything.

### Issue Context
`GroupSlim` already has `Latitude/Longitude` and `ZipCodeLatitude/ZipCodeLongitude`. `GroupService` demonstrates how these are mapped from `Group.Location` and `Group.ZipCodeLocation`.

### Fix Focus Areas
- src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs[40-61]
- src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[108-122]
- src/web/Jordnaer/Features/Groups/GroupService.cs[45-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. console.error logs raw error 📘 Rule violation ⛨ Security
Description
• The new JS interop functions log raw error objects to the browser console via `console.error(...,
  error)`.
• This can expose internal stack traces/implementation details to end-users (browser-accessible
  logs) and produces unstructured logs that are harder to audit/monitor.
Code

src/web/Jordnaer/wwwroot/js/leaflet-interop.js[R452-456]

+            return true;
+        } catch (error) {
+            console.error('Error updating group markers:', error);
+            return false;
+        }
Evidence
The compliance checklist requires secure error handling (avoid exposing internal details to
end-users) and secure logging practices (avoid leaking sensitive details and keep logs
useful/structured). The added code logs the raw error object directly to the client console, which
is user-accessible and may include stack traces/internal details.

Rule 4: Generic: Secure Error Handling
Rule 5: Generic: Secure Logging Practices
src/web/Jordnaer/wwwroot/js/leaflet-interop.js[452-456]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Client-side code logs raw error objects to `console.error`, which can expose internal details (stack traces) to end-users and is not structured for monitoring.

## Issue Context
The JS interop layer returns `false` on failures and currently uses `console.error(&#x27;...&#x27;, error)`.

## Fix Focus Areas
- src/web/Jordnaer/wwwroot/js/leaflet-interop.js[452-456]
- src/web/Jordnaer/wwwroot/js/leaflet-interop.js[512-515]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. p variable is unclear 📘 Rule violation ✓ Correctness
Description
• In fitBoundsToMarkers, the variable name p does not clearly communicate meaning (it represents
  padding in pixels).
• This reduces readability and violates the self-documenting naming requirement.
Code

src/web/Jordnaer/wwwroot/js/leaflet-interop.js[R504-507]

+                const p = padding ?? 50;
+                mapInstance.map.fitBounds(bounds, {
+                    padding: [p, p],
+                    maxZoom: 15
Evidence
The naming rule requires identifiers to clearly express purpose and intent and discourages
non-conventional single-letter variables. The added code introduces const p = padding ?? 50;,
where p is not self-describing.

Rule 2: Generic: Meaningful Naming and Self-Documenting Code
src/web/Jordnaer/wwwroot/js/leaflet-interop.js[504-507]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The single-letter variable `p` is not self-documenting and makes the code harder to read/maintain.

## Issue Context
`p` represents the padding (pixels) used in `fitBounds`.

## Fix Focus Areas
- src/web/Jordnaer/wwwroot/js/leaflet-interop.js[504-507]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. No initial bounds fit 🐞 Bug ⛯ Reliability
Suggestion Impact:The commit changed the same marker-update block but removed the FitBoundsToMarkersAsync call that would fit the map to markers, rather than adding/ensuring an initial bounds fit as suggested.

code diff:

@@ -335,9 +335,6 @@
             {
                 _previousGroups = Groups;
                 await _mapComponent.UpdateGroupMarkersAsync(Groups);
-
-                // Automatically fit the map to show all markers
-                await _mapComponent.FitBoundsToMarkersAsync();
             }
         }

Description
• When the map initializes, MapSearchFilter pushes initial Groups markers but does not call
  FitBoundsToMarkersAsync.
• If the user returns to the page with cached search results (or results are already present on
  first render), markers may be off-screen because the map is centered by geolocation/default instead.
• Bounds fitting currently only happens in OnParametersSetAsync when the Groups parameter
  changes after initialization, which may not occur in the cached-results path.
Code

src/web/Jordnaer/Features/Map/MapSearchFilter.razor[R108-113]

+                // Push initial group markers after map is initialized
+                if (Groups is not null)
+                {
+                    _previousGroups = Groups;
+                    await _mapComponent.UpdateGroupMarkersAsync(Groups);
+                }
Evidence
The page explicitly restores cached search results on initialization and passes them into
GroupSearchForm/MapSearchFilter. The map’s first-render path updates markers but never fits
bounds; the only fit call is in OnParametersSetAsync, which is gated behind map initialization and
change detection.

src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor[47-55]
src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor[12-16]
src/web/Jordnaer/Features/Map/MapSearchFilter.razor[90-115]
src/web/Jordnaer/Features/Map/MapSearchFilter.razor[319-341]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
On first render, `MapSearchFilter` updates markers but doesn’t fit the map to their bounds. When returning with cached search results, the map may remain centered on the user/default location and markers can be off-screen.

### Issue Context
Bounds fitting is currently only triggered from `OnParametersSetAsync` when `Groups` changes after the map is initialized.

### Fix Focus Areas
- src/web/Jordnaer/Features/Map/MapSearchFilter.razor[90-115]
- src/web/Jordnaer/Features/Map/MapSearchFilter.razor[319-341]
- src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor[47-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
6. Fragile marker updates 🐞 Bug ⛯ Reliability
Suggestion Impact:The commit materializes the Groups IEnumerable once (Groups?.ToList()) and replaces `!Groups.Any()` with a `Count == 0` check, avoiding repeated/expensive enumeration of a potentially non-materialized sequence; it also passes the materialized list to UpdateGroupMarkersAsync. However, it still relies on reference inequality (Groups != _previousGroups) for change detection.

code diff:

     protected override async Task OnParametersSetAsync()
     {
         // Update group markers only when Groups parameter actually changes
         if (_mapComponent?.IsInitialized == true)
         {
+            // Materialize Groups once to avoid multiple enumeration
+            var groupsList = Groups?.ToList();
+
             // Handle when groups are cleared (set to null or empty)
-            if (Groups is null || !Groups.Any())
+            if (groupsList is null || groupsList.Count == 0)
             {
                 if (_previousGroups is not null)
                 {
@@ -334,10 +402,7 @@
             else if (Groups != _previousGroups)
             {
                 _previousGroups = Groups;
-                await _mapComponent.UpdateGroupMarkersAsync(Groups);
-
-                // Automatically fit the map to show all markers
-                await _mapComponent.FitBoundsToMarkersAsync();
+                await _mapComponent.UpdateGroupMarkersAsync(groupsList);
             }
         }
     }

Description
• Marker recalculation and map updates rely on reference equality (ReferenceEquals / `Groups !=
  _previousGroups`), so in-place list mutations won’t be detected and markers can become stale.
• MapSearchFilter uses !Groups.Any() which will enumerate an arbitrary IEnumerable; this can
  be unexpectedly expensive or problematic if a non-materialized sequence is ever passed.
• These are latent issues today (current call-site passes lists), but the parameter types are
  IEnumerable<...> and invite future misuse.
Code

src/web/Jordnaer/Features/Map/MapSearchFilter.razor[R319-341]

+    protected override async Task OnParametersSetAsync()
+    {
+        // Update group markers only when Groups parameter actually changes
+        if (_mapComponent?.IsInitialized == true)
+        {
+            // Handle when groups are cleared (set to null or empty)
+            if (Groups is null || !Groups.Any())
+            {
+                if (_previousGroups is not null)
+                {
+                    _previousGroups = null;
+                    await _mapComponent.ClearGroupMarkersAsync();
+                }
+            }
+            // Handle when groups are updated
+            else if (Groups != _previousGroups)
+            {
+                _previousGroups = Groups;
+                await _mapComponent.UpdateGroupMarkersAsync(Groups);
+
+                // Automatically fit the map to show all markers
+                await _mapComponent.FitBoundsToMarkersAsync();
+            }
Evidence
The current implementation uses reference equality to decide whether to do work, and calls .Any()
on a general IEnumerable. This pattern is sensitive to how the parent constructs and updates the
sequences (new instance vs in-place mutation; materialized vs streaming).

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[100-106]
src/web/Jordnaer/Features/Map/MapSearchFilter.razor[319-341]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Marker update logic relies on reference equality and enumerates `IEnumerable` via `.Any()`. This can miss in-place mutations and can be unexpectedly expensive for non-materialized enumerables.

### Issue Context
Current call sites pass lists, but the public component API accepts `IEnumerable`, so it’s easy for future callers to pass a query/iterator.

### Fix Focus Areas
- src/web/Jordnaer/Features/Map/MapSearchFilter.razor[319-341]
- src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[100-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Possible location privacy leak 🐞 Bug ⛨ Security
Suggestion Impact:The commit updated GroupSearchService to also map ZipCodeLatitude/ZipCodeLongitude (in addition to Latitude/Longitude), enabling use of zip-center coordinates, but it did not change marker construction to stop using exact coordinates or add access gating.

code diff:

# File: src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
@@ -46,11 +46,17 @@
 							  .Select(group => new GroupSlim
 							  {
 								  ProfilePictureUrl = group.ProfilePictureUrl,
+								  WebsiteUrl = group.WebsiteUrl,
 								  Name = group.Name,
 								  ShortDescription = group.ShortDescription,
 								  Description = group.Description,
+								  Address = group.Address,
 								  ZipCode = group.ZipCode,
 								  City = group.City,
+								  Latitude = group.Location != null ? group.Location.Y : null,
+								  Longitude = group.Location != null ? group.Location.X : null,
+								  ZipCodeLatitude = group.ZipCodeLocation != null ? group.ZipCodeLocation.Y : null,
+								  ZipCodeLongitude = group.ZipCodeLocation != null ? group.ZipCodeLocation.X : null,
 								  Categories = group.Categories.Select(category => category.Name).ToArray(),

Description
GroupSlim explicitly models both exact coordinates (Latitude/Longitude) and zip-center
  coordinates intended for non-members (ZipCodeLatitude/ZipCodeLongitude).
• The new marker rendering uses Latitude/Longitude directly, which could expose precise group
  locations on a public discovery map (depending on how groups populate Location).
• Consider using zip-center coordinates (or applying an access check) for non-members and only using
  exact coordinates where appropriate.
Code

src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[R109-121]

+		_groupMarkers = Groups?
+			.Where(g => g.Latitude.HasValue && g.Longitude.HasValue)
+			.Select(g => new GroupMarkerData
+			{
+				Id = g.Id,
+				Name = g.Name,
+				ProfilePictureUrl = g.ProfilePictureUrl,
+				ShortDescription = g.ShortDescription,
+				ZipCode = g.ZipCode,
+				City = g.City,
+				Latitude = g.Latitude!.Value,
+				Longitude = g.Longitude!.Value
+			})
Evidence
The DTO comments and existing mappings indicate that exact vs zip-center coordinates are
intentionally separate for privacy. The PR maps markers from the exact coordinate fields without any
membership/access gating.

src/shared/Jordnaer.Shared/Groups/GroupSlim.cs[20-38]
src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[109-121]
src/web/Jordnaer/Features/Groups/GroupService.cs[45-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Map markers are currently built from `GroupSlim.Latitude/Longitude`. `GroupSlim` also contains zip-center coordinates intended for non-members, which suggests exact coordinates may be sensitive.

### Issue Context
Discovery pages can be accessed without group membership. If `Latitude/Longitude` represent exact address-derived points, this could leak private location data.

### Fix Focus Areas
- src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor[108-122]
- src/shared/Jordnaer.Shared/Groups/GroupSlim.cs[20-38]
- src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs[40-61]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread src/web/Jordnaer/Features/Map/LeafletMap.razor Outdated
Comment thread src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor

@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: 1

🤖 Fix all issues with AI agents
In `@src/web/Jordnaer/wwwroot/js/leaflet-interop.js`:
- Around line 316-327: The group.websiteUrl is currently inserted into
websiteHtml using this.escapeAttribute/escapeHtml but that doesn't block
javascript: or other dangerous schemes; update the popup-building logic that
sets websiteHtml to first validate/parsel the URL (using the URL constructor or
equivalent) and only render the anchor if the parsed protocol is "http:" or
"https:" (otherwise skip rendering the link or render as plain text). Locate the
block that reads group.websiteUrl and the use of
this.escapeAttribute/this.escapeHtml and add a guard that checks new
URL(group.websiteUrl).protocol for "http:" or "https:" before including the <a
href=...> element.
🧹 Nitpick comments (2)
src/web/Jordnaer/Features/Map/MapSearchFilter.razor (1)

319-340: Potential multiple enumeration of Groups (IEnumerable).

Line 325 calls Groups.Any(), and if the branch on line 337 is taken, Groups is enumerated again inside UpdateGroupMarkersAsync (which calls .ToList() in the interop layer). Since the upstream caller (GroupSearchForm) materializes to a List<GroupMarkerData>, this is safe today, but the parameter type IEnumerable<GroupMarkerData>? doesn't enforce that.

A lightweight guard would be to check _previousGroups first to short-circuit more cheaply:

Suggested reorder
     if (_mapComponent?.IsInitialized == true)
     {
-        if (Groups is null || !Groups.Any())
+        if (Groups is null or [])
         {
             if (_previousGroups is not null)
             {
                 _previousGroups = null;
                 await _mapComponent.ClearGroupMarkersAsync();
             }
         }
-        else if (Groups != _previousGroups)
+        else if (!ReferenceEquals(Groups, _previousGroups))
         {
             _previousGroups = Groups;
             await _mapComponent.UpdateGroupMarkersAsync(Groups);
         }
     }

Using ReferenceEquals makes the intent explicit (matching GroupSearchForm.OnParametersSet), and list pattern []) avoids enumerating an arbitrary IEnumerable while still working on List<T>.

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

258-276: Same javascript: URI concern applies to profilePictureUrl in <img src="...">.

While <img src="javascript:..."> is not exploitable in modern browsers, data: URIs in src can render unexpected content. Consider applying the same https?:// scheme check to profilePictureUrl for defense in depth, or validate URLs server-side on save.

Comment thread src/web/Jordnaer/wwwroot/js/leaflet-interop.js Outdated

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/web/Jordnaer/Features/Map/MapSearchFilter.razor (1)

98-128: ⚠️ Potential issue | 🟠 Major

Remove the dead _skipGeolocation field and consolidate geolocation skip logic.

The private field _skipGeolocation is never set to true anywhere in the file, making the check !_skipGeolocation on line 115 always evaluate to true and thus dead code. The field is initialized to false (line 57), checked at line 115, and only reset to false at line 291. This overlaps with the SkipGeolocation parameter which already controls whether geolocation should be attempted. Remove the private field and rely solely on the SkipGeolocation parameter.

🤖 Fix all issues with AI agents
In `@src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor`:
- Around line 114-128: Replace the eval-based interop in
ClearScrollPositionAsync: instead of calling JsRuntime.InvokeVoidAsync("eval",
...) create a named JS function (e.g.
window.scrollHelper.clearGroupSearchScroll) that performs
sessionStorage.removeItem for 'groupSearch:scrollY' and
'groupSearch:visibleItemId' and window.scrollTo({ top: 0, behavior: 'instant'
}); then call it from ClearScrollPositionAsync using JsRuntime.InvokeVoidAsync
with the function identifier (e.g. "scrollHelper.clearGroupSearchScroll"); keep
the existing try/catch behavior for JS interop errors if desired.
🧹 Nitpick comments (4)
src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor (1)

57-63: Redundant else branch — fields already initialized at declaration.

_filter, _searchResult, and _hasSearched are already initialized to the same default values on lines 42–46. This else block is a no-op.

Proposed cleanup
         if (Cache.SearchFilter is not null && Cache.SearchResult is not null)
         {
             _filter = Cache.SearchFilter;
             _searchResult = Cache.SearchResult;
             _hasSearched = true;
         }
-        else
-        {
-            // Ensure clean state if cache is empty
-            _filter = new GroupSearchFilter();
-            _searchResult = new GroupSearchResult();
-            _hasSearched = false;
-        }
src/web/Jordnaer/Features/GroupSearch/GroupSearchForm.razor (1)

149-181: Task.Delay(100) is fragile for ensuring child component rendering.

The hardcoded 100ms delay on line 154 is a race condition — it may be insufficient on slow devices/connections and wasteful on fast ones. Consider using the OnMapInitialized callback from LeafletMap to signal readiness instead of guessing with a delay, or at minimum rely on the polling loop already present in MapSearchFilter.OnAfterRenderAsync.

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

383-404: Potential multiple enumeration of IEnumerable<GroupMarkerData>.

Line 389 calls Groups.Any() and line 401 passes Groups to UpdateGroupMarkersAsync, which could enumerate the sequence twice if the caller passes a lazy IEnumerable. Currently safe because GroupSearchForm materializes to a List, but this is fragile if other callers are added.

Defensive materialization

Consider accepting IReadOnlyList<GroupMarkerData>? for the Groups parameter, or materializing once in OnParametersSetAsync.


347-381: Map initialization polling loop is duplicated.

The while (_mapComponent?.IsInitialized != true && attempts < 20) pattern appears both in OnAfterRenderAsync (line 106) and RestoreMapStateAsync (line 362). Consider extracting a WaitForMapInitializationAsync() helper.

Comment thread src/web/Jordnaer/Pages/GroupSearch/GroupSearch.razor
@NielsPilgaard NielsPilgaard merged commit 45ae00b into main Feb 6, 2026
3 checks passed
@NielsPilgaard NielsPilgaard deleted the feature/group-locations branch February 6, 2026 22:59
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Jordnaer Community Website Feb 6, 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