diff --git a/src/JobsJobsJobs.sln b/src/JobsJobsJobs.sln
index d3bbc63..77b6417 100644
--- a/src/JobsJobsJobs.sln
+++ b/src/JobsJobsJobs.sln
@@ -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
diff --git a/src/JobsJobsJobs/Client/AppState.cs b/src/JobsJobsJobs/Client/AppState.cs
index 6a771a8..62abfa9 100644
--- a/src/JobsJobsJobs/Client/AppState.cs
+++ b/src/JobsJobsJobs/Client/AppState.cs
@@ -1,4 +1,5 @@
using JobsJobsJobs.Shared;
+using System;
namespace JobsJobsJobs.Client
{
@@ -12,16 +13,40 @@ namespace JobsJobsJobs.Client
///
public class AppState
{
+ public event Action OnChange = () => { };
+
+ private UserInfo? _user = null;
+
///
/// The information of the currently logged-in user
///
- public UserInfo? User { get; set; } = null;
+ public UserInfo? User
+ {
+ get => _user;
+ set
+ {
+ _user = value;
+ NotifyChanged();
+ }
+ }
+
+ private string _jwt = "";
///
/// The JSON Web Token (JWT) for the currently logged-on user
///
- public string Jwt { get; set; } = "";
+ public string Jwt
+ {
+ get => _jwt;
+ set
+ {
+ _jwt = value;
+ NotifyChanged();
+ }
+ }
public AppState() { }
+
+ private void NotifyChanged() => OnChange.Invoke();
}
}
diff --git a/src/JobsJobsJobs/Client/Pages/Citizen/Dashboard.razor b/src/JobsJobsJobs/Client/Pages/Citizen/Dashboard.razor
index 33a7a87..0b21952 100644
--- a/src/JobsJobsJobs/Client/Pages/Citizen/Dashboard.razor
+++ b/src/JobsJobsJobs/Client/Pages/Citizen/Dashboard.razor
@@ -2,13 +2,20 @@
@inject HttpClient http
@inject AppState state
-
Dashboard
+Welcome, @state.User!.Name!
-Here's the dashboard, homie...
-
-@if (hasProfile != null)
+@if (retrievingProfile)
{
- Has profile? @hasProfile
+ Retrieving your employment profile...
+}
+else if (profile != null)
+{
+ Your employment profile was last updated @profile.LastUpdatedOn
+}
+else
+{
+ You do not have an employment profile established; click “Profile”* in the menu to get started!
+ * Once it's there...
}
@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;
}
}
}
diff --git a/src/JobsJobsJobs/Client/Shared/NavMenu.razor b/src/JobsJobsJobs/Client/Shared/NavMenu.razor
index 1e4fd93..d0fc6c4 100644
--- a/src/JobsJobsJobs/Client/Shared/NavMenu.razor
+++ b/src/JobsJobsJobs/Client/Shared/NavMenu.razor
@@ -26,26 +26,32 @@
else
{
-
+
Dashboard
+
+
+ Profile
+
+
Log Off
}
-
-
- Fetch data
-
-
@code {
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ state.OnChange += StateHasChanged;
+ }
+
///
/// The client ID for Jobs, Jobs, Jobs at No Agenda Social
///
@@ -76,4 +82,4 @@
{
collapseNavMenu = !collapseNavMenu;
}
-}
+}
\ No newline at end of file
diff --git a/src/JobsJobsJobs/Server/Areas/Api/Controllers/CitizenController.cs b/src/JobsJobsJobs/Server/Areas/Api/Controllers/CitizenController.cs
index 6d53ac6..b79380e 100644
--- a/src/JobsJobsJobs/Server/Areas/Api/Controllers/CitizenController.cs
+++ b/src/JobsJobsJobs/Server/Areas/Api/Controllers/CitizenController.cs
@@ -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);
}
diff --git a/src/JobsJobsJobs/Server/Data/CitizenExtensions.cs b/src/JobsJobsJobs/Server/Data/CitizenExtensions.cs
index b2e4cd2..9a58071 100644
--- a/src/JobsJobsJobs/Server/Data/CitizenExtensions.cs
+++ b/src/JobsJobsJobs/Server/Data/CitizenExtensions.cs
@@ -20,7 +20,7 @@ namespace JobsJobsJobs.Server.Data
/// A populated citizen
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"));
///
/// 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);
}
diff --git a/src/JobsJobsJobs/Server/Data/NpgsqlDataReaderExtensions.cs b/src/JobsJobsJobs/Server/Data/NpgsqlDataReaderExtensions.cs
index 4d3f588..feaf76d 100644
--- a/src/JobsJobsJobs/Server/Data/NpgsqlDataReaderExtensions.cs
+++ b/src/JobsJobsJobs/Server/Data/NpgsqlDataReaderExtensions.cs
@@ -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
/// The specified field as a boolean
public static bool GetBoolean(this NpgsqlDataReader rdr, string name) => rdr.GetBoolean(rdr.GetOrdinal(name));
+ ///
+ /// Get an Instant by its name
+ ///
+ /// The name of the field to be retrieved as an Instant
+ /// The specified field as an Instant
+ public static Instant GetInstant(this NpgsqlDataReader rdr, string name) =>
+ rdr.GetFieldValue(rdr.GetOrdinal(name));
+
///
/// Get a 64-bit integer by its name
///
@@ -26,14 +35,6 @@ namespace JobsJobsJobs.Server.Data
/// The specified field as a 64-bit integer
public static long GetInt64(this NpgsqlDataReader rdr, string name) => rdr.GetInt64(rdr.GetOrdinal(name));
- ///
- /// Get milliseconds by its name
- ///
- /// The name of the field to be retrieved as milliseconds
- /// The specified field as milliseconds
- public static Milliseconds GetMilliseconds(this NpgsqlDataReader rdr, string name) =>
- new Milliseconds(rdr.GetInt64(name));
-
///
/// Get a string by its name
///
diff --git a/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs b/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
index b27d777..363ba32 100644
--- a/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
+++ b/src/JobsJobsJobs/Server/Data/ProfileExtensions.cs
@@ -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"))
diff --git a/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj b/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj
index e18f2c4..dd6057c 100644
--- a/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj
+++ b/src/JobsJobsJobs/Server/JobsJobsJobs.Server.csproj
@@ -7,8 +7,9 @@
-
-
+
+
+
diff --git a/src/JobsJobsJobs/Server/Startup.cs b/src/JobsJobsJobs/Server/Startup.cs
index af3d54a..5c8018d 100644
--- a/src/JobsJobsJobs/Server/Startup.cs
+++ b/src/JobsJobsJobs/Server/Startup.cs
@@ -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(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();
diff --git a/src/JobsJobsJobs/Shared/Domain/Citizen.cs b/src/JobsJobsJobs/Shared/Domain/Citizen.cs
index 65b205d..f2ff382 100644
--- a/src/JobsJobsJobs/Shared/Domain/Citizen.cs
+++ b/src/JobsJobsJobs/Shared/Domain/Citizen.cs
@@ -1,4 +1,6 @@
-namespace JobsJobsJobs.Shared
+using NodaTime;
+
+namespace JobsJobsJobs.Shared
{
///
/// A user of Jobs, Jobs, Jobs
@@ -8,6 +10,6 @@
string NaUser,
string DisplayName,
string ProfileUrl,
- Milliseconds JoinedOn,
- Milliseconds LastSeenOn);
+ Instant JoinedOn,
+ Instant LastSeenOn);
}
diff --git a/src/JobsJobsJobs/Shared/Domain/Milliseconds.cs b/src/JobsJobsJobs/Shared/Domain/Milliseconds.cs
deleted file mode 100644
index ac86b99..0000000
--- a/src/JobsJobsJobs/Shared/Domain/Milliseconds.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-
-namespace JobsJobsJobs.Shared
-{
- ///
- /// Milliseconds past the epoch (JavaScript's date storage format)
- ///
- public record Milliseconds(long Millis)
- {
- ///
- /// Get the milliseconds value for now
- ///
- /// A new milliseconds from the time now
- public static Milliseconds Now() =>
- new Milliseconds(
- (DateTime.UtcNow.Ticks - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks) / 10000L);
- }
-}
diff --git a/src/JobsJobsJobs/Shared/Domain/Profile.cs b/src/JobsJobsJobs/Shared/Domain/Profile.cs
index 7006af5..604cf9e 100644
--- a/src/JobsJobsJobs/Shared/Domain/Profile.cs
+++ b/src/JobsJobsJobs/Shared/Domain/Profile.cs
@@ -1,4 +1,6 @@
-namespace JobsJobsJobs.Shared
+using NodaTime;
+
+namespace JobsJobsJobs.Shared
{
///
/// A job seeker profile
@@ -12,7 +14,7 @@
bool RemoteWork,
bool FullTime,
MarkdownString Biography,
- Milliseconds LastUpdatedOn,
+ Instant LastUpdatedOn,
MarkdownString? Experience)
{
///
diff --git a/src/JobsJobsJobs/Shared/Domain/Success.cs b/src/JobsJobsJobs/Shared/Domain/Success.cs
index 2c2958b..502d379 100644
--- a/src/JobsJobsJobs/Shared/Domain/Success.cs
+++ b/src/JobsJobsJobs/Shared/Domain/Success.cs
@@ -1,4 +1,6 @@
-namespace JobsJobsJobs.Shared
+using NodaTime;
+
+namespace JobsJobsJobs.Shared
{
///
/// 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);
}
diff --git a/src/JobsJobsJobs/Shared/JobsJobsJobs.Shared.csproj b/src/JobsJobsJobs/Shared/JobsJobsJobs.Shared.csproj
index bd50b80..84ed6e0 100644
--- a/src/JobsJobsJobs/Shared/JobsJobsJobs.Shared.csproj
+++ b/src/JobsJobsJobs/Shared/JobsJobsJobs.Shared.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/database/tables.sql b/src/database/tables.sql
index 0231cc6..c660257 100644
--- a/src/database/tables.sql
+++ b/src/database/tables.sql
@@ -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),