Profile save/load works
Still need to add skills, but #2 is getting there...
This commit is contained in:
@@ -25,7 +25,7 @@ else
|
||||
@code {
|
||||
|
||||
bool retrievingProfile = true;
|
||||
JobsJobsJobs.Shared.Profile? profile = null;
|
||||
Profile? profile = null;
|
||||
string? errorMessage = null;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
|
||||
89
src/JobsJobsJobs/Client/Pages/Citizen/EditProfile.razor
Normal file
89
src/JobsJobsJobs/Client/Pages/Citizen/EditProfile.razor
Normal file
@@ -0,0 +1,89 @@
|
||||
@page "/citizen/profile"
|
||||
|
||||
<h3>Employment Profile</h3>
|
||||
|
||||
@if (ErrorMessage != "")
|
||||
{
|
||||
<p>@ErrorMessage</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm Model="@ProfileForm" OnValidSubmit="@SaveProfile">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="seeking" class="form-check-input" @bind-Value="@ProfileForm.IsSeekingEmployment" />
|
||||
<label for="seeking" class="form-check-label">I am currently seeking employment</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="continentId" class="jjj-required">Continent</label>
|
||||
<InputSelect id="continentId" @bind-Value="@ProfileForm.ContinentId" class="form-control">
|
||||
<option>– Select –</option>
|
||||
@foreach (var (id, name) in Continents)
|
||||
{
|
||||
<option value="@id">@name</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => ProfileForm.ContinentId)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-6 col-md-8">
|
||||
<div class="form-group">
|
||||
<label for="region" class="jjj-required">Region</label>
|
||||
<InputText id="region" @bind-Value="@ProfileForm.Region" class="form-control" />
|
||||
<ValidationMessage For="@(() => ProfileForm.Region)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="bio" class="jjj-required">Professional Biography</label>
|
||||
<MarkdownEditor Id="bio" @bind-Text="@ProfileForm.Biography" />
|
||||
<ValidationMessage For="@(() => ProfileForm.Biography)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-12 offset-md-2 col-md-4">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="isRemote" class="form-check-input" @bind-Value="@ProfileForm.RemoteWork" />
|
||||
<label for="isRemote" class="form-check-label">I am looking for remote work</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-12 col-md-4">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="isFull" class="form-check-input" @bind-Value="@ProfileForm.FullTime" />
|
||||
<label for="isFull" class="form-check-label">I am looking for full-time work</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label for="experience">Experience</label>
|
||||
<MarkdownEditor Id="experience" @bind-Text="@ProfileForm.Experience" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="isPublic" class="form-check-input" @bind-Value="@ProfileForm.IsPublic" />
|
||||
<label for="isPublic" class="form-check-label">
|
||||
Allow my profile to be searched publicly (outside NA Social)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<br>
|
||||
<button type="submit" class="btn btn-outline-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
84
src/JobsJobsJobs/Client/Pages/Citizen/EditProfile.razor.cs
Normal file
84
src/JobsJobsJobs/Client/Pages/Citizen/EditProfile.razor.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
{
|
||||
/// <summary>
|
||||
/// Profile edit page (called EditProfile so as not to create naming conflicts)
|
||||
/// </summary>
|
||||
public partial class EditProfile : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The form for this page
|
||||
/// </summary>
|
||||
private ProfileForm ProfileForm { get; set; } = new ProfileForm();
|
||||
|
||||
/// <summary>
|
||||
/// All continents
|
||||
/// </summary>
|
||||
private IEnumerable<Continent> Continents { get; set; } = Enumerable.Empty<Continent>();
|
||||
|
||||
/// <summary>
|
||||
/// Error message from API access
|
||||
/// </summary>
|
||||
private string ErrorMessage { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client instance to use for API access
|
||||
/// </summary>
|
||||
[Inject]
|
||||
private HttpClient Http { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Application state
|
||||
/// </summary>
|
||||
[Inject]
|
||||
private AppState State { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ServerApi.SetJwt(Http, State);
|
||||
var continentResult = await ServerApi.AllContinents(Http, State);
|
||||
if (continentResult.IsOk)
|
||||
{
|
||||
Continents = continentResult.Ok;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessage = continentResult.Error;
|
||||
}
|
||||
|
||||
var result = await ServerApi.RetrieveProfile(Http, State);
|
||||
if (result.IsOk)
|
||||
{
|
||||
System.Console.WriteLine($"Result is null? {result.Ok == null}");
|
||||
ProfileForm = (result.Ok == null) ? new ProfileForm() : ProfileForm.FromProfile(result.Ok);
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessage = result.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveProfile()
|
||||
{
|
||||
var res = await Http.PostAsJsonAsync("/api/profile/save", ProfileForm);
|
||||
if (res.IsSuccessStatusCode)
|
||||
{
|
||||
// TODO: success notification
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: probably not the best way to handle this...
|
||||
ErrorMessage = await res.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
@page "/citizen/profile"
|
||||
@using JobsJobsJobs.Client.ViewModels
|
||||
@inject HttpClient http
|
||||
@inject AppState state
|
||||
|
||||
<h3>Employment Profile</h3>
|
||||
|
||||
@if (errorMessage != "")
|
||||
{
|
||||
<p>@errorMessage</p>
|
||||
}
|
||||
else if (profileForm != null)
|
||||
{
|
||||
<EditForm Model="@profileForm" OnValidSubmit="@SaveProfile">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-12 col-md-4">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="seeking" class="form-check-input" @bind-Value="@profileForm.IsSeekingEmployment" />
|
||||
<label for="seeking" class="form-check-label">I am currently seeking employment</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-12 col-md-8">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="isPublic" class="form-check-input" @bind-Value="@profileForm.IsPublic" />
|
||||
<label for="isPublic" class="form-check-label">
|
||||
Allow my profile to be searched publicly (outside NA Social)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-12 col-md-4">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="isRemote" class="form-check-input" @bind-Value="@profileForm.RemoteWork" />
|
||||
<label for="isRemote" class="form-check-label">I am looking for remote work</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-12 col-md-8">
|
||||
<div class="form-check">
|
||||
<InputCheckbox id="isFull" class="form-check-input" @bind-Value="@profileForm.FullTime" />
|
||||
<label for="isFull" class="form-check-label">I am looking for full-time work</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="continentId">Continent</label>
|
||||
<InputSelect id="continentId" @bind-Value="@profileForm.ContinentId" class="form-control">
|
||||
<option>– Select –</option>
|
||||
@foreach (var (id, name) in continents)
|
||||
{
|
||||
<option value="@id">@name</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => profileForm.ContinentId)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-6 col-md-8">
|
||||
<div class="form-group">
|
||||
<label for="region">Region</label>
|
||||
<InputText id="region" @bind-Value="@profileForm.Region" class="form-control" />
|
||||
<ValidationMessage For="@(() => profileForm.Region)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="bio">Professional Biography</label>
|
||||
<MarkdownEditor Id="bio" @bind-Text="@profileForm.Biography" />
|
||||
<ValidationMessage For="@(() => profileForm.Biography)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label for="experience">Experience</label>
|
||||
<MarkdownEditor Id="experience" @bind-Text="@profileForm.Experience" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<br>
|
||||
<button type="submit" class="btn btn-outline-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
public JobsJobsJobs.Shared.Profile? profile = null;
|
||||
|
||||
public ProfileForm? profileForm = null;
|
||||
|
||||
private IEnumerable<Continent> continents = Enumerable.Empty<Continent>();
|
||||
|
||||
public string errorMessage = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var continentResult = await ServerApi.AllContinents(http, state);
|
||||
if (continentResult.IsOk)
|
||||
{
|
||||
continents = continentResult.Ok;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = continentResult.Error;
|
||||
}
|
||||
|
||||
var result = await ServerApi.RetrieveProfile(http, state);
|
||||
if (result.IsOk)
|
||||
{
|
||||
profile = result.Ok;
|
||||
profileForm = (profile == null) ? new ProfileForm() : ProfileForm.FromProfile(profile);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = result.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveProfile()
|
||||
{
|
||||
// TODO: save profile
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -7,6 +9,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client
|
||||
@@ -16,6 +19,25 @@ namespace JobsJobsJobs.Client
|
||||
/// </summary>
|
||||
public static class ServerApi
|
||||
{
|
||||
/// <summary>
|
||||
/// System.Text.Json options configured for NodaTime
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Static constructor
|
||||
/// </summary>
|
||||
static ServerApi()
|
||||
{
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
options.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
_serializerOptions = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an API URL
|
||||
/// </summary>
|
||||
@@ -37,6 +59,14 @@ namespace JobsJobsJobs.Client
|
||||
return req;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the JSON Web Token (JWT) bearer header for the given HTTP client
|
||||
/// </summary>
|
||||
/// <param name="http">The HTTP client whose authentication header should be set</param>
|
||||
/// <param name="state">The current application state</param>
|
||||
public static void SetJwt(HttpClient http, AppState state) =>
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", state.Jwt);
|
||||
|
||||
/// <summary>
|
||||
/// Log on a user with the authorization code received from No Agenda Social
|
||||
/// </summary>
|
||||
@@ -73,7 +103,8 @@ namespace JobsJobsJobs.Client
|
||||
return true switch
|
||||
{
|
||||
_ when res.StatusCode == HttpStatusCode.NoContent => Result<Profile?>.AsOk(null),
|
||||
_ when res.IsSuccessStatusCode => Result<Profile?>.AsOk(await res.Content.ReadFromJsonAsync<Profile>()),
|
||||
_ when res.IsSuccessStatusCode => Result<Profile?>.AsOk(
|
||||
await res.Content.ReadFromJsonAsync<Profile>(_serializerOptions)),
|
||||
_ => Result<Profile?>.AsError(await res.Content.ReadAsStringAsync()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
section.preview {
|
||||
border: solid 1px darkgray;
|
||||
border-radius: 2rem;
|
||||
border-radius: .5rem;
|
||||
padding: .25rem;
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsJobsJobs.Client.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// The data required to update a profile
|
||||
/// </summary>
|
||||
public class ProfileForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the citizen to whom this profile belongs is actively seeking employment
|
||||
/// </summary>
|
||||
public bool IsSeekingEmployment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this profile should appear in the public search
|
||||
/// </summary>
|
||||
public bool IsPublic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the continent on which the citizen is located
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(12, MinimumLength = 1)]
|
||||
[Display(Name = "Continent")]
|
||||
public string ContinentId { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The area within that continent where the citizen is located
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(255)]
|
||||
public string Region { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// If the citizen is available for remote work
|
||||
/// </summary>
|
||||
public bool RemoteWork { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the citizen is seeking full-time employment
|
||||
/// </summary>
|
||||
public bool FullTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's professional biography
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Biography { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The user's past experience
|
||||
/// </summary>
|
||||
public string Experience { get; set; } = "";
|
||||
|
||||
public static ProfileForm FromProfile(Profile profile) =>
|
||||
new ProfileForm
|
||||
{
|
||||
IsSeekingEmployment = profile.SeekingEmployment,
|
||||
IsPublic = profile.IsPublic,
|
||||
ContinentId = profile.ContinentId.ToString(),
|
||||
Region = profile.Region,
|
||||
RemoteWork = profile.RemoteWork,
|
||||
FullTime = profile.FullTime,
|
||||
Biography = profile.Biography.Text,
|
||||
Experience = profile.Experience?.Text ?? ""
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -56,3 +56,12 @@ a.audio {
|
||||
a.audio:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label.jjj-required {
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
label.jjj-required::after {
|
||||
color: red;
|
||||
content: ' *';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user