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())
+ {
+
+
+
+ Profile |
+ Continent |
+ Region |
+ Remote? |
+ Skills |
+
+
+
+ @foreach (var profile in SearchResults)
+ {
+
+ 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
+
+
+ Job Seekers
+
+
Log On
diff --git a/src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor b/src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor
new file mode 100644
index 0000000..550e6a0
--- /dev/null
+++ b/src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor
@@ -0,0 +1,60 @@
+@using JobsJobsJobs.Shared.Api
+
+
+
+
+
+
+@code {
+ [Parameter]
+ public PublicSearch Criteria { get; set; } = default!;
+
+ [Parameter]
+ public EventCallback OnSearch { get; set; } = default!;
+
+ [Parameter]
+ public IEnumerable Continents { get; set; } = default!;
+}
diff --git a/src/JobsJobsJobs/Server/Areas/Api/Controllers/ContinentController.cs b/src/JobsJobsJobs/Server/Areas/Api/Controllers/ContinentController.cs
index 42808e9..e7925af 100644
--- a/src/JobsJobsJobs/Server/Areas/Api/Controllers/ContinentController.cs
+++ b/src/JobsJobsJobs/Server/Areas/Api/Controllers/ContinentController.cs
@@ -1,5 +1,4 @@
using JobsJobsJobs.Server.Data;
-using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
@@ -9,7 +8,6 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
/// API endpoint for continent information
///
[Route("api/[controller]")]
- [Authorize]
[ApiController]
public class ContinentController : ControllerBase
{
diff --git a/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs b/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs
index 534a18a..da4e376 100644
--- a/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs
+++ b/src/JobsJobsJobs/Server/Areas/Api/Controllers/ProfileController.cs
@@ -120,6 +120,11 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
public async Task Search([FromQuery] ProfileSearch search) =>
Ok(await _db.SearchProfiles(search));
+ [HttpGet("public-search")]
+ [AllowAnonymous]
+ public async Task SearchPublic([FromQuery] PublicSearch search) =>
+ Ok(await _db.SearchPublicProfiles(search));
+
[HttpPatch("employment-found")]
public async Task EmploymentFound()
{
diff --git a/src/JobsJobsJobs/Server/Data/Converters.cs b/src/JobsJobsJobs/Server/Data/Converters.cs
index 7f8d07f..66230a2 100644
--- a/src/JobsJobsJobs/Server/Data/Converters.cs
+++ b/src/JobsJobsJobs/Server/Data/Converters.cs
@@ -12,38 +12,36 @@ namespace JobsJobsJobs.Server.Data
/// Citizen ID converter
///
public static readonly ValueConverter CitizenIdConverter =
- new ValueConverter(v => v.ToString(), v => CitizenId.Parse(v));
+ new(v => v.ToString(), v => CitizenId.Parse(v));
///
/// Continent ID converter
///
public static readonly ValueConverter ContinentIdConverter =
- new ValueConverter(v => v.ToString(), v => ContinentId.Parse(v));
+ new(v => v.ToString(), v => ContinentId.Parse(v));
///
/// Markdown converter
///
public static readonly ValueConverter MarkdownStringConverter =
- new ValueConverter(v => v.Text, v => new MarkdownString(v));
+ new(v => v.Text, v => new MarkdownString(v));
///
/// Markdown converter for possibly-null values
///
public static readonly ValueConverter OptionalMarkdownStringConverter =
- new ValueConverter(
- v => v == null ? null : v.Text,
- v => v == null ? null : new MarkdownString(v));
+ new(v => v == null ? null : v.Text, v => v == null ? null : new MarkdownString(v));
///
/// Skill ID converter
///
public static readonly ValueConverter SkillIdConverter =
- new ValueConverter(v => v.ToString(), v => SkillId.Parse(v));
+ new(v => v.ToString(), v => SkillId.Parse(v));
///
/// Success ID converter
///
public static readonly ValueConverter SuccessIdConverter =
- new ValueConverter(v => v.ToString(), v => SuccessId.Parse(v));
+ new(v => v.ToString(), v => SuccessId.Parse(v));
}
}
diff --git a/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs b/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
index fdd9780..7b1d0e7 100644
--- a/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
+++ b/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
@@ -111,7 +111,7 @@ namespace JobsJobsJobs.Server.Data
///
/// Search profiles by the given criteria
///
- // TODO: A criteria parameter!
+ /// The search parameters
/// The information for profiles matching the criteria
public static async Task> SearchProfiles(this JobsDbContext db,
ProfileSearch search)
@@ -160,7 +160,56 @@ namespace JobsJobsJobs.Server.Data
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
.ToListAsync().ConfigureAwait(false);
}
-
+
+ ///
+ /// Search public profiles by the given criteria
+ ///
+ /// The search parameters
+ /// The information for profiles matching the criteria
+ public static async Task> SearchPublicProfiles(this JobsDbContext db,
+ PublicSearch search)
+ {
+ var query = db.Profiles
+ .Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c })
+ .Where(it => it.Profile.IsPublic);
+
+ 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.Region))
+ {
+ query = query.Where(it => it.Profile.Region.ToLower().Contains(search.Region.ToLower()));
+ }
+
+ 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 (useIds)
+ {
+ query = query.Where(it => citizenIds.Contains(it.Citizen.Id));
+ }
+
+ return await query.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.CitizenName,
+ x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
+ .ToListAsync().ConfigureAwait(false);
+ }
+
///
/// Delete skills and profile for the given citizen
///
diff --git a/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj.user b/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj.user
index 4aff699..476125d 100644
--- a/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj.user
+++ b/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj.user
@@ -6,7 +6,7 @@
JobsJobsJobs.Server
MvcControllerEmptyScaffolder
root/Common/MVC/Controller
- FolderProfile
+ C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\Properties\PublishProfiles\FolderProfile.pubxml
ProjectDebugger
diff --git a/src/JobsJobsJobs/Shared/Api/PublicSearch.cs b/src/JobsJobsJobs/Shared/Api/PublicSearch.cs
new file mode 100644
index 0000000..91e4e01
--- /dev/null
+++ b/src/JobsJobsJobs/Shared/Api/PublicSearch.cs
@@ -0,0 +1,37 @@
+namespace JobsJobsJobs.Shared.Api
+{
+ ///
+ /// The parameters for a public job search
+ ///
+ public class PublicSearch
+ {
+ ///
+ /// Retrieve citizens from this continent
+ ///
+ public string? ContinentId { get; set; }
+
+ ///
+ /// Retrieve citizens from this region
+ ///
+ public string? Region { get; set; }
+
+ ///
+ /// Text for a search within a citizen's skills
+ ///
+ public string? Skill { 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(Region)
+ && string.IsNullOrEmpty(Skill)
+ && string.IsNullOrEmpty(RemoteWork);
+ }
+}