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
+ }
+
@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);
}
}