diff --git a/src/JobsJobsJobs/Client/Pages/Profile/Search.razor b/src/JobsJobsJobs/Client/Pages/Profile/Search.razor index 30d935b..0175372 100644 --- a/src/JobsJobsJobs/Client/Pages/Profile/Search.razor +++ b/src/JobsJobsJobs/Client/Pages/Profile/Search.razor @@ -12,6 +12,59 @@ } else { + if (!Searched) + { +

Instructions go here

+ } +
+ +
+
+ + + + @foreach (var (id, name) in Continents) + { + + } + +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
@if (SearchResults.Any()) { diff --git a/src/JobsJobsJobs/Client/Pages/Profile/Search.razor.cs b/src/JobsJobsJobs/Client/Pages/Profile/Search.razor.cs index b0fc88b..337443b 100644 --- a/src/JobsJobsJobs/Client/Pages/Profile/Search.razor.cs +++ b/src/JobsJobsJobs/Client/Pages/Profile/Search.razor.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components; using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; namespace JobsJobsJobs.Client.Pages.Profile @@ -20,6 +21,11 @@ namespace JobsJobsJobs.Client.Pages.Profile /// private bool Searching { get; set; } = false; + /// + /// The search criteria + /// + private ProfileSearch Criteria { get; set; } = new ProfileSearch(); + /// /// Error messages encountered while searching for profiles /// @@ -60,8 +66,8 @@ namespace JobsJobsJobs.Client.Pages.Profile { Searching = true; - // TODO: send a filter with this request - var searchResult = await ServerApi.RetrieveMany(http, "profile/search"); + var searchResult = await ServerApi.RetrieveMany(http, + $"profile/search{SearchQuery()}"); if (searchResult.IsOk) { @@ -85,5 +91,27 @@ namespace JobsJobsJobs.Client.Pages.Profile /// 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 string SearchQuery() + { + if (Criteria.IsEmptySearch) return ""; + + string part(string name, Func func) => + string.IsNullOrEmpty(func(Criteria)) ? "" : $"{name}={WebUtility.UrlEncode(func(Criteria))}"; + + IEnumerable parts() + { + yield return part("ContinentId", it => it.ContinentId); + yield return part("Skill", it => it.Skill); + yield return part("BioExperience", it => it.BioExperience); + yield return part("RemoteWork", it => it.RemoteWork); + } + + return $"?{string.Join("&", parts().Where(it => !string.IsNullOrEmpty(it)).ToArray())}"; + } } } diff --git a/src/JobsJobsJobs/Client/wwwroot/css/app.css b/src/JobsJobsJobs/Client/wwwroot/css/app.css index 877de2d..3a4c3f1 100644 --- a/src/JobsJobsJobs/Client/wwwroot/css/app.css +++ b/src/JobsJobsJobs/Client/wwwroot/css/app.css @@ -65,3 +65,7 @@ label.jjj-required::after { color: red; content: ' *'; } + +label.jjj-label { + font-style: italic; +} diff --git a/src/JobsJobsJobs/Directory.Build.props b/src/JobsJobsJobs/Directory.Build.props index 4441614..eaa6e00 100644 --- a/src/JobsJobsJobs/Directory.Build.props +++ b/src/JobsJobsJobs/Directory.Build.props @@ -2,7 +2,7 @@ net5.0 enable - 0.7.1.0 - 0.7.1.0 + 0.7.2.0 + 0.7.2.0 diff --git a/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs b/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs index b1e1095..a1bff06 100644 --- a/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs +++ b/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs @@ -112,9 +112,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers } [HttpGet("search")] - public async Task Search() - { - return Ok(await _db.SearchProfiles()); - } + public async Task Search([FromQuery] ProfileSearch search) => + Ok(await _db.SearchProfiles(search)); } } diff --git a/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs b/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs index 65f318e..4b59d88 100644 --- a/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs +++ b/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs @@ -115,12 +115,51 @@ namespace JobsJobsJobs.Server.Data /// // TODO: A criteria parameter! /// The information for profiles matching the criteria - public static async Task> SearchProfiles(this JobsDbContext db) + public static async Task> SearchProfiles(this JobsDbContext db, + ProfileSearch search) { - return await db.Profiles - .Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c }) - .Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.DisplayName, x.Profile.SeekingEmployment, - x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn)) + var query = db.Profiles + .Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c }); + + var useIds = false; + var citizenIds = new List(); + + if (!string.IsNullOrEmpty(search.ContinentId)) + { + query = query.Where(it => it.Profile.ContinentId == ContinentId.Parse(search.ContinentId)); + } + + if (!string.IsNullOrEmpty(search.RemoteWork)) + { + query = query.Where(it => it.Profile.RemoteWork == (search.RemoteWork == "yes")); + } + + if (!string.IsNullOrEmpty(search.Skill)) + { + useIds = true; + citizenIds.AddRange(await db.Skills + .Where(s => s.Description.ToLower().Contains(search.Skill.ToLower())) + .Select(s => s.CitizenId) + .ToListAsync().ConfigureAwait(false)); + } + + if (!string.IsNullOrEmpty(search.BioExperience)) + { + useIds = true; + citizenIds.AddRange(await db.Profiles + .FromSqlRaw("SELECT citizen_id FROM profile WHERE biography ILIKE {0} OR experience ILIKE {0}", + $"%{search.BioExperience}%") + .Select(p => p.Id) + .ToListAsync().ConfigureAwait(false)); + } + + if (useIds) + { + query = query.Where(it => citizenIds.Contains(it.Citizen.Id)); + } + + return await query.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.DisplayName, + x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn)) .ToListAsync().ConfigureAwait(false); } } diff --git a/src/JobsJobsJobs/Shared/Api/ProfileSearch.cs b/src/JobsJobsJobs/Shared/Api/ProfileSearch.cs new file mode 100644 index 0000000..7d25d55 --- /dev/null +++ b/src/JobsJobsJobs/Shared/Api/ProfileSearch.cs @@ -0,0 +1,37 @@ +namespace JobsJobsJobs.Shared.Api +{ + /// + /// The various ways profiles can be searched + /// + public class ProfileSearch + { + /// + /// Retrieve citizens from this continent + /// + public string? ContinentId { get; set; } + + /// + /// Text for a search within a citizen's skills + /// + public string? Skill { get; set; } + + /// + /// Text for a search with a citizen's professional biography and experience fields + /// + public string? BioExperience { get; set; } + + /// + /// Whether to retrieve citizens who do or do not want remote work + /// + public string RemoteWork { get; set; } = ""; + + /// + /// Is the search empty? + /// + public bool IsEmptySearch => + string.IsNullOrEmpty(ContinentId) + && string.IsNullOrEmpty(Skill) + && string.IsNullOrEmpty(BioExperience) + && string.IsNullOrEmpty(RemoteWork); + } +} diff --git a/src/JobsJobsJobs/Shared/Result.cs b/src/JobsJobsJobs/Shared/Result.cs index 535e81f..7bbb869 100644 --- a/src/JobsJobsJobs/Shared/Result.cs +++ b/src/JobsJobsJobs/Shared/Result.cs @@ -1,4 +1,6 @@ -namespace JobsJobsJobs.Shared +using System; + +namespace JobsJobsJobs.Shared { /// /// A result with two different possibilities @@ -60,5 +62,24 @@ /// The error message /// The Error result public static Result AsError(string error) => new Result(false) { Error = error }; + + /// + /// Transform a result if it is OK, passing the error along if it is an error + /// + /// The transforming function + /// The existing result + /// The resultant result + public static Result Bind(Func> f, Result result) => + result.IsOk ? f(result.Ok) : result; + + /// + /// Transform a result to a different type if it is OK, passing the error along if it is an error + /// + /// The type to which the result is transformed + /// The transforming function + /// The existing result + /// The resultant result + public static Result Map(Func> f, Result result) => + result.IsOk ? f(result.Ok) : Result.AsError(result.Error); } }