Skip to content

Commit dc3421d

Browse files
committed
make search scroll experience much better
1 parent 97c7871 commit dc3421d

File tree

7 files changed

+114
-26
lines changed

7 files changed

+114
-26
lines changed

src/shared/Jordnaer.Shared/Groups/GroupSearchFilter.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel.DataAnnotations;
2+
using System.Diagnostics.CodeAnalysis;
23

34
namespace Jordnaer.Shared;
45

@@ -19,6 +20,32 @@ public record GroupSearchFilter
1920

2021
public int PageNumber { get; set; } = 1;
2122
public int PageSize { get; set; } = 10;
23+
24+
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
25+
public override int GetHashCode()
26+
{
27+
unchecked // Allow arithmetic overflow, numbers will just "wrap around"
28+
{
29+
var hash = 17;
30+
31+
hash = hash * 23 + (Name?.GetHashCode() ?? 0);
32+
hash = hash * 23 + (Categories != null ? Categories.Aggregate(0, (current, category) => current + category.GetHashCode()) : 0);
33+
hash = hash * 23 + WithinRadiusKilometers.GetHashCode();
34+
hash = hash * 23 + (Location?.GetHashCode() ?? 0);
35+
36+
return hash;
37+
}
38+
}
39+
40+
public virtual bool Equals(UserSearchFilter? other)
41+
{
42+
return other is not null &&
43+
Name == other.Name &&
44+
((Categories == null && other.Categories == null) ||
45+
(Categories != null && other.Categories != null && Categories.SequenceEqual(other.Categories))) &&
46+
WithinRadiusKilometers == other.WithinRadiusKilometers &&
47+
Location == other.Location;
48+
}
2249
}
2350

2451
file class RadiusRequiredAttribute : ValidationAttribute
@@ -46,7 +73,6 @@ protected override ValidationResult IsValid(object? value, ValidationContext val
4673
if (userSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(userSearchFilter.Location))
4774
{
4875
return ValidationResult.Success!;
49-
5076
}
5177

5278
return string.IsNullOrEmpty(userSearchFilter.Location)

src/shared/Jordnaer.Shared/UserSearch/UserSearchFilter.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.ComponentModel.DataAnnotations;
2+
using System.Diagnostics.CodeAnalysis;
23

34
namespace Jordnaer.Shared;
45
public record UserSearchFilter
@@ -24,6 +25,38 @@ public record UserSearchFilter
2425

2526
public int PageNumber { get; set; } = 1;
2627
public int PageSize { get; set; } = 10;
28+
29+
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
30+
public override int GetHashCode()
31+
{
32+
unchecked // Allow arithmetic overflow, numbers will just "wrap around"
33+
{
34+
var hash = 17;
35+
36+
hash = hash * 23 + (Name?.GetHashCode() ?? 0);
37+
hash = hash * 23 + (Categories != null ? Categories.Aggregate(0, (current, category) => current + category.GetHashCode()) : 0);
38+
hash = hash * 23 + WithinRadiusKilometers.GetHashCode();
39+
hash = hash * 23 + (Location?.GetHashCode() ?? 0);
40+
hash = hash * 23 + MinimumChildAge.GetHashCode();
41+
hash = hash * 23 + MaximumChildAge.GetHashCode();
42+
hash = hash * 23 + ChildGender.GetHashCode();
43+
44+
return hash;
45+
}
46+
}
47+
48+
public virtual bool Equals(UserSearchFilter? other)
49+
{
50+
return other is not null &&
51+
Name == other.Name &&
52+
((Categories == null && other.Categories == null) ||
53+
(Categories != null && other.Categories != null && Categories.SequenceEqual(other.Categories))) &&
54+
WithinRadiusKilometers == other.WithinRadiusKilometers &&
55+
Location == other.Location &&
56+
MinimumChildAge == other.MinimumChildAge &&
57+
MaximumChildAge == other.MaximumChildAge &&
58+
ChildGender == other.ChildGender;
59+
}
2760
}
2861

2962
file class RadiusRequiredAttribute : ValidationAttribute

src/web/Jordnaer/Extensions/JsRuntimeExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public static class JsRuntimeExtensions
88
public static async Task GoBackAsync(this IJSRuntime jsRuntime)
99
=> await jsRuntime.InvokeVoidAsyncWithErrorHandling("history.back");
1010

11+
public static async Task NavigateTo(this IJSRuntime jsRuntime, string newUrl)
12+
=> await jsRuntime.InvokeVoidAsyncWithErrorHandling("utilities.updatePathAndQueryString", newUrl);
13+
1114
public static async ValueTask<GeoLocation?> GetGeolocation(this IJSRuntime jsRuntime)
1215
{
1316
var (success, geoLocation) = await jsRuntime.InvokeAsyncWithErrorHandling<GeoLocation>("utilities.getGeolocation");

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
@inject IGroupSearchService GroupSearchService
44
@inject NavigationManager Navigation
55
@inject GroupSearchResultCache Cache
6+
@inject IJSRuntime JsRuntime
7+
@inject ISnackbar Snackbar
68

79
<MetadataComponent Title="Grupper"
810
Description="Søg efter grupper" />
@@ -77,7 +79,11 @@
7779

7880
_searchResult = await GroupSearchService.GetGroupsAsync(_filter);
7981

80-
UpdateQueryString();
82+
await UpdateQueryString();
83+
84+
Snackbar.Add(message: $"{_searchResult.TotalCount} brugere matchede søgningen.",
85+
severity: _searchResult.TotalCount is 0 ? Severity.Info : Severity.Success,
86+
options => options.VisibleStateDuration = 3500);
8187

8288
_hasSearched = true;
8389

@@ -88,7 +94,7 @@
8894
}
8995

9096
private static readonly Dictionary<string, object?> _queryStrings = [];
91-
private void UpdateQueryString()
97+
private async Task UpdateQueryString()
9298
{
9399
_queryStrings[nameof(_filter.Name).ToLower()] = _filter.Name;
94100
_queryStrings[nameof(_filter.Categories).ToLower()] = _filter.Categories;
@@ -98,10 +104,8 @@
98104
_queryStrings[nameof(_filter.PageNumber).ToLower()] = _filter.PageNumber;
99105

100106
var newUrl = Navigation.GetUriWithQueryParameters(_queryStrings);
101-
if (Navigation.Uri != newUrl)
102-
{
103-
Navigation.NavigateTo(newUrl);
104-
}
107+
108+
await JsRuntime.NavigateTo(newUrl);
105109
}
106110

107111
private async ValueTask LoadFromQueryString()
@@ -138,8 +142,7 @@
138142

139143
_filter = filter;
140144

141-
if (_filter.Equals(Cache.SearchFilter) &&
142-
Cache.SearchResult is not null)
145+
if (_filter.Equals(Cache.SearchFilter) && Cache.SearchResult is not null)
143146
{
144147
_searchResult = Cache.SearchResult;
145148

src/web/Jordnaer/Pages/UserSearch/UserSearch.razor

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
@inject IUserSearchService UserSearchService
44
@inject NavigationManager Navigation
55
@inject UserSearchResultCache Cache
6+
@inject IJSRuntime JsRuntime
7+
@inject ISnackbar Snackbar
68

79
<MetadataComponent Title="Søg efter brugere"
810
Description="Søg efter brugere" />
@@ -72,7 +74,11 @@
7274

7375
_searchResult = await UserSearchService.GetUsersAsync(_filter);
7476

75-
UpdateQueryString();
77+
Snackbar.Add(message: $"{_searchResult.TotalCount} brugere matchede søgningen.",
78+
severity: _searchResult.TotalCount is 0 ? Severity.Info : Severity.Success,
79+
options => options.VisibleStateDuration = 3500);
80+
81+
await UpdateQueryString();
7682

7783
_hasSearched = true;
7884

@@ -83,7 +89,7 @@
8389
}
8490

8591
private static readonly Dictionary<string, object?> _queryStrings = [];
86-
private void UpdateQueryString()
92+
private async Task UpdateQueryString()
8793
{
8894
_queryStrings[nameof(_filter.Name).ToLower()] = _filter.Name;
8995
_queryStrings[nameof(_filter.Categories).ToLower()] = _filter.Categories;
@@ -96,15 +102,13 @@
96102
_queryStrings[nameof(_filter.PageNumber).ToLower()] = _filter.PageNumber;
97103

98104
var newUrl = Navigation.GetUriWithQueryParameters(_queryStrings);
99-
if (Navigation.Uri != newUrl)
100-
{
101-
Navigation.NavigateTo(newUrl);
102-
}
105+
106+
await JsRuntime.NavigateTo(newUrl);
103107
}
104108

105109
private async ValueTask LoadFromQueryString()
106110
{
107-
var queryStrings = new Uri(Navigation.Uri).Query;
111+
var queryStrings = new Uri(Navigation.Uri).Query;
108112
if (string.IsNullOrEmpty(queryStrings))
109113
{
110114
return;
@@ -143,15 +147,14 @@
143147

144148
_filter = filter;
145149

146-
if (_filter.Equals(Cache.SearchFilter) &&
147-
Cache.SearchResult is not null)
150+
if (_filter.Equals(Cache.SearchFilter) && Cache.SearchResult is not null)
148151
{
149-
_searchResult = Cache.SearchResult;
152+
_searchResult = Cache.SearchResult;
150153

151-
_hasSearched = true;
152-
_isSearching = false;
154+
_hasSearched = true;
155+
_isSearching = false;
153156

154-
return;
157+
return;
155158
}
156159

157160
await Search();

src/web/Jordnaer/wwwroot/js/scroll.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,24 @@ window.scrollFunctions = {
55
}
66
},
77
loadScrollPosition: function (prefix) {
8-
window.scrollTo(0, sessionStorage.getItem(`${prefix}:scrollPosition`));
8+
const scrollPosition = sessionStorage.getItem(`${prefix}:scrollPosition`);
9+
if (!scrollPosition) {
10+
return;
11+
}
12+
13+
setTimeout(function () {
14+
window.scrollTo({
15+
top: scrollPosition,
16+
left: 0,
17+
behavior: 'instant' // 'auto', 'instant' or 'smooth' (default is 'auto')
18+
});
19+
}, 50); // The delay is required to ensure the scroll position is restored after the page has been rendered
920
},
1021
scrollToBottomOfElement: function (selector) {
1122
const element = document.querySelector(selector);
1223

1324
if (!element) return;
14-
25+
1526
element.scrollTop = element.scrollHeight;
1627
}
1728
};

src/web/Jordnaer/wwwroot/js/utilities.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ window.utilities = {
66

77
element.style.setProperty("display", "none", "important")
88
},
9-
9+
1010
focusElement: function (selector) {
1111
const element = document.querySelector(selector);
1212

@@ -15,11 +15,20 @@ window.utilities = {
1515
element.focus();
1616
},
1717

18-
getGeolocation: async function() {
18+
getGeolocation: async function () {
1919
const position = await new Promise((resolve, reject) => {
2020
navigator.geolocation.getCurrentPosition(resolve, reject);
2121
});
2222

2323
return position.coords;
24+
},
25+
26+
updatePathAndQueryString: function (newUri) {
27+
const currentUrl = new URL(window.location.href);
28+
const newUrl = new URL(newUri, window.location.origin);
29+
30+
if (currentUrl.pathname !== newUrl.pathname || currentUrl.search !== newUrl.search) {
31+
window.history.pushState({}, '', newUrl.pathname + newUrl.search);
32+
}
2433
}
2534
};

0 commit comments

Comments
 (0)