Convert to Blazor #6
@ -9,6 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsJobsJobs.Client", "Jobs
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsJobsJobs.Shared", "JobsJobsJobs\Shared\JobsJobsJobs.Shared.csproj", "{AE329284-47DA-4E76-B542-47489B271130}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50B51580-9F09-41E2-BC78-DAD38C37B583}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
database\tables.sql = database\tables.sql
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -1,4 +1,5 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using System;
|
||||
|
||||
namespace JobsJobsJobs.Client
|
||||
{
|
||||
@ -12,16 +13,40 @@ namespace JobsJobsJobs.Client
|
||||
/// </summary>
|
||||
public class AppState
|
||||
{
|
||||
public event Action OnChange = () => { };
|
||||
|
||||
private UserInfo? _user = null;
|
||||
|
||||
/// <summary>
|
||||
/// The information of the currently logged-in user
|
||||
/// </summary>
|
||||
public UserInfo? User { get; set; } = null;
|
||||
public UserInfo? User
|
||||
{
|
||||
get => _user;
|
||||
set
|
||||
{
|
||||
_user = value;
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _jwt = "";
|
||||
|
||||
/// <summary>
|
||||
/// The JSON Web Token (JWT) for the currently logged-on user
|
||||
/// </summary>
|
||||
public string Jwt { get; set; } = "";
|
||||
public string Jwt
|
||||
{
|
||||
get => _jwt;
|
||||
set
|
||||
{
|
||||
_jwt = value;
|
||||
NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public AppState() { }
|
||||
|
||||
private void NotifyChanged() => OnChange.Invoke();
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,20 @@
|
||||
@inject HttpClient http
|
||||
@inject AppState state
|
||||
|
||||
<h3>Dashboard</h3>
|
||||
<h3>Welcome, @state.User!.Name!</h3>
|
||||
|
||||
<p>Here's the dashboard, homie...</p>
|
||||
|
||||
@if (hasProfile != null)
|
||||
@if (retrievingProfile)
|
||||
{
|
||||
<p>Has profile? @hasProfile</p>
|
||||
<p>Retrieving your employment profile...</p>
|
||||
}
|
||||
else if (profile != null)
|
||||
{
|
||||
<p>Your employment profile was last updated @profile.LastUpdatedOn</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>You do not have an employment profile established; click “Profile”* in the menu to get started!</p>
|
||||
<p><em>* Once it's there...</em></p>
|
||||
}
|
||||
|
||||
@if (errorMessage != null)
|
||||
@ -17,21 +24,22 @@
|
||||
}
|
||||
@code {
|
||||
|
||||
bool? hasProfile = null;
|
||||
bool retrievingProfile = true;
|
||||
Profile? profile = null;
|
||||
string? errorMessage = null;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (state.User != null)
|
||||
{
|
||||
var profile = await ServerApi.RetrieveProfile(http, state);
|
||||
if (profile.IsOk)
|
||||
var profileResult = await ServerApi.RetrieveProfile(http, state);
|
||||
if (profileResult.IsOk)
|
||||
{
|
||||
hasProfile = profile.Ok != null;
|
||||
profile = profileResult.Ok;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = profile.Error;
|
||||
errorMessage = profileResult.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,26 +26,32 @@
|
||||
else
|
||||
{
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="dashboard">
|
||||
<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-spreadsheet" aria-hidden="true"></span> 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="fetchdata">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
state.OnChange += StateHasChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The client ID for Jobs, Jobs, Jobs at No Agenda Social
|
||||
/// </summary>
|
||||
@ -76,4 +82,4 @@
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NodaTime;
|
||||
using Npgsql;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -14,11 +15,14 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
{
|
||||
private readonly IConfigurationSection _config;
|
||||
|
||||
private readonly IClock _clock;
|
||||
|
||||
private readonly NpgsqlConnection _db;
|
||||
|
||||
public CitizenController(IConfiguration config, NpgsqlConnection db)
|
||||
public CitizenController(IConfiguration config, IClock clock, NpgsqlConnection db)
|
||||
{
|
||||
_config = config.GetSection("Auth");
|
||||
_clock = clock;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
@ -32,7 +36,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
|
||||
// Step 2 - Find / establish Jobs, Jobs, Jobs account
|
||||
var account = accountResult.Ok;
|
||||
var now = Milliseconds.Now();
|
||||
var now = _clock.GetCurrentInstant();
|
||||
|
||||
await _db.OpenAsync();
|
||||
var citizen = await _db.FindCitizenByNAUser(account.Username);
|
||||
@ -47,7 +51,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
citizen = citizen with
|
||||
{
|
||||
DisplayName = account.DisplayName,
|
||||
LastSeenOn = Milliseconds.Now()
|
||||
LastSeenOn = now
|
||||
};
|
||||
await _db.UpdateCitizenOnLogOn(citizen);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// <returns>A populated citizen</returns>
|
||||
private static Citizen ToCitizen(NpgsqlDataReader rdr) =>
|
||||
new Citizen(CitizenId.Parse(rdr.GetString("id")), rdr.GetString("na_user"), rdr.GetString("display_name"),
|
||||
rdr.GetString("profile_url"), rdr.GetMilliseconds("joined_on"), rdr.GetMilliseconds("last_seen_on"));
|
||||
rdr.GetString("profile_url"), rdr.GetInstant("joined_on"), rdr.GetInstant("last_seen_on"));
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a citizen by their No Agenda Social user name
|
||||
@ -58,8 +58,8 @@ namespace JobsJobsJobs.Server.Data
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@na_user", citizen.NaUser));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@display_name", citizen.DisplayName));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@profile_url", citizen.ProfileUrl));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@joined_on", citizen.JoinedOn.Millis));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@last_seen_on", citizen.LastSeenOn.Millis));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@joined_on", citizen.JoinedOn));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@last_seen_on", citizen.LastSeenOn));
|
||||
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
@ -78,7 +78,7 @@ namespace JobsJobsJobs.Server.Data
|
||||
WHERE id = @id";
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@id", citizen.Id.ToString()));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@display_name", citizen.DisplayName));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@last_seen_on", citizen.LastSeenOn.Millis));
|
||||
cmd.Parameters.Add(new NpgsqlParameter("@last_seen_on", citizen.LastSeenOn));
|
||||
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using NodaTime;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -19,6 +20,14 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// <returns>The specified field as a boolean</returns>
|
||||
public static bool GetBoolean(this NpgsqlDataReader rdr, string name) => rdr.GetBoolean(rdr.GetOrdinal(name));
|
||||
|
||||
/// <summary>
|
||||
/// Get an Instant by its name
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the field to be retrieved as an Instant</param>
|
||||
/// <returns>The specified field as an Instant</returns>
|
||||
public static Instant GetInstant(this NpgsqlDataReader rdr, string name) =>
|
||||
rdr.GetFieldValue<Instant>(rdr.GetOrdinal(name));
|
||||
|
||||
/// <summary>
|
||||
/// Get a 64-bit integer by its name
|
||||
/// </summary>
|
||||
@ -26,14 +35,6 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// <returns>The specified field as a 64-bit integer</returns>
|
||||
public static long GetInt64(this NpgsqlDataReader rdr, string name) => rdr.GetInt64(rdr.GetOrdinal(name));
|
||||
|
||||
/// <summary>
|
||||
/// Get milliseconds by its name
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the field to be retrieved as milliseconds</param>
|
||||
/// <returns>The specified field as milliseconds</returns>
|
||||
public static Milliseconds GetMilliseconds(this NpgsqlDataReader rdr, string name) =>
|
||||
new Milliseconds(rdr.GetInt64(name));
|
||||
|
||||
/// <summary>
|
||||
/// Get a string by its name
|
||||
/// </summary>
|
||||
|
@ -23,7 +23,7 @@ namespace JobsJobsJobs.Server.Data
|
||||
return new Profile(CitizenId.Parse(rdr.GetString("id")), rdr.GetBoolean("seeking_employment"),
|
||||
rdr.GetBoolean("is_public"), continentId, rdr.GetString("region"), rdr.GetBoolean("remote_work"),
|
||||
rdr.GetBoolean("full_time"), new MarkdownString(rdr.GetString("biography")),
|
||||
rdr.GetMilliseconds("last_updated_on"),
|
||||
rdr.GetInstant("last_updated_on"),
|
||||
rdr.IsDBNull("experience") ? null : new MarkdownString(rdr.GetString("experience")))
|
||||
{
|
||||
Continent = new Continent(continentId, rdr.GetString("continent_name"))
|
||||
|
@ -7,8 +7,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.1" />
|
||||
<PackageReference Include="Npgsql" Version="5.0.1.1" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="5.0.1.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
using JobsJobsJobs.Server.Data;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
@ -8,8 +7,8 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
using Npgsql;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace JobsJobsJobs.Server
|
||||
@ -28,6 +27,7 @@ namespace JobsJobsJobs.Server
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped(_ => new NpgsqlConnection(Configuration.GetConnectionString("JobsDb")));
|
||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
services.AddLogging();
|
||||
services.AddControllersWithViews();
|
||||
services.AddRazorPages();
|
||||
@ -54,6 +54,8 @@ namespace JobsJobsJobs.Server
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
NpgsqlConnection.GlobalTypeMapper.UseNodaTime();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
@ -69,10 +71,6 @@ namespace JobsJobsJobs.Server
|
||||
app.UseHttpsRedirection();
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
// TODO: middleware to extract user info from our custom JWT, and see if it will fill the standard stuff.
|
||||
// Alternately, maybe we can even configure the default services and middleware so that it will work
|
||||
// to give us the currently-logged-on user.
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace JobsJobsJobs.Shared
|
||||
using NodaTime;
|
||||
|
||||
namespace JobsJobsJobs.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// A user of Jobs, Jobs, Jobs
|
||||
@ -8,6 +10,6 @@
|
||||
string NaUser,
|
||||
string DisplayName,
|
||||
string ProfileUrl,
|
||||
Milliseconds JoinedOn,
|
||||
Milliseconds LastSeenOn);
|
||||
Instant JoinedOn,
|
||||
Instant LastSeenOn);
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace JobsJobsJobs.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// Milliseconds past the epoch (JavaScript's date storage format)
|
||||
/// </summary>
|
||||
public record Milliseconds(long Millis)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the milliseconds value for now
|
||||
/// </summary>
|
||||
/// <returns>A new milliseconds from the time now</returns>
|
||||
public static Milliseconds Now() =>
|
||||
new Milliseconds(
|
||||
(DateTime.UtcNow.Ticks - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks) / 10000L);
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
namespace JobsJobsJobs.Shared
|
||||
using NodaTime;
|
||||
|
||||
namespace JobsJobsJobs.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// A job seeker profile
|
||||
@ -12,7 +14,7 @@
|
||||
bool RemoteWork,
|
||||
bool FullTime,
|
||||
MarkdownString Biography,
|
||||
Milliseconds LastUpdatedOn,
|
||||
Instant LastUpdatedOn,
|
||||
MarkdownString? Experience)
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace JobsJobsJobs.Shared
|
||||
using NodaTime;
|
||||
|
||||
namespace JobsJobsJobs.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// A record of success finding employment
|
||||
@ -6,7 +8,7 @@
|
||||
public record Success(
|
||||
SuccessId Id,
|
||||
CitizenId CitizenId,
|
||||
Milliseconds RecordedOn,
|
||||
Instant RecordedOn,
|
||||
bool FromHere,
|
||||
MarkdownString? Story);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.22.0" />
|
||||
<PackageReference Include="Nanoid" Version="2.1.0" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -6,8 +6,8 @@ CREATE TABLE jjj.citizen (
|
||||
na_user VARCHAR(50) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
profile_url VARCHAR(1024) NOT NULL,
|
||||
joined_on BIGINT NOT NULL,
|
||||
last_seen_on BIGINT NOT NULL,
|
||||
joined_on TIMESTAMP NOT NULL,
|
||||
last_seen_on TIMESTAMP NOT NULL,
|
||||
CONSTRAINT pk_citizen PRIMARY KEY (id),
|
||||
CONSTRAINT uk_na_user UNIQUE (na_user)
|
||||
);
|
||||
@ -45,7 +45,7 @@ CREATE TABLE jjj.profile (
|
||||
remote_work BOOLEAN NOT NULL,
|
||||
full_time BOOLEAN NOT NULL,
|
||||
biography TEXT NOT NULL,
|
||||
last_updated_on BIGINT NOT NULL,
|
||||
last_updated_on TIMESTAMP NOT NULL,
|
||||
experience TEXT,
|
||||
CONSTRAINT pk_profile PRIMARY KEY (citizen_id),
|
||||
CONSTRAINT fk_profile_citizen FOREIGN KEY (citizen_id) REFERENCES jjj.citizen (id),
|
||||
@ -100,7 +100,7 @@ COMMENT ON INDEX jjj.idx_skill_citizen IS 'FK index';
|
||||
CREATE TABLE jjj.success (
|
||||
id VARCHAR(12) NOT NULL,
|
||||
citizen_id VARCHAR(12) NOT NULL,
|
||||
recorded_on BIGINT NOT NULL,
|
||||
recorded_on TIMESTAMP NOT NULL,
|
||||
from_here BOOLEAN NOT NULL,
|
||||
story TEXT,
|
||||
CONSTRAINT pk_success PRIMARY KEY (id),
|
||||
|
Loading…
Reference in New Issue
Block a user