From fb147888c5f2719f3f8cae97ebb2f9132012d3a7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Apr 2021 21:49:22 -0400 Subject: [PATCH] Add public search page / initial API (#5) --- .../Client/Pages/Profiles/Search.razor.cs | 22 +-- .../Client/Pages/Profiles/Seeking.razor | 56 ++++++++ .../Client/Pages/Profiles/Seeking.razor.cs | 134 ++++++++++++++++++ src/JobsJobsJobs/Client/Shared/NavMenu.razor | 5 + .../Client/Shared/PublicSearchForm.razor | 60 ++++++++ .../Api/Controllers/ContinentController.cs | 2 - .../Api/Controllers/ProfileController.cs | 5 + src/JobsJobsJobs/Server/Data/Converters.cs | 14 +- .../Server/Data/ProfileExtensions.cs | 53 ++++++- .../Server/JobsJobsJobs.Server.csproj.user | 2 +- src/JobsJobsJobs/Shared/Api/PublicSearch.cs | 37 +++++ 11 files changed, 368 insertions(+), 22 deletions(-) create mode 100644 src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor create mode 100644 src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs create mode 100644 src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor create mode 100644 src/JobsJobsJobs/Shared/Api/PublicSearch.cs diff --git a/src/JobsJobsJobs/Client/Pages/Profiles/Search.razor.cs b/src/JobsJobsJobs/Client/Pages/Profiles/Search.razor.cs index bc725c7..e3b05b3 100644 --- a/src/JobsJobsJobs/Client/Pages/Profiles/Search.razor.cs +++ b/src/JobsJobsJobs/Client/Pages/Profiles/Search.razor.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.WebUtilities; using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading.Tasks; namespace JobsJobsJobs.Client.Pages.Profiles @@ -56,10 +55,10 @@ namespace JobsJobsJobs.Client.Pages.Profiles { if (query.TryGetValue(part, out var partValue)) func(partValue); } - setPart("ContinentId", x => Criteria.ContinentId = x); - setPart("Skill", x => Criteria.Skill = x); - setPart("BioExperience", x => Criteria.BioExperience = x); - setPart("RemoteWork", x => Criteria.RemoteWork = x); + setPart(nameof(Criteria.ContinentId), x => Criteria.ContinentId = x); + setPart(nameof(Criteria.Skill), x => Criteria.Skill = x); + setPart(nameof(Criteria.BioExperience), x => Criteria.BioExperience = x); + setPart(nameof(Criteria.RemoteWork), x => Criteria.RemoteWork = x); await RetrieveProfiles(); } @@ -100,6 +99,11 @@ namespace JobsJobsJobs.Client.Pages.Profiles Searching = false; } + /// + /// Return a CSS class if the user is actively seeking work + /// + /// The result in question + /// A string with the appropriate CSS class, if actively seeking work private static string? IsSeeking(ProfileSearchResult profile) => profile.SeekingEmployment ? "font-weight-bold" : null; @@ -124,10 +128,10 @@ namespace JobsJobsJobs.Client.Pages.Profiles if (!string.IsNullOrEmpty(func(Criteria))) dict.Add(name, func(Criteria)); } - part("ContinentId", it => it.ContinentId); - part("Skill", it => it.Skill); - part("BioExperience", it => it.BioExperience); - part("RemoteWork", it => it.RemoteWork); + part(nameof(Criteria.ContinentId), it => it.ContinentId); + part(nameof(Criteria.Skill), it => it.Skill); + part(nameof(Criteria.BioExperience), it => it.BioExperience); + part(nameof(Criteria.RemoteWork), it => it.RemoteWork); return dict; } diff --git a/src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor b/src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor new file mode 100644 index 0000000..06166b1 --- /dev/null +++ b/src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor @@ -0,0 +1,56 @@ +@page "/profile/seeking" +@inject HttpClient http +@inject NavigationManager nav +@inject AppState state + + +

People Seeking Work

+ + + @if (Searching) + { +

Searching profiles...

+ } + else + { + if (!Searched) + { +

Enter one or more criteria to filter results, or just click “Search” to list all profiles.

+ } + + + +
+ @if (SearchResults.Any()) + { + + + + + + + + + + + + @foreach (var profile in SearchResults) + { + + + + + + + + + } + +
ProfileContinentRegionRemote?Skills
View@profile.DisplayName@YesOrNo(profile.SeekingEmployment)@YesOrNo(profile.RemoteWork)@YesOrNo(profile.FullTime)
+ } + else if (Searched) + { +

No results found for the specified criteria

+ } + } +
diff --git a/src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs b/src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs new file mode 100644 index 0000000..bea3afb --- /dev/null +++ b/src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs @@ -0,0 +1,134 @@ +using JobsJobsJobs.Shared; +using JobsJobsJobs.Shared.Api; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace JobsJobsJobs.Client.Pages.Profiles +{ + public partial class Seeking : ComponentBase + { + /// + /// Whether a search has been performed + /// + private bool Searched { get; set; } = false; + + /// + /// Indicates whether a request for matching profiles is in progress + /// + private bool Searching { get; set; } = false; + + /// + /// The search criteria + /// + private PublicSearch Criteria { get; set; } = new PublicSearch(); + + /// + /// Error messages encountered while searching for profiles + /// + private IList ErrorMessages { get; } = new List(); + + /// + /// All continents + /// + private IEnumerable Continents { get; set; } = Enumerable.Empty(); + + /// + /// The search results + /// + private IEnumerable SearchResults { get; set; } = Enumerable.Empty(); + + protected override async Task OnInitializedAsync() + { + Continents = await state.GetContinents(http); + + // Determine if we have searched before + var query = QueryHelpers.ParseQuery(nav.ToAbsoluteUri(nav.Uri).Query); + + if (query.TryGetValue("Searched", out var searched)) + { + Searched = Convert.ToBoolean(searched); + void setPart(string part, Action func) + { + if (query.TryGetValue(part, out var partValue)) func(partValue); + } + setPart(nameof(Criteria.ContinentId), x => Criteria.ContinentId = x); + setPart(nameof(Criteria.Region), x => Criteria.Region = x); + setPart(nameof(Criteria.Skill), x => Criteria.Skill = x); + setPart(nameof(Criteria.RemoteWork), x => Criteria.RemoteWork = x); + + await RetrieveProfiles(); + } + } + + /// + /// Do a search + /// + /// This navigates with the parameters in the URL; this should trigger a search + private async Task DoSearch() + { + var query = SearchQuery(); + query.Add("Searched", "True"); + nav.NavigateTo(QueryHelpers.AddQueryString("/profile/search", query)); + await RetrieveProfiles(); + } + + /// + /// Retreive profiles matching the current search criteria + /// + private async Task RetrieveProfiles() + { + Searching = true; + + var searchResult = await ServerApi.RetrieveMany(http, + QueryHelpers.AddQueryString("profile/search", SearchQuery())); + + if (searchResult.IsOk) + { + SearchResults = searchResult.Ok; + } + else + { + ErrorMessages.Add(searchResult.Error); + } + + Searched = true; + Searching = false; + } + + private static string? IsSeeking(ProfileSearchResult profile) => + profile.SeekingEmployment ? "font-weight-bold" : null; + + /// + /// Return "Yes" for true and "No" for false + /// + /// The condition in question + /// "Yes" for true, "No" for false + private static string YesOrNo(bool condition) => condition ? "Yes" : "No"; + + /// + /// Create a search query string from the currently-entered criteria + /// + /// The query string for the currently-entered criteria + private IDictionary SearchQuery() + { + var dict = new Dictionary(); + if (Criteria.IsEmptySearch) return dict; + + void part(string name, Func func) + { + if (!string.IsNullOrEmpty(func(Criteria))) dict.Add(name, func(Criteria)); + } + + part(nameof(Criteria.ContinentId), it => it.ContinentId); + part(nameof(Criteria.Region), it => it.Region); + part(nameof(Criteria.Skill), it => it.Skill); + part(nameof(Criteria.RemoteWork), it => it.RemoteWork); + + return dict; + } + } +} diff --git a/src/JobsJobsJobs/Client/Shared/NavMenu.razor b/src/JobsJobsJobs/Client/Shared/NavMenu.razor index 0aa76bc..7bb8323 100644 --- a/src/JobsJobsJobs/Client/Shared/NavMenu.razor +++ b/src/JobsJobsJobs/Client/Shared/NavMenu.razor @@ -18,6 +18,11 @@ Home +