Return all users with profiles (#3)

Also fixed server pre-rendering and added log off functionality
This commit is contained in:
Daniel J. Summers 2021-01-10 22:06:26 -05:00
parent 0446098e09
commit 15c1a3ff2c
18 changed files with 270 additions and 52 deletions

View File

@ -11,6 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsJobsJobs.Shared", "Jobs
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50B51580-9F09-41E2-BC78-DAD38C37B583}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50B51580-9F09-41E2-BC78-DAD38C37B583}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
JobsJobsJobs\Directory.Build.props = JobsJobsJobs\Directory.Build.props
database\tables.sql = database\tables.sql database\tables.sql = database\tables.sql
EndProjectSection EndProjectSection
EndProject EndProject

View File

@ -1,12 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <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> <ItemGroup>
<PackageReference Include="Blazored.Toast" Version="3.1.2" /> <PackageReference Include="Blazored.Toast" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" />

View File

@ -44,6 +44,12 @@ else
</ErrorList> </ErrorList>
} }
<hr> <hr>
<h4>Phase 3 &ndash; What Works <small><em>(<span class="text-uppercase">In Progress</span> ~~ Last Updated January 10<sup>th</sup>, 2021)</em></small></h4>
<p>
The &ldquo;View Profiles&rdquo; 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 &ndash; What Works <small><em>(Last Updated January 8<sup>th</sup>, 2021)</em></small></h4> <h4>Phase 2 &ndash; What Works <small><em>(Last Updated January 8<sup>th</sup>, 2021)</em></small></h4>
<p> <p>
If you&rsquo;ve gotten this far, you&rsquo;ve already passed If you&rsquo;ve gotten this far, you&rsquo;ve already passed

View 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("/");
}
}

View 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>

View 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";
}
}

View 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);
}

View File

@ -35,7 +35,7 @@
{ {
var version = Assembly.GetExecutingAssembly().GetName().Version!; var version = Assembly.GetExecutingAssembly().GetName().Version!;
Version = $"v{version.Major}.{version.Minor}"; Version = $"v{version.Major}.{version.Minor}";
if (version.Revision > 0) Version += $".{version.Revision}"; if (version.Build > 0) Version += $".{version.Build}";
base.OnInitialized(); base.OnInitialized();
} }
} }

View File

@ -33,11 +33,16 @@
</li> </li>
<li class="nav-item px-3"> <li class="nav-item px-3">
<NavLink class="nav-link" href="/citizen/profile"> <NavLink class="nav-link" href="/citizen/profile">
<span class="oi oi-pencil" aria-hidden="true"></span> Edit Profile <span class="oi oi-pencil" aria-hidden="true"></span> Edit Your Profile
</NavLink> </NavLink>
</li> </li>
<li class="nav-item px-3"> <li class="nav-item px-3">
<NavLink class="nav-link" href="counter"> <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 <span class="oi oi-plus" aria-hidden="true"></span> Log Off
</NavLink> </NavLink>
</li> </li>

View File

@ -3,9 +3,9 @@
[Parameter] [Parameter]
public string Title { get; set; } = default!; 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"); await js.InvokeVoidAsync("setPageTitle", $"{Title} ~ Jobs, Jobs, Jobs");
} }
} }

View 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>

View File

@ -110,5 +110,11 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
var profile = await _db.FindProfileByCitizen(CitizenId.Parse(id)); var profile = await _db.FindProfileByCitizen(CitizenId.Parse(id));
return profile == null ? NotFound() : Ok(profile); return profile == null ? NotFound() : Ok(profile);
} }
[HttpGet("search")]
public async Task<IActionResult> Search()
{
return Ok(await _db.SearchProfiles());
}
} }
} }

View File

@ -1,4 +1,5 @@
using JobsJobsJobs.Shared; using JobsJobsJobs.Shared;
using JobsJobsJobs.Shared.Api;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql; using Npgsql;
using System; using System;
@ -108,5 +109,19 @@ namespace JobsJobsJobs.Server.Data
/// <returns>The count of skills for the given citizen</returns> /// <returns>The count of skills for the given citizen</returns>
public static async Task<int> CountSkillsByCitizen(this JobsDbContext db, CitizenId citizenId) => public static async Task<int> CountSkillsByCitizen(this JobsDbContext db, CitizenId citizenId) =>
await db.Skills.CountAsync(s => s.CitizenId == citizenId).ConfigureAwait(false); 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);
}
} }
} }

View File

@ -1,11 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
<UserSecretsId>553960ef-0c79-47d4-98d8-9ca1708e558f</UserSecretsId> <UserSecretsId>553960ef-0c79-47d4-98d8-9ca1708e558f</UserSecretsId>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<FileVersion>0.7.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -28,5 +24,4 @@
<Folder Include="Controllers\" /> <Folder Include="Controllers\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -16,7 +16,9 @@
</head> </head>
<body> <body>
<div id="app">
<component type="typeof(JobsJobsJobs.Client.App)" render-mode="WebAssemblyPrerendered" /> <component type="typeof(JobsJobsJobs.Client.App)" render-mode="WebAssemblyPrerendered" />
</div>
<div id="blazor-error-ui"> <div id="blazor-error-ui">
An unhandled error has occurred. An unhandled error has occurred.
@ -24,6 +26,16 @@
<a class="dismiss">🗙</a> <a class="dismiss">🗙</a>
</div> </div>
<script src="_framework/blazor.webassembly.js"></script> <script src="_framework/blazor.webassembly.js"></script>
<script>
var Audio = {
play(audio) {
document.getElementById(audio).play()
}
}
function setPageTitle(theTitle) {
document.title = theTitle
}
</script>
</body> </body>
</html> </html>

View File

@ -3,8 +3,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -35,7 +33,9 @@ namespace JobsJobsJobs.Server
services.AddDbContext<JobsDbContext>(options => services.AddDbContext<JobsDbContext>(options =>
{ {
options.UseNpgsql(Configuration.GetConnectionString("JobsDb"), o => o.UseNodaTime()); 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.AddSingleton<IClock>(SystemClock.Instance);
services.AddLogging(); services.AddLogging();
@ -98,7 +98,7 @@ namespace JobsJobsJobs.Server
endpoints.MapRazorPages(); endpoints.MapRazorPages();
endpoints.MapControllers(); endpoints.MapControllers();
endpoints.MapFallback("api/{**slug}", send404); endpoints.MapFallback("api/{**slug}", send404);
endpoints.MapFallbackToFile("{**slug}", "index.html"); endpoints.MapFallbackToPage("{**slug}", "/_Host");
}); });
} }
} }

View 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);
}

View File

@ -1,12 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <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> <ItemGroup>
<PackageReference Include="Markdig" Version="0.22.1" /> <PackageReference Include="Markdig" Version="0.22.1" />
<PackageReference Include="Nanoid" Version="2.1.0" /> <PackageReference Include="Nanoid" Version="2.1.0" />