Return all users with profiles (#3)
Also fixed server pre-rendering and added log off functionality
This commit is contained in:
parent
0446098e09
commit
15c1a3ff2c
|
@ -11,6 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsJobsJobs.Shared", "Jobs
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50B51580-9F09-41E2-BC78-DAD38C37B583}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
JobsJobsJobs\Directory.Build.props = JobsJobsJobs\Directory.Build.props
|
||||
database\tables.sql = database\tables.sql
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<FileVersion>0.7.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.Toast" Version="3.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" />
|
||||
|
|
|
@ -44,6 +44,12 @@ else
|
|||
</ErrorList>
|
||||
}
|
||||
<hr>
|
||||
<h4>Phase 3 – What Works <small><em>(<span class="text-uppercase">In Progress</span> ~~ Last Updated January 10<sup>th</sup>, 2021)</em></small></h4>
|
||||
<p>
|
||||
The “View Profiles” link at the side does not have any search capabilities, but it does provide a list of
|
||||
citizens who have filled out profiles, along with a way to view those profiles.
|
||||
</p>
|
||||
<hr>
|
||||
<h4>Phase 2 – What Works <small><em>(Last Updated January 8<sup>th</sup>, 2021)</em></small></h4>
|
||||
<p>
|
||||
If you’ve gotten this far, you’ve already passed
|
||||
|
|
13
src/JobsJobsJobs/Client/Pages/Citizen/LogOff.razor
Normal file
13
src/JobsJobsJobs/Client/Pages/Citizen/LogOff.razor
Normal file
|
@ -0,0 +1,13 @@
|
|||
@page "/citizen/log-off"
|
||||
@inject NavigationManager nav
|
||||
@inject AppState state
|
||||
@inject IToastService toast
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
state.Jwt = "";
|
||||
state.User = null;
|
||||
toast.ShowSuccess("Have a Nice Day!", "Log Off Successful");
|
||||
nav.NavigateTo("/");
|
||||
}
|
||||
}
|
44
src/JobsJobsJobs/Client/Pages/Profile/Search.razor
Normal file
44
src/JobsJobsJobs/Client/Pages/Profile/Search.razor
Normal file
|
@ -0,0 +1,44 @@
|
|||
@page "/profile/search"
|
||||
@inject HttpClient http
|
||||
@inject AppState state
|
||||
|
||||
<PageTitle Title="Search Profiles" />
|
||||
<h3>Search Profiles</h3>
|
||||
|
||||
<ErrorList Errors=@ErrorMessages>
|
||||
@if (Searching)
|
||||
{
|
||||
<p>Searching profiles...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (SearchResults.Any())
|
||||
{
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Profile</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col" class="text-center">Seeking?</th>
|
||||
<th scope="col" class="text-center">Remote?</th>
|
||||
<th scope="col" class="text-center">Full-Time?</th>
|
||||
<th scope="col">Last Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var profile in SearchResults)
|
||||
{
|
||||
<tr>
|
||||
<td><a href="/profile/view/@profile.CitizenId">View</a></td>
|
||||
<td class=@IsSeeking(profile)>@profile.DisplayName</td>
|
||||
<td class="text-center">@YesOrNo(profile.SeekingEmployment)</td>
|
||||
<td class="text-center">@YesOrNo(profile.RemoteWork)</td>
|
||||
<td class="text-center">@YesOrNo(profile.FullTime)</td>
|
||||
<td><FullDate TheDate=@profile.LastUpdated /></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
</ErrorList>
|
89
src/JobsJobsJobs/Client/Pages/Profile/Search.razor.cs
Normal file
89
src/JobsJobsJobs/Client/Pages/Profile/Search.razor.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client.Pages.Profile
|
||||
{
|
||||
public partial class Search : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a search has been performed
|
||||
/// </summary>
|
||||
private bool Searched { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether a request for matching profiles is in progress
|
||||
/// </summary>
|
||||
private bool Searching { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Error messages encountered while searching for profiles
|
||||
/// </summary>
|
||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// All continents
|
||||
/// </summary>
|
||||
private IEnumerable<Continent> Continents { get; set; } = Enumerable.Empty<Continent>();
|
||||
|
||||
/// <summary>
|
||||
/// The search results
|
||||
/// </summary>
|
||||
private IEnumerable<ProfileSearchResult> SearchResults { get; set; } = Enumerable.Empty<ProfileSearchResult>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ServerApi.SetJwt(http, state);
|
||||
var continentResult = await ServerApi.RetrieveMany<Continent>(http, "continent/all");
|
||||
|
||||
if (continentResult.IsOk)
|
||||
{
|
||||
Continents = continentResult.Ok;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(continentResult.Error);
|
||||
}
|
||||
|
||||
// TODO: remove this call once the filter is ready
|
||||
await RetrieveProfiles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retreive profiles matching the current search criteria
|
||||
/// </summary>
|
||||
private async Task RetrieveProfiles()
|
||||
{
|
||||
Searching = true;
|
||||
|
||||
// TODO: send a filter with this request
|
||||
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http, "profile/search");
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Return "Yes" for true and "No" for false
|
||||
/// </summary>
|
||||
/// <param name="condition">The condition in question</param>
|
||||
/// <returns>"Yes" for true, "No" for false</returns>
|
||||
private static string YesOrNo(bool condition) => condition ? "Yes" : "No";
|
||||
}
|
||||
}
|
23
src/JobsJobsJobs/Client/Shared/FullDate.razor
Normal file
23
src/JobsJobsJobs/Client/Shared/FullDate.razor
Normal file
|
@ -0,0 +1,23 @@
|
|||
@using NodaTime
|
||||
@using NodaTime.Text
|
||||
@using System.Globalization
|
||||
|
||||
@Translated
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The pattern with which dates will be formatted
|
||||
/// </summary>
|
||||
private static InstantPattern pattern = InstantPattern.Create("ld<MMMM d, yyyy>", CultureInfo.CurrentCulture);
|
||||
|
||||
/// <summary>
|
||||
/// The date to be formatted
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Instant TheDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The formatted date
|
||||
/// </summary>
|
||||
private string Translated => pattern.Format(TheDate);
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
{
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||
Version = $"v{version.Major}.{version.Minor}";
|
||||
if (version.Revision > 0) Version += $".{version.Revision}";
|
||||
if (version.Build > 0) Version += $".{version.Build}";
|
||||
base.OnInitialized();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,29 +18,34 @@
|
|||
</li>
|
||||
@if (state.User == null)
|
||||
{
|
||||
<li class="nav-item px-3">
|
||||
<a class="nav-link" href="@AuthUrl">
|
||||
<span class="oi oi-account-login" aria-hidden="true"></span> Log On
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<a class="nav-link" href="@AuthUrl">
|
||||
<span class="oi oi-account-login" aria-hidden="true"></span> Log On
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/citizen/dashboard">
|
||||
<span class="oi oi-dashboard" aria-hidden="true"></span> Dashboard
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/citizen/profile">
|
||||
<span class="oi oi-pencil" aria-hidden="true"></span> Edit Profile
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Log Off
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/citizen/dashboard">
|
||||
<span class="oi oi-dashboard" aria-hidden="true"></span> Dashboard
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/citizen/profile">
|
||||
<span class="oi oi-pencil" aria-hidden="true"></span> Edit Your Profile
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/profile/search">
|
||||
<span class="oi oi-spreadsheet" aria-hidden="true"></span> View Profiles
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/citizen/log-off">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Log Off
|
||||
</NavLink>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
[Parameter]
|
||||
public string Title { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
await js.InvokeVoidAsync("setPageTitle", $"{Title} ~ Jobs, Jobs, Jobs");
|
||||
}
|
||||
}
|
||||
|
|
8
src/JobsJobsJobs/Directory.Build.props
Normal file
8
src/JobsJobsJobs/Directory.Build.props
Normal file
|
@ -0,0 +1,8 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyVersion>0.7.1.0</AssemblyVersion>
|
||||
<FileVersion>0.7.1.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -110,5 +110,11 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||
var profile = await _db.FindProfileByCitizen(CitizenId.Parse(id));
|
||||
return profile == null ? NotFound() : Ok(profile);
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> Search()
|
||||
{
|
||||
return Ok(await _db.SearchProfiles());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using System;
|
||||
|
@ -108,5 +109,19 @@ namespace JobsJobsJobs.Server.Data
|
|||
/// <returns>The count of skills for the given citizen</returns>
|
||||
public static async Task<int> CountSkillsByCitizen(this JobsDbContext db, CitizenId citizenId) =>
|
||||
await db.Skills.CountAsync(s => s.CitizenId == citizenId).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Search profiles by the given criteria
|
||||
/// </summary>
|
||||
// TODO: A criteria parameter!
|
||||
/// <returns>The information for profiles matching the criteria</returns>
|
||||
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db)
|
||||
{
|
||||
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))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>553960ef-0c79-47d4-98d8-9ca1708e558f</UserSecretsId>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<FileVersion>0.7.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -28,5 +24,4 @@
|
|||
<Folder Include="Controllers\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -16,14 +16,26 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<component type="typeof(JobsJobsJobs.Client.App)" render-mode="WebAssemblyPrerendered" />
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script>
|
||||
var Audio = {
|
||||
play(audio) {
|
||||
document.getElementById(audio).play()
|
||||
}
|
||||
}
|
||||
function setPageTitle(theTitle) {
|
||||
document.title = theTitle
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -3,8 +3,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -35,7 +33,9 @@ namespace JobsJobsJobs.Server
|
|||
services.AddDbContext<JobsDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("JobsDb"), o => o.UseNodaTime());
|
||||
// options.LogTo(System.Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
|
||||
#if DEBUG
|
||||
options.LogTo(System.Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
|
||||
#endif
|
||||
});
|
||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
services.AddLogging();
|
||||
|
@ -98,7 +98,7 @@ namespace JobsJobsJobs.Server
|
|||
endpoints.MapRazorPages();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapFallback("api/{**slug}", send404);
|
||||
endpoints.MapFallbackToFile("{**slug}", "index.html");
|
||||
endpoints.MapFallbackToPage("{**slug}", "/_Host");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
15
src/JobsJobsJobs/Shared/Api/ProfileSearchResult.cs
Normal file
15
src/JobsJobsJobs/Shared/Api/ProfileSearchResult.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace JobsJobsJobs.Shared.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// A user matching the profile search
|
||||
/// </summary>
|
||||
public record ProfileSearchResult(
|
||||
CitizenId CitizenId,
|
||||
string DisplayName,
|
||||
bool SeekingEmployment,
|
||||
bool RemoteWork,
|
||||
bool FullTime,
|
||||
Instant LastUpdated);
|
||||
}
|
|
@ -1,12 +1,5 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyVersion>0.7.0.0</AssemblyVersion>
|
||||
<FileVersion>0.7.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.22.1" />
|
||||
<PackageReference Include="Nanoid" Version="2.1.0" />
|
||||
|
|
Loading…
Reference in New Issue
Block a user