Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e30e28c279 | |||
| b98d28adb4 | |||
| 60ed7e1e79 | |||
| fb147888c5 | |||
| 4a73927e64 | |||
| 93c99b430f | |||
| 9e9b519c25 | |||
| d6f4628e1c | |||
| e63a12b774 | |||
| 8350d0bddf | |||
| 1b2540e5d2 | |||
| 21ba35408e | |||
| 7784ee66ea | |||
| 7c6804c2af | |||
| a5c88a1034 | |||
| 46882bdfc6 | |||
| a6fd891cc5 | |||
| 7f7eb191fb | |||
| 340b93c6d7 | |||
| 4155072990 | |||
| feb3c5fd4a | |||
| 7839b8eb57 | |||
| 15c1a3ff2c |
@@ -1,3 +1,5 @@
|
|||||||
# jobs-jobs-jobs
|
# Jobs, Jobs, Jobs <small>_(and Jobs - Let's Vote for Jobs!)_</small>
|
||||||
|
|
||||||
Repository for the development of No Agenda Jobs - currently parked [here](http://jobs.bitbadger.solutions)
|
Source repository for **Jobs, Jobs, Jobs**, the jobs and career site for No Agenda nation.
|
||||||
|
|
||||||
|
What is No Agenda? [So glad you asked!](https://noagendashow.net)
|
||||||
|
|||||||
2
src/.dockerignore
Normal file
2
src/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
10
src/Dockerfile
Normal file
10
src/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||||
|
WORKDIR /jjj
|
||||||
|
COPY . ./
|
||||||
|
WORKDIR /jjj/JobsJobsJobs/Server
|
||||||
|
RUN dotnet publish JobsJobsJobs.Server.csproj -c Release /p:PublishProfile=Properties/PublishProfiles/FolderProfile.xml
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:5.0
|
||||||
|
WORKDIR /jjj
|
||||||
|
COPY --from=build /jjj/JobsJobsJobs/Server/bin/Release/net5.0/linux-x64/publish/ ./
|
||||||
|
ENTRYPOINT [ "/jjj/JobsJobsJobs.Server" ]
|
||||||
@@ -11,6 +11,10 @@ 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
|
||||||
|
.dockerignore = .dockerignore
|
||||||
|
database\12-add-real-name.sql = database\12-add-real-name.sql
|
||||||
|
JobsJobsJobs\Directory.Build.props = JobsJobsJobs\Directory.Build.props
|
||||||
|
Dockerfile = Dockerfile
|
||||||
database\tables.sql = database\tables.sql
|
database\tables.sql = database\tables.sql
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
using JobsJobsJobs.Shared;
|
using JobsJobsJobs.Shared;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using NodaTime;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Client
|
namespace JobsJobsJobs.Client
|
||||||
{
|
{
|
||||||
@@ -13,6 +19,17 @@ namespace JobsJobsJobs.Client
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppState
|
public class AppState
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The application version, as a nice display string
|
||||||
|
/// </summary>
|
||||||
|
public static Lazy<string> Version => new(() =>
|
||||||
|
{
|
||||||
|
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
var display = $"v{version.Major}.{version.Minor}";
|
||||||
|
if (version.Build > 0) display += $".{version.Build}";
|
||||||
|
return display;
|
||||||
|
});
|
||||||
|
|
||||||
public event Action OnChange = () => { };
|
public event Action OnChange = () => { };
|
||||||
|
|
||||||
private UserInfo? _user = null;
|
private UserInfo? _user = null;
|
||||||
@@ -45,6 +62,59 @@ namespace JobsJobsJobs.Client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Continent>? _continents = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a list of continents (only retrieves once per application load)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="http">The HTTP client to use to obtain continents the first time</param>
|
||||||
|
/// <returns>The list of continents</returns>
|
||||||
|
/// <exception cref="ApplicationException">If the continents cannot be loaded</exception>
|
||||||
|
public async Task<IEnumerable<Continent>> GetContinents(HttpClient http)
|
||||||
|
{
|
||||||
|
if (_continents == null)
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, this);
|
||||||
|
var continentResult = await ServerApi.RetrieveMany<Continent>(http, "continent/all");
|
||||||
|
|
||||||
|
if (continentResult.IsOk)
|
||||||
|
{
|
||||||
|
_continents = continentResult.Ok;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ApplicationException($"Could not load continents - {continentResult.Error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _continents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTimeZone? _tz = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the time zone for the current user's browser
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="js">The JS interop runtime for the application</param>
|
||||||
|
/// <returns>The time zone based on the user's browser</returns>
|
||||||
|
public async Task<DateTimeZone> GetTimeZone(IJSRuntime js)
|
||||||
|
{
|
||||||
|
if (_tz == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(await js.InvokeAsync<string>("getTimeZone"));
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
}
|
||||||
|
if (_tz == null)
|
||||||
|
{
|
||||||
|
// Either the zone wasn't found, or the user's browser denied us access to it; there's not much to do
|
||||||
|
// here but set it to UTC and move on
|
||||||
|
_tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull("Etc/UTC")!;
|
||||||
|
}
|
||||||
|
return _tz;
|
||||||
|
}
|
||||||
|
|
||||||
public AppState() { }
|
public AppState() { }
|
||||||
|
|
||||||
private void NotifyChanged() => OnChange.Invoke();
|
private void NotifyChanged() => OnChange.Invoke();
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
@page "/citizen/dashboard"
|
|
||||||
@inject HttpClient http
|
|
||||||
@inject AppState state
|
|
||||||
|
|
||||||
<PageTitle Title="Dashboard" />
|
|
||||||
|
|
||||||
<h3>Welcome, @state.User!.Name!</h3>
|
|
||||||
|
|
||||||
@if (RetrievingData)
|
|
||||||
{
|
|
||||||
<p>Retrieving your employment profile...</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<ErrorList Errors=@ErrorMessages>
|
|
||||||
@if (Profile != null)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
Your employment profile was last updated <FullDateTime TheDate=@Profile.LastUpdatedOn />. Your profile currently
|
|
||||||
lists @Profile.Skills.Length skill@(Profile.Skills.Length != 1 ? "s" : "").
|
|
||||||
</p>
|
|
||||||
<p><a href="/profile/view/@state.User.Id">View Your Employment Profile</a></p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
You do not have an employment profile established; click “Edit Profile” in the menu to get
|
|
||||||
started!
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
@{
|
|
||||||
/**
|
|
||||||
This is phase 3 stuff...
|
|
||||||
<hr>
|
|
||||||
<p>
|
|
||||||
There @(ProfileCount == 1 ? "is" : "are") @(ProfileCount == 0 ? "no" : ProfileCount) employment
|
|
||||||
profile@(ProfileCount != 1 ? "s" : "") from citizens of Gitmo Nation.
|
|
||||||
@if (ProfileCount > 0)
|
|
||||||
{
|
|
||||||
<text>Take a look around and see if you can help them find work!</text> <em>(coming soon)</em>
|
|
||||||
}
|
|
||||||
</p> */
|
|
||||||
}
|
|
||||||
</ErrorList>
|
|
||||||
}
|
|
||||||
<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
|
|
||||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/1" target="_blank">Phase 1</a>, which enabled users of
|
|
||||||
No Agenda Social to create accounts here, simply by allowing the application read access to their profiles. Unless
|
|
||||||
there are requests for tighter integration with that site, this is the only access to your NAS information that this
|
|
||||||
application will require.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/2" target="_blank">Phase 2</a> allows you to complete
|
|
||||||
your employment profile. Much of this is straightforward, but there are a few things you might want to know before
|
|
||||||
you fill it out:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
The “View Your Employment Profile” link (which you”ll see on this page, once your profile is
|
|
||||||
established) shows your profile the way all other validated users will be able to see it (once
|
|
||||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/3" target="_blank">Phase 3</a> is complete). While
|
|
||||||
this site does not perform communication with others over No Agenda Social, the name on employment profiles is a
|
|
||||||
link to that user’s profile; from there, others can communicate further with you using the tools Mastodon
|
|
||||||
provides.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The “Professional Biography” and “Experience” sections support Markdown, a plain-text way
|
|
||||||
to specify formatting quite similar to that provided by word processors. The
|
|
||||||
<a href="https://daringfireball.net/projects/markdown/" target="_blank">original page</a> for the project is a good
|
|
||||||
overview of its capabilities, and the pages at <a href="https://www.markdownguide.org/" target="_blank">Markdown
|
|
||||||
Guide</a> give in-depth lessons to make the most of this language. The version of Markdown employed here supports
|
|
||||||
many popular extensions, include smart quotes (turning "a quote" into “a quote”), tables,
|
|
||||||
super/subscripts, and more.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Skills are optional, but they are the place to record skills you have. Along with the skill, there is a
|
|
||||||
“Notes” section, which can be used to indicate the time you’ve practiced a particular skill, the
|
|
||||||
mastery you have of that skill, etc. It is free-form text, so it is all up to you how you utilize the field.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The “Experience” field is intended to capture a chronological or topical employment history; with this
|
|
||||||
“quick-n-dirty” implementation, this Markdown box can be used to capture that information however you
|
|
||||||
would like it presented to fellow citizens.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/5" target="_blank">Phase 5</a> includes allowing
|
|
||||||
public access to the continent, region, and skills fields of Gitmo Nation citizens who indicate that they are both
|
|
||||||
seeking employment <strong>and</strong> want their information disclosed to public users. The “Allow my
|
|
||||||
profile to be searched publicly” checkbox, at the bottom of the page where you edit your employment profile,
|
|
||||||
is how you opt your profile in to this list.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
@page "/citizen/profile"
|
|
||||||
@inject HttpClient http
|
|
||||||
@inject AppState state
|
|
||||||
@inject NavigationManager nav
|
|
||||||
@inject IToastService toast
|
|
||||||
|
|
||||||
<PageTitle Title="Edit Profile" />
|
|
||||||
|
|
||||||
<h3>Employment Profile</h3>
|
|
||||||
|
|
||||||
@if (AllLoaded)
|
|
||||||
{
|
|
||||||
<ErrorList Errors=@ErrorMessages>
|
|
||||||
<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"
|
|
||||||
placeholder="Country, state, geographic area, etc." />
|
|
||||||
<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>
|
|
||||||
<hr>
|
|
||||||
<h4>
|
|
||||||
Skills
|
|
||||||
<button type="button" class="btn btn-outline-primary" @onclick=@AddNewSkill>Add a Skill</button>
|
|
||||||
</h4>
|
|
||||||
@foreach (var skill in ProfileForm.Skills)
|
|
||||||
{
|
|
||||||
<SkillEdit Skill=@skill OnRemove=@RemoveSkill />
|
|
||||||
}
|
|
||||||
<hr>
|
|
||||||
<h4>Experience</h4>
|
|
||||||
<p>
|
|
||||||
This application does not have a place to individually list your chronological job history; however, you can
|
|
||||||
use this area to list prior jobs, their dates, and anything else you want to include that’s not already a
|
|
||||||
part of your Professional Biography above.
|
|
||||||
</p>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="col">
|
|
||||||
<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>
|
|
||||||
@if (!IsNew)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
<br><a href="/profile/view/@state.User!.Id"><span class="oi oi-file"></span> View Your User Profile</a>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</ErrorList>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>Loading your profile...</p>
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
@inject NavigationManager nav
|
@inject NavigationManager nav
|
||||||
@inject AppState state
|
@inject AppState state
|
||||||
|
|
||||||
<PageTitle Title="Logging in..." />
|
<PageTitle Title="Logging on..." />
|
||||||
|
|
||||||
<p>@Message</p>
|
<p>@Message</p>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Client.Pages.Citizen
|
namespace JobsJobsJobs.Client.Pages.Citizens
|
||||||
{
|
{
|
||||||
public partial class Authorized : ComponentBase
|
public partial class Authorized : ComponentBase
|
||||||
{
|
{
|
||||||
46
src/JobsJobsJobs/Client/Pages/Citizens/Dashboard.razor
Normal file
46
src/JobsJobsJobs/Client/Pages/Citizens/Dashboard.razor
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@page "/citizen/dashboard"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
<PageTitle Title="Dashboard" />
|
||||||
|
|
||||||
|
<h3>Welcome, @state.User!.Name!</h3>
|
||||||
|
|
||||||
|
<Loading OnLoad=@LoadProfile Message=@(new MarkupString("Retrieving your employment profile…"))>
|
||||||
|
@if (Profile != null)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
Your employment profile was last updated <FullDateTime TheDate=@Profile.LastUpdatedOn />. Your profile currently
|
||||||
|
lists @Profile.Skills.Count skill@(Profile.Skills.Count != 1 ? "s" : "").
|
||||||
|
</p>
|
||||||
|
<p><a href="/profile/view/@state.User.Id">View Your Employment Profile</a></p>
|
||||||
|
@if (Profile.SeekingEmployment)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
Your profile indicates that you are seeking employment. Once you find it,
|
||||||
|
<a href="/success-story/add">tell your fellow citizens about it!</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
You do not have an employment profile established; click “Edit Profile” in the menu to get
|
||||||
|
started!
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
There @(ProfileCount == 1 ? "is" : "are") @(ProfileCount == 0 ? "no" : ProfileCount) employment
|
||||||
|
profile@(ProfileCount != 1 ? "s" : "") from citizens of Gitmo Nation.
|
||||||
|
@if (ProfileCount > 0)
|
||||||
|
{
|
||||||
|
<text>Take a look around and see if you can help them find work!</text>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</Loading>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
To see how this application works, check out “How It Works” in the sidebar (last updated June
|
||||||
|
14<sup>th</sup>, 2021).
|
||||||
|
</p>
|
||||||
@@ -1,25 +1,20 @@
|
|||||||
using JobsJobsJobs.Shared.Api;
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Domain = JobsJobsJobs.Shared;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Client.Pages.Citizen
|
namespace JobsJobsJobs.Client.Pages.Citizens
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The first page a user sees after signing in
|
/// The first page a user sees after signing in
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class Dashboard : ComponentBase
|
public partial class Dashboard : ComponentBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether the data is being retrieved
|
|
||||||
/// </summary>
|
|
||||||
private bool RetrievingData { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The user's profile
|
/// The user's profile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Domain.Profile? Profile { get; set; } = null;
|
private Profile? Profile { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of profiles
|
/// The number of profiles
|
||||||
@@ -27,11 +22,10 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
private int ProfileCount { get; set; }
|
private int ProfileCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error messages from data access
|
/// Load the user's profile information
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
/// <param name="errors">A collection to report errors that may be encountered</param>
|
||||||
|
public async Task LoadProfile(ICollection<string> errors)
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
{
|
||||||
if (state.User != null)
|
if (state.User != null)
|
||||||
{
|
{
|
||||||
@@ -47,7 +41,7 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ErrorMessages.Add(profileTask.Result.Error);
|
errors.Add(profileTask.Result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileCountTask.Result.IsOk)
|
if (profileCountTask.Result.IsOk)
|
||||||
@@ -56,10 +50,8 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ErrorMessages.Add(profileCountTask.Result.Error);
|
errors.Add(profileCountTask.Result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
RetrievingData = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
132
src/JobsJobsJobs/Client/Pages/Citizens/EditProfile.razor
Normal file
132
src/JobsJobsJobs/Client/Pages/Citizens/EditProfile.razor
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@page "/citizen/profile"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@inject IToastService toast
|
||||||
|
|
||||||
|
<PageTitle Title="Edit Profile" />
|
||||||
|
|
||||||
|
<h3>Employment Profile</h3>
|
||||||
|
|
||||||
|
<Loading OnLoad=@SetUpProfile Message=@(new MarkupString("Loading Your Profile…"))>
|
||||||
|
<EditForm Model=@ProfileForm OnValidSubmit=@SaveProfile>
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col col-xs-12 col-sm-10 col-md-8 col-lg-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="realName" class="jjj-label">Real Name</label>
|
||||||
|
<InputText id="realName" @bind-Value=@ProfileForm.RealName class="form-control"
|
||||||
|
placeholder="Leave blank to use your NAS display name" />
|
||||||
|
<ValidationMessage For=@(() => ProfileForm.RealName) />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
@if (IsSeeking)
|
||||||
|
{
|
||||||
|
<em> If you have found employment, consider
|
||||||
|
<a href="/success-story/add">telling your fellow citizens about it</a>
|
||||||
|
</em>
|
||||||
|
}
|
||||||
|
</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"
|
||||||
|
placeholder="Country, state, geographic area, etc." />
|
||||||
|
<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>
|
||||||
|
<hr>
|
||||||
|
<h4>
|
||||||
|
Skills
|
||||||
|
<button type="button" class="btn btn-outline-primary" @onclick=@AddNewSkill>Add a Skill</button>
|
||||||
|
</h4>
|
||||||
|
@foreach (var skill in ProfileForm.Skills)
|
||||||
|
{
|
||||||
|
<SkillEdit Skill=@skill OnRemove=@RemoveSkill />
|
||||||
|
}
|
||||||
|
<hr>
|
||||||
|
<h4>Experience</h4>
|
||||||
|
<p>
|
||||||
|
This application does not have a place to individually list your chronological job history; however, you can
|
||||||
|
use this area to list prior jobs, their dates, and anything else you want to include that’s not already a
|
||||||
|
part of your Professional Biography above.
|
||||||
|
</p>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<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>
|
||||||
|
@if (!IsNew)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<br><a href="/profile/view/@state.User!.Id"><span class="oi oi-file"></span> View Your User Profile</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<p>
|
||||||
|
<br>If you want to delete your profile, or your entire account,
|
||||||
|
<a href="/so-long/options">see your deletion options here</a>.
|
||||||
|
</p>
|
||||||
|
</Loading>
|
||||||
@@ -6,7 +6,7 @@ using System.Linq;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Client.Pages.Citizen
|
namespace JobsJobsJobs.Client.Pages.Citizens
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Profile edit page (called EditProfile so as not to create naming conflicts)
|
/// Profile edit page (called EditProfile so as not to create naming conflicts)
|
||||||
@@ -19,9 +19,10 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
private int _newSkillCounter = 0;
|
private int _newSkillCounter = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A flag that indicates all the required API calls have completed, and the form is ready to be displayed
|
/// Whether the citizen is seeking employment at the time the profile is loaded (used to show success story
|
||||||
|
/// link)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool AllLoaded { get; set; } = false;
|
private bool IsSeeking { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The form for this page
|
/// The form for this page
|
||||||
@@ -39,26 +40,19 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
private bool IsNew { get; set; } = false;
|
private bool IsNew { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error messages from API access
|
/// Set up the data needed to add or edit the user's profile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
/// <param name="errors">The collection where errors can be reported</param>
|
||||||
|
public async Task SetUpProfile(ICollection<string> errors)
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
{
|
||||||
ServerApi.SetJwt(http, state);
|
ServerApi.SetJwt(http, state);
|
||||||
var continentTask = ServerApi.RetrieveMany<Continent>(http, "continent/all");
|
var continentTask = state.GetContinents(http);
|
||||||
var profileTask = ServerApi.RetrieveProfile(http, state);
|
var profileTask = ServerApi.RetrieveProfile(http, state);
|
||||||
|
var citizenTask = ServerApi.RetrieveOne<Citizen>(http, $"citizen/get/{state.User!.Id}");
|
||||||
|
|
||||||
await Task.WhenAll(continentTask, profileTask);
|
await Task.WhenAll(continentTask, profileTask, citizenTask);
|
||||||
|
|
||||||
if (continentTask.Result.IsOk)
|
Continents = continentTask.Result;
|
||||||
{
|
|
||||||
Continents = continentTask.Result.Ok;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ErrorMessages.Add(continentTask.Result.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profileTask.Result.IsOk)
|
if (profileTask.Result.IsOk)
|
||||||
{
|
{
|
||||||
@@ -70,15 +64,23 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ProfileForm = ProfileForm.FromProfile(profileTask.Result.Ok);
|
ProfileForm = ProfileForm.FromProfile(profileTask.Result.Ok);
|
||||||
|
IsSeeking = profileTask.Result.Ok.SeekingEmployment;
|
||||||
}
|
}
|
||||||
if (ProfileForm.Skills.Count == 0) AddNewSkill();
|
if (ProfileForm.Skills.Count == 0) AddNewSkill();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ErrorMessages.Add(profileTask.Result.Error);
|
errors.Add(profileTask.Result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
AllLoaded = true;
|
if (citizenTask.Result.IsOk)
|
||||||
|
{
|
||||||
|
ProfileForm.RealName = citizenTask.Result.Ok!.RealName ?? "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add(citizenTask.Result.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -106,18 +108,27 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
|||||||
foreach (var blankSkill in blankSkills) ProfileForm.Skills.Remove(blankSkill);
|
foreach (var blankSkill in blankSkills) ProfileForm.Skills.Remove(blankSkill);
|
||||||
|
|
||||||
var res = await http.PostAsJsonAsync("/api/profile/save", ProfileForm);
|
var res = await http.PostAsJsonAsync("/api/profile/save", ProfileForm);
|
||||||
if (res.IsSuccessStatusCode)
|
if (res.IsSuccessStatusCode && state.User != null)
|
||||||
{
|
{
|
||||||
|
var citizen = await ServerApi.RetrieveOne<Citizen>(http, $"citizen/get/{state.User.Id}");
|
||||||
|
|
||||||
|
if (citizen.IsOk)
|
||||||
|
{
|
||||||
|
state.User = state.User with { Name = citizen.Ok!.CitizenName };
|
||||||
toast.ShowSuccess("Profile Saved Successfully");
|
toast.ShowSuccess("Profile Saved Successfully");
|
||||||
nav.NavigateTo($"/profile/view/{state.User!.Id}");
|
nav.NavigateTo($"/profile/view/{state.User!.Id}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
toast.ShowError(citizen.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
var error = await res.Content.ReadAsStringAsync();
|
var error = await res.Content.ReadAsStringAsync();
|
||||||
if (!string.IsNullOrEmpty(error)) error = $"- {error}";
|
if (!string.IsNullOrEmpty(error)) error = $"- {error}";
|
||||||
toast.ShowError($"{(int)res.StatusCode} {error}");
|
toast.ShowError($"{(int)res.StatusCode} {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
src/JobsJobsJobs/Client/Pages/Citizens/LogOff.razor
Normal file
13
src/JobsJobsJobs/Client/Pages/Citizens/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("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/JobsJobsJobs/Client/Pages/HowItWorks.razor
Normal file
81
src/JobsJobsJobs/Client/Pages/HowItWorks.razor
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
@page "/how-it-works"
|
||||||
|
|
||||||
|
<PageTitle Title="How It Works" />
|
||||||
|
|
||||||
|
<h3>How It Works</h3>
|
||||||
|
|
||||||
|
<h4>Completing Your Profile</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
The “View Your Employment Profile” link (which you”ll see on this page, once your profile is
|
||||||
|
established) shows your profile the way all other validated users will be able to see it. While this site does not
|
||||||
|
perform communication with others over No Agenda Social, the name on employment profiles is a link to that
|
||||||
|
user’s profile; from there, others can communicate further with you using the tools Mastodon provides.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The “Professional Biography” and “Experience” sections support Markdown, a plain-text way
|
||||||
|
to specify formatting quite similar to that provided by word processors. The
|
||||||
|
<a href="https://daringfireball.net/projects/markdown/" target="_blank">original page</a> for the project is a good
|
||||||
|
overview of its capabilities, and the pages at
|
||||||
|
<a href="https://www.markdownguide.org/" target="_blank">Markdown Guide</a> give in-depth lessons to make the most
|
||||||
|
of this language. The version of Markdown employed here supports many popular extensions, include smart quotes
|
||||||
|
(turning "a quote" into “a quote”), tables, super/subscripts, and more.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Skills are optional, but they are the place to record skills you have. Along with the skill, there is a
|
||||||
|
“Notes” section, which can be used to indicate the time you’ve practiced a particular skill, the
|
||||||
|
mastery you have of that skill, etc. It is free-form text, so it is all up to you how you utilize the field.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The “Experience” field is intended to capture a chronological or topical employment history; with this
|
||||||
|
“quick-n-dirty” implementation, this Markdown box can be used to capture that information however you
|
||||||
|
would like it presented to fellow citizens.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If you check the “Allow my profile to be searched publicly” checkbox, <strong>and</strong> you are
|
||||||
|
seeking employment, your continent, region, and skills fields will be searchable and displayed to public users of
|
||||||
|
the site. They will not be tied to your No Agenda Social handle or real name; they are there to let people peek
|
||||||
|
behind the curtain a bit, and hopefully inspire them to join us.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Searching Profiles</h4>
|
||||||
|
<p>
|
||||||
|
The “View Profiles” link at the side allows you to search for profiles by continent, the citizen’s
|
||||||
|
desire for remote work, a skill, or any text in their professional biography and experience. If you find someone with
|
||||||
|
whom you’d like to discuss potential opportunities, the name at the top of the profile links to their No Agenda
|
||||||
|
Social account, where you can use its features to get in touch.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Finding Employment</h4>
|
||||||
|
<p>
|
||||||
|
If your profile indicates that you are seeking employment, and you secure employment, that is something you will
|
||||||
|
want to update (and – congratulations!). From both the Dashboard and the Edit Profile pages, you will see a
|
||||||
|
link that encourages you to tell us about it. Click either of those links, and you will be brought to a page that
|
||||||
|
allows you to indicate whether your employment actually came from someone finding your profile on Jobs, Jobs, Jobs,
|
||||||
|
and gives you a place to write about the experience. These stories are only viewable by validated users, so feel free
|
||||||
|
to use as much (or as little) identifying information as you’d like. You can also submit this page with all the
|
||||||
|
fields blank; in that case, your “Seeking Employment” flag is cleared, and the “story” is
|
||||||
|
recorded.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
As a validated user, you can also view others success stories. Clicking “Success Stories” in the sidebar
|
||||||
|
will display a list of all the stories that have been recorded. If there is a story to be read, there will be a link
|
||||||
|
to read it; if you submitted the story, there will also be an “Edit” link.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Publicly Available Information</h4>
|
||||||
|
<p>
|
||||||
|
The “Job Seekers” page for profile information will allow users to search for and display the continent,
|
||||||
|
region, skills, and notes of users who are seeking employment <strong>and</strong> have opted in to their information
|
||||||
|
being publicly searchable. If you are a public user, this information is always the latest we have; check out the link
|
||||||
|
at the top of the search results for how you can learn more about these fine human resources!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Help / Suggestions</h4>
|
||||||
|
<p>
|
||||||
|
This is open-source software
|
||||||
|
<a href="https://github.com/bit-badger/jobs-jobs-jobs" _target="_blank">developed on Github</a>; feel free to
|
||||||
|
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank">create an issue there</a>, or look up
|
||||||
|
@("@")danieljsummers on No Agenda Social.
|
||||||
|
</p>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<PageTitle Title="Welcome!" />
|
<PageTitle Title="Welcome!" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Future home of No Agenda Jobs, where citizens of Gitmo Nation can assist one another in finding or enhancing their
|
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in finding
|
||||||
employment. This will enable them to continue providing value for value to Adam and John, as they continue their work
|
employment. This will enable them to continue providing value-for-value to Adam and John, as they continue their work
|
||||||
deconstructing the misinformation that passes for news on a day-to-day basis.
|
deconstructing the misinformation that passes for news on a day-to-day basis.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
413
src/JobsJobsJobs/Client/Pages/PrivacyPolicy.razor
Normal file
413
src/JobsJobsJobs/Client/Pages/PrivacyPolicy.razor
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
@page "/privacy-policy"
|
||||||
|
|
||||||
|
<PageTitle Title="Privacy Policy" />
|
||||||
|
|
||||||
|
<h3>Privacy Policy</h3>
|
||||||
|
<p><em>(as of February 6<sup>th</sup>, 2021)</em></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
@Name (“we,” “our,” or “us”) is committed to protecting your privacy. This Privacy Policy explains how your personal
|
||||||
|
information is collected, used, and disclosed by @Name.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This Privacy Policy applies to our website, and its associated subdomains (collectively, our “Service”) alongside
|
||||||
|
our application, @Name. By accessing or using our Service, you signify that you have read, understood, and agree to
|
||||||
|
our collection, storage, use, and disclosure of your personal information as described in this Privacy Policy and our
|
||||||
|
Terms of Service.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Definitions and key terms</h4>
|
||||||
|
<p>
|
||||||
|
To help explain things as clearly as possible in this Privacy Policy, every time any of these terms are referenced,
|
||||||
|
are strictly defined as:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Cookie: small amount of data generated by a website and saved by your web browser. It is used to identify your
|
||||||
|
browser, provide analytics, remember information about you such as your language preference or login information.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Company: when this policy mentions “Company,” “we,” “us,” or “our,” it refers to @Name, that is responsible for
|
||||||
|
your information under this Privacy Policy.
|
||||||
|
</li>
|
||||||
|
<li>Country: where @Name or the owners/founders of @Name are based, in this case is US.</li>
|
||||||
|
<li>
|
||||||
|
Customer: refers to the company, organization or person that signs up to use the @Name Service to manage the
|
||||||
|
relationships with your consumers or service users.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Device: any internet connected device such as a phone, tablet, computer or any other device that can be used to
|
||||||
|
visit @Name and use the services.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
IP address: Every device connected to the Internet is assigned a number known as an Internet protocol (IP) address.
|
||||||
|
These numbers are usually assigned in geographic blocks. An IP address can often be used to identify the location
|
||||||
|
from which a device is connecting to the Internet.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Personnel: refers to those individuals who are employed by @Name or are under contract to perform a service on
|
||||||
|
behalf of one of the parties.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Personal Data: any information that directly, indirectly, or in connection with other information — including a
|
||||||
|
personal identification number — allows for the identification or identifiability of a natural person.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Service: refers to the service provided by @Name as described in the relative terms (if available) and on this
|
||||||
|
platform.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and others who
|
||||||
|
provide our content or whose products or services we think may interest you.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Website: @Name’s site, which can be accessed via this URL: <a href="/">https://noagendacareers.com/</a>
|
||||||
|
</li>
|
||||||
|
<li>You: a person or entity that is registered with @Name to use the Services.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>What Information Do We Collect?</h4>
|
||||||
|
<p>
|
||||||
|
We collect information from you when you visit our website, register on our site, or fill out a form.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Name / Username</li>
|
||||||
|
<li>Coarse Geographic Location</li>
|
||||||
|
<li>Employment History</li>
|
||||||
|
<li>No Agenda Social Account Name / Profile</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>How Do We Use The Information We Collect?</h4>
|
||||||
|
<p>
|
||||||
|
Any of the information we collect from you may be used in one of the following ways:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>To personalize your experience (your information helps us to better respond to your individual needs)</li>
|
||||||
|
<li>
|
||||||
|
To improve our website (we continually strive to improve our website offerings based on the information and
|
||||||
|
feedback we receive from you)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
To improve customer service (your information helps us to more effectively respond to your customer service
|
||||||
|
requests and support needs)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>When does @Name use end user information from third parties?</h4>
|
||||||
|
<p>@Name will collect End User Data necessary to provide the @Name services to our customers.</p>
|
||||||
|
<p>
|
||||||
|
End users may voluntarily provide us with information they have made available on social media websites (specifically
|
||||||
|
No Agenda Social). If you provide us with any such information, we may collect publicly available information from
|
||||||
|
the social media websites you have indicated. You can control how much of your information social media websites make
|
||||||
|
public by visiting these websites and changing your privacy settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>When does @Name use customer information from third parties?</h4>
|
||||||
|
<p>We do not utilize third party information apart from the end-user data described above.</p>
|
||||||
|
|
||||||
|
<h4>Do we share the information we collect with third parties?</h4>
|
||||||
|
<p>
|
||||||
|
We may disclose personal and non-personal information about you to government or law enforcement officials or private
|
||||||
|
parties as we, in our sole discretion, believe necessary or appropriate in order to respond to claims, legal process
|
||||||
|
(including subpoenas), to protect our rights and interests or those of a third party, the safety of the public or any
|
||||||
|
person, to prevent or stop any illegal, unethical, or legally actionable activity, or to otherwise comply with
|
||||||
|
applicable court orders, laws, rules and regulations.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Where and when is information collected from customers and end users?</h4>
|
||||||
|
<p>
|
||||||
|
@Name will collect personal information that you submit to us. We may also receive personal information about you
|
||||||
|
from third parties as described above.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>How Do We Use Your E-mail Address?</h4>
|
||||||
|
<p>
|
||||||
|
We do not collect nor use an e-mail address. If you have provided it in the free text areas of the site, other
|
||||||
|
validated users may be able to view it, but @Name does not search for nor utilize e-mail addresses from those areas.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>How Long Do We Keep Your Information?</h4>
|
||||||
|
<p>
|
||||||
|
We keep your information only so long as we need it to provide @Name to you and fulfill the purposes described in
|
||||||
|
this policy. When we no longer need to use your information and there is no need for us to keep it to comply with our
|
||||||
|
legal or regulatory obligations, we’ll either remove it from our systems or depersonalize it so that we can’t
|
||||||
|
identify you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>How Do We Protect Your Information?</h4>
|
||||||
|
<p>
|
||||||
|
We implement a variety of security measures to maintain the safety of your personal information when you enter,
|
||||||
|
submit, or access your personal information. We mandate the use of a secure server. We cannot, however, ensure or
|
||||||
|
warrant the absolute security of any information you transmit to @Name or guarantee that your information on the
|
||||||
|
Service may not be accessed, disclosed, altered, or destroyed by a breach of any of our physical, technical, or
|
||||||
|
managerial safeguards.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Could my information be transferred to other countries?</h4>
|
||||||
|
<p>
|
||||||
|
@Name is hosted in the US. Information collected via our website may be viewed and hosted anywhere in the world,
|
||||||
|
including countries that may not have laws of general applicability regulating the use and transfer of such data. To
|
||||||
|
the fullest extent allowed by applicable law, by using any of the above, you voluntarily consent to the trans-border
|
||||||
|
transfer and hosting of such information.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Is the information collected through the @Name Service secure?</h4>
|
||||||
|
<p>
|
||||||
|
We take precautions to protect the security of your information. We have physical, electronic, and managerial
|
||||||
|
procedures to help safeguard, prevent unauthorized access, maintain data security, and correctly use your
|
||||||
|
information. However, neither people nor security systems are foolproof, including encryption systems. In addition,
|
||||||
|
people can commit intentional crimes, make mistakes, or fail to follow policies. Therefore, while we use reasonable
|
||||||
|
efforts to protect your personal information, we cannot guarantee its absolute security. If applicable law imposes
|
||||||
|
any non-disclaimable duty to protect your personal information, you agree that intentional misconduct will be the
|
||||||
|
standards used to measure our compliance with that duty.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Can I update or correct my information?</h4>
|
||||||
|
<p>
|
||||||
|
The rights you have to request updates or corrections to the information @Name collects depend on your relationship
|
||||||
|
with @Name.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Customers have the right to request the restriction of certain uses and disclosures of personally identifiable
|
||||||
|
information as follows. You can contact us in order to (1) update or correct your personally identifiable
|
||||||
|
information, or (3) delete the personally identifiable information maintained about you on our systems (subject to
|
||||||
|
the following paragraph), by cancelling your account. Such updates, corrections, changes and deletions will have no
|
||||||
|
effect on other information that we maintain in accordance with this Privacy Policy prior to such update, correction,
|
||||||
|
change, or deletion. You are responsible for maintaining the secrecy of your unique password and account information
|
||||||
|
at all times.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
@Name also provides ways for users to modify or remove the information we have collected from them from the
|
||||||
|
application; these actions will have the same effect as contacting us to modify or remove data.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You should be aware that it is not technologically possible to remove each and every record of the information you
|
||||||
|
have provided to us from our system. The need to back up our systems to protect information from inadvertent loss
|
||||||
|
means that a copy of your information may exist in a non-erasable form that will be difficult or impossible for us to
|
||||||
|
locate. Promptly after receiving your request, all personal information stored in databases we actively use, and
|
||||||
|
other readily searchable media will be updated, corrected, changed, or deleted, as appropriate, as soon as and to the
|
||||||
|
extent reasonably and technically practicable.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you are an end user and wish to update, delete, or receive any information we have about you, you may do so by
|
||||||
|
contacting the organization of which you are a customer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Governing Law</h4>
|
||||||
|
<p>
|
||||||
|
This Privacy Policy is governed by the laws of US without regard to its conflict of laws provision. You consent to
|
||||||
|
the exclusive jurisdiction of the courts in connection with any action or dispute arising between the parties under
|
||||||
|
or in connection with this Privacy Policy except for those individuals who may have rights to make claims under
|
||||||
|
Privacy Shield, or the Swiss-US framework.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The laws of US, excluding its conflicts of law rules, shall govern this Agreement and your use of the website. Your
|
||||||
|
use of the website may also be subject to other local, state, national, or international laws.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By using @Name or contacting us directly, you signify your acceptance of this Privacy Policy. If you do not agree to
|
||||||
|
this Privacy Policy, you should not engage with our website, or use our services. Continued use of the website,
|
||||||
|
direct engagement with us, or following the posting of changes to this Privacy Policy that do not significantly
|
||||||
|
affect the use or disclosure of your personal information will mean that you accept those changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Your Consent</h4>
|
||||||
|
<p>
|
||||||
|
We’ve updated our Privacy Policy to provide you with complete transparency into what is being set when you
|
||||||
|
visit our site and how it’s being used. By using our website, registering an account, or making a purchase, you
|
||||||
|
hereby consent to our Privacy Policy and agree to its terms.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Links to Other Websites</h4>
|
||||||
|
<p>
|
||||||
|
This Privacy Policy applies only to the Services. The Services may contain links to other websites not operated or
|
||||||
|
controlled by @Name. We are not responsible for the content, accuracy or opinions expressed in such websites, and
|
||||||
|
such websites are not investigated, monitored or checked for accuracy or completeness by us. Please remember that
|
||||||
|
when you use a link to go from the Services to another website, our Privacy Policy is no longer in effect. Your
|
||||||
|
browsing and interaction on any other website, including those that have a link on our platform, is subject to that
|
||||||
|
website’s own rules and policies. Such third parties may use their own cookies or other methods to collect
|
||||||
|
information about you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Cookies</h4>
|
||||||
|
<p>@Name does not use Cookies.</p>
|
||||||
|
|
||||||
|
<h4>Kids' Privacy</h4>
|
||||||
|
<p>
|
||||||
|
We do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from
|
||||||
|
anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with
|
||||||
|
Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age
|
||||||
|
of 13 without verification of parental consent, We take steps to remove that information from Our servers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Changes To Our Privacy Policy</h4>
|
||||||
|
<p>
|
||||||
|
We may change our Service and policies, and we may need to make changes to this Privacy Policy so that they
|
||||||
|
accurately reflect our Service and policies. Unless otherwise required by law, we will notify you (for example,
|
||||||
|
through our Service) before we make changes to this Privacy Policy and give you an opportunity to review them before
|
||||||
|
they go into effect. Then, if you continue to use the Service, you will be bound by the updated Privacy Policy. If
|
||||||
|
you do not want to agree to this or any updated Privacy Policy, you can delete your account.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Third-Party Services</h4>
|
||||||
|
<p>
|
||||||
|
We may display, include or make available third-party content (including data, information, applications and other
|
||||||
|
products services) or provide links to third-party websites or services (“Third-Party Services”).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You acknowledge and agree that @Name shall not be responsible for any Third-Party Services, including their accuracy,
|
||||||
|
completeness, timeliness, validity, copyright compliance, legality, decency, quality or any other aspect thereof.
|
||||||
|
@Name does not assume and shall not have any liability or responsibility to you or any other person or entity for any
|
||||||
|
Third-Party Services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Third-Party Services and links thereto are provided solely as a convenience to you and you access and use them
|
||||||
|
entirely at your own risk and subject to such third parties' terms and conditions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Tracking Technologies</h4>
|
||||||
|
<p>
|
||||||
|
@Name does not use any tracking technologies. When an authorization code is received from No Agenda Social, that
|
||||||
|
token is stored in the browser’s memory, and the Service uses tokens on each request for data. If the page is
|
||||||
|
refreshed or the browser window/tab is closed, this token disappears, and a new one must be generated before the
|
||||||
|
application can be used again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Information about General Data Protection Regulation (GDPR)</h4>
|
||||||
|
<p>
|
||||||
|
We may be collecting and using information from you if you are from the European Economic Area (EEA), and in this
|
||||||
|
section of our Privacy Policy we are going to explain exactly how and why is this data collected, and how we maintain
|
||||||
|
this data under protection from being replicated or used in the wrong way.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>What is GDPR?</h4>
|
||||||
|
<p>
|
||||||
|
GDPR is an EU-wide privacy and data protection law that regulates how EU residents’ data is protected by
|
||||||
|
companies and enhances the control the EU residents have, over their personal data.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The GDPR is relevant to any globally operating company and not just the EU-based businesses and EU residents. Our
|
||||||
|
customers’ data is important irrespective of where they are located, which is why we have implemented GDPR controls
|
||||||
|
as our baseline standard for all our operations worldwide.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>What is personal data?</h4>
|
||||||
|
<p>
|
||||||
|
Any data that relates to an identifiable or identified individual. GDPR covers a broad spectrum of information that
|
||||||
|
could be used on its own, or in combination with other pieces of information, to identify a person. Personal data
|
||||||
|
extends beyond a person’s name or email address. Some examples include financial information, political opinions,
|
||||||
|
genetic data, biometric data, IP addresses, physical address, sexual orientation, and ethnicity.
|
||||||
|
</p>
|
||||||
|
<p>The Data Protection Principles include requirements such as:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Personal data collected must be processed in a fair, legal, and transparent way and should only be used in a way
|
||||||
|
that a person would reasonably expect.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Personal data should only be collected to fulfil a specific purpose and it should only be used for that purpose.
|
||||||
|
Organizations must specify why they need the personal data when they collect it.
|
||||||
|
</li>
|
||||||
|
<li>Personal data should be held no longer than necessary to fulfil its purpose.</li>
|
||||||
|
<li>
|
||||||
|
People covered by the GDPR have the right to access their own personal data. They can also request a copy of their
|
||||||
|
data, and that their data be updated, deleted, restricted, or moved to another organization.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>Why is GDPR important?</h4>
|
||||||
|
<p>
|
||||||
|
GDPR adds some new requirements regarding how companies should protect individuals’ personal data that they
|
||||||
|
collect and process. It also raises the stakes for compliance by increasing enforcement and imposing greater fines
|
||||||
|
for breach. Beyond these facts, it’s simply the right thing to do. At @Name we strongly believe that your data
|
||||||
|
privacy is very important and we already have solid security and privacy practices in place that go beyond the
|
||||||
|
requirements of this regulation.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Individual Data Subject’s Rights - Data Access, Portability, and Deletion</h4>
|
||||||
|
<p>
|
||||||
|
We are committed to helping our customers meet the data subject rights requirements of GDPR. @Name processes or
|
||||||
|
stores all personal data in fully vetted, DPA compliant vendors. We do store all conversation and personal data for
|
||||||
|
up to 6 years unless your account is deleted. In which case, we dispose of all data in accordance with our Terms of
|
||||||
|
Service and Privacy Policy, but we will not hold it longer than 60 days.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We are aware that if you are working with EU customers, you need to be able to provide them with the ability to
|
||||||
|
access, update, retrieve and remove personal data. We got you! We've been set up as self service from the start and
|
||||||
|
have always given you access to your data. Our customer support team is here for you to answer any questions you
|
||||||
|
might have about working with the API.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>California Residents</h4>
|
||||||
|
<p>
|
||||||
|
The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal Information we collect and
|
||||||
|
how we use it, the categories of sources from whom we collect Personal Information, and the third parties with whom
|
||||||
|
we share it, which we have explained above.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We are also required to communicate information about rights California residents have under California law. You may
|
||||||
|
exercise the following rights:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of
|
||||||
|
Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are
|
||||||
|
collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific
|
||||||
|
pieces of Personal Information we have collected about you.
|
||||||
|
</li>
|
||||||
|
<li>Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.</li>
|
||||||
|
<li>
|
||||||
|
Right to Delete. You may submit a verifiable request to close your account and we will delete Personal Information
|
||||||
|
about you that we have collected.
|
||||||
|
</li>
|
||||||
|
<li>Request that a business that sells a consumer's personal data, not sell the consumer's personal data.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please
|
||||||
|
contact us.
|
||||||
|
</p>
|
||||||
|
<p>We do not sell the Personal Information of our users.</p>
|
||||||
|
<p>For more information about these rights, please contact us.</p>
|
||||||
|
|
||||||
|
<h4>California Online Privacy Protection Act (CalOPPA)</h4>
|
||||||
|
<p>
|
||||||
|
CalOPPA requires us to disclose categories of Personal Information we collect and how we use it, the categories of
|
||||||
|
sources from whom we collect Personal Information, and the third parties with whom we share it, which we have
|
||||||
|
explained above.
|
||||||
|
</p>
|
||||||
|
<p>CalOPPA users have the following rights:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of
|
||||||
|
Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are
|
||||||
|
collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific
|
||||||
|
pieces of Personal Information we have collected about you.
|
||||||
|
</li>
|
||||||
|
<li>Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.</li>
|
||||||
|
<li>
|
||||||
|
Right to Delete. You may submit a verifiable request to close your account and we will delete Personal Information
|
||||||
|
about you that we have collected.
|
||||||
|
</li>
|
||||||
|
<li>Right to request that a business that sells a consumer's personal data, not sell the consumer's personal data.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please
|
||||||
|
contact us.
|
||||||
|
</p>
|
||||||
|
<p>We do not sell the Personal Information of our users.</p>
|
||||||
|
<p>For more information about these rights, please contact us.</p>
|
||||||
|
|
||||||
|
<h4>Contact Us</h4>
|
||||||
|
<p>Don't hesitate to contact us if you have any questions.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Via this Link: <a href="/how-it-works">https://noagendacareers.com/how-it-works</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the application
|
||||||
|
/// </summary>
|
||||||
|
private string Name = "Jobs, Jobs, Jobs";
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
@page "/profile/view/{Id}"
|
|
||||||
@inject HttpClient http
|
|
||||||
@inject AppState state
|
|
||||||
|
|
||||||
@if (IsLoading)
|
|
||||||
{
|
|
||||||
<p>Loading profile...</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<PageTitle Title=@($"Employment Profile for {Citizen.DisplayName}") />
|
|
||||||
<ErrorList Errors=@ErrorMessages>
|
|
||||||
<h2><a href="@Citizen.ProfileUrl" target="_blank">@Citizen.DisplayName</a></h2>
|
|
||||||
<h4>@Profile.Continent!.Name, @Profile.Region</h4>
|
|
||||||
<p>@WorkTypes</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
@(new MarkupString(Profile.Biography.ToHtml()))
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@if (Profile.Skills.Length > 0)
|
|
||||||
{
|
|
||||||
<hr>
|
|
||||||
<h4>Skills</h4>
|
|
||||||
<ul>
|
|
||||||
@foreach (var skill in Profile.Skills)
|
|
||||||
{
|
|
||||||
var notes = skill.Notes == null ? "" : $" ({skill.Notes})";
|
|
||||||
<li>@skill.Description@notes</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Profile.Experience != null)
|
|
||||||
{
|
|
||||||
<hr>
|
|
||||||
<h4>Experience / Employment History</h4>
|
|
||||||
<div>
|
|
||||||
@(new MarkupString(Profile.Experience.ToHtml()))
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Id == state.User!.Id.ToString())
|
|
||||||
{
|
|
||||||
<hr>
|
|
||||||
<p><a href="/citizen/profile"><span class="oi oi-pencil"></span> Edit Your Profile</a></p>
|
|
||||||
}
|
|
||||||
</ErrorList>
|
|
||||||
}
|
|
||||||
57
src/JobsJobsJobs/Client/Pages/Profiles/Search.razor
Normal file
57
src/JobsJobsJobs/Client/Pages/Profiles/Search.razor
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@page "/profile/search"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
<PageTitle Title="Search Profiles" />
|
||||||
|
<h3>Search Profiles</h3>
|
||||||
|
|
||||||
|
<ErrorList Errors=@ErrorMessages>
|
||||||
|
@if (Searching)
|
||||||
|
{
|
||||||
|
<p>Searching profiles...</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!Searched)
|
||||||
|
{
|
||||||
|
<p>Enter one or more criteria to filter results, or just click “Search” to list all profiles.</p>
|
||||||
|
}
|
||||||
|
<Collapsible HeaderText="Search Criteria" Collapsed=@(Searched && SearchResults.Any())>
|
||||||
|
<ProfileSearchForm Criteria=@Criteria OnSearch=@DoSearch Continents=@Continents />
|
||||||
|
</Collapsible>
|
||||||
|
<br>
|
||||||
|
@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>
|
||||||
|
}
|
||||||
|
else if (Searched)
|
||||||
|
{
|
||||||
|
<p>No results found for the specified criteria</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ErrorList>
|
||||||
139
src/JobsJobsJobs/Client/Pages/Profiles/Search.razor.cs
Normal file
139
src/JobsJobsJobs/Client/Pages/Profiles/Search.razor.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.Profiles
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
/// The search criteria
|
||||||
|
/// </summary>
|
||||||
|
private ProfileSearch Criteria { get; set; } = new ProfileSearch();
|
||||||
|
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
Continents = await state.GetContinents(http);
|
||||||
|
|
||||||
|
// Determine if we have searched before
|
||||||
|
var query = QueryHelpers.ParseQuery(nav.ToAbsoluteUri(nav.Uri).Query);
|
||||||
|
|
||||||
|
if (query.TryGetValue("Searched", out var searched))
|
||||||
|
{
|
||||||
|
Searched = Convert.ToBoolean(searched);
|
||||||
|
void setPart(string part, Action<string> func)
|
||||||
|
{
|
||||||
|
if (query.TryGetValue(part, out var partValue)) func(partValue);
|
||||||
|
}
|
||||||
|
setPart(nameof(Criteria.ContinentId), x => Criteria.ContinentId = x);
|
||||||
|
setPart(nameof(Criteria.Skill), x => Criteria.Skill = x);
|
||||||
|
setPart(nameof(Criteria.BioExperience), x => Criteria.BioExperience = x);
|
||||||
|
setPart(nameof(Criteria.RemoteWork), x => Criteria.RemoteWork = x);
|
||||||
|
|
||||||
|
await RetrieveProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do a search
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This navigates with the parameters in the URL; this should trigger a search</remarks>
|
||||||
|
private async Task DoSearch()
|
||||||
|
{
|
||||||
|
var query = SearchQuery();
|
||||||
|
query.Add("Searched", "True");
|
||||||
|
nav.NavigateTo(QueryHelpers.AddQueryString("/profile/search", query));
|
||||||
|
await RetrieveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retreive profiles matching the current search criteria
|
||||||
|
/// </summary>
|
||||||
|
private async Task RetrieveProfiles()
|
||||||
|
{
|
||||||
|
Searching = true;
|
||||||
|
|
||||||
|
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http,
|
||||||
|
QueryHelpers.AddQueryString("profile/search", SearchQuery()));
|
||||||
|
|
||||||
|
if (searchResult.IsOk)
|
||||||
|
{
|
||||||
|
SearchResults = searchResult.Ok;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorMessages.Add(searchResult.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Searched = true;
|
||||||
|
Searching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a CSS class if the user is actively seeking work
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profile">The result in question</param>
|
||||||
|
/// <returns>A string with the appropriate CSS class, if actively seeking work</returns>
|
||||||
|
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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a search query string from the currently-entered criteria
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The query string for the currently-entered criteria</returns>
|
||||||
|
private IDictionary<string, string?> SearchQuery()
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string?>();
|
||||||
|
if (Criteria.IsEmptySearch) return dict;
|
||||||
|
|
||||||
|
void part(string name, Func<ProfileSearch, string?> func)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(func(Criteria))) dict.Add(name, func(Criteria));
|
||||||
|
}
|
||||||
|
|
||||||
|
part(nameof(Criteria.ContinentId), it => it.ContinentId);
|
||||||
|
part(nameof(Criteria.Skill), it => it.Skill);
|
||||||
|
part(nameof(Criteria.BioExperience), it => it.BioExperience);
|
||||||
|
part(nameof(Criteria.RemoteWork), it => it.RemoteWork);
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor
Normal file
62
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
@page "/profile/seeking"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
<PageTitle Title="People Seeking Work" />
|
||||||
|
<h3>People Seeking Work</h3>
|
||||||
|
|
||||||
|
<ErrorList Errors=@ErrorMessages>
|
||||||
|
@if (Searching)
|
||||||
|
{
|
||||||
|
<p>Searching profiles...</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!Searched)
|
||||||
|
{
|
||||||
|
<p>Enter one or more criteria to filter results, or just click “Search” to list all profiles.</p>
|
||||||
|
}
|
||||||
|
<Collapsible HeaderText="Search Criteria" Collapsed=@(Searched && SearchResults.Any())>
|
||||||
|
<PublicSearchForm Criteria=@Criteria OnSearch=@DoSearch Continents=@Continents />
|
||||||
|
</Collapsible>
|
||||||
|
<br>
|
||||||
|
@if (SearchResults.Any())
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
These profiles match your search criteria. To learn more about these people, join the merry band of human
|
||||||
|
resources in the <a href="https://noagendashow.net" target="_blank">No Agenda</a> tribe!
|
||||||
|
</p>
|
||||||
|
<table class="table table-sm table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Continent</th>
|
||||||
|
<th scope="col" class="text-center">Region</th>
|
||||||
|
<th scope="col" class="text-center">Remote?</th>
|
||||||
|
<th scope="col" class="text-center">Skills</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var profile in SearchResults)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@profile.Continent</td>
|
||||||
|
<td>@profile.Region</td>
|
||||||
|
<td class="text-center">@YesOrNo(profile.RemoteWork)</td>
|
||||||
|
<td>
|
||||||
|
@foreach (var skill in profile.Skills)
|
||||||
|
{
|
||||||
|
@skill.Replace(" ()", "")<br>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
else if (Searched)
|
||||||
|
{
|
||||||
|
<p>No results found for the specified criteria</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ErrorList>
|
||||||
134
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs
Normal file
134
src/JobsJobsJobs/Client/Pages/Profiles/Seeking.razor.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.Profiles
|
||||||
|
{
|
||||||
|
public partial class Seeking : 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>
|
||||||
|
/// The search criteria
|
||||||
|
/// </summary>
|
||||||
|
private PublicSearch Criteria { get; set; } = new PublicSearch();
|
||||||
|
|
||||||
|
/// <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<PublicSearchResult> SearchResults { get; set; } = Enumerable.Empty<PublicSearchResult>();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
Continents = await state.GetContinents(http);
|
||||||
|
|
||||||
|
// Determine if we have searched before
|
||||||
|
var query = QueryHelpers.ParseQuery(nav.ToAbsoluteUri(nav.Uri).Query);
|
||||||
|
|
||||||
|
if (query.TryGetValue("Searched", out var searched))
|
||||||
|
{
|
||||||
|
Searched = Convert.ToBoolean(searched);
|
||||||
|
void setPart(string part, Action<string> func)
|
||||||
|
{
|
||||||
|
if (query.TryGetValue(part, out var partValue)) func(partValue);
|
||||||
|
}
|
||||||
|
setPart(nameof(Criteria.ContinentId), x => Criteria.ContinentId = x);
|
||||||
|
setPart(nameof(Criteria.Region), x => Criteria.Region = x);
|
||||||
|
setPart(nameof(Criteria.Skill), x => Criteria.Skill = x);
|
||||||
|
setPart(nameof(Criteria.RemoteWork), x => Criteria.RemoteWork = x);
|
||||||
|
|
||||||
|
await RetrieveProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do a search
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This navigates with the parameters in the URL; this should trigger a search</remarks>
|
||||||
|
private async Task DoSearch()
|
||||||
|
{
|
||||||
|
var query = SearchQuery();
|
||||||
|
query.Add("Searched", "True");
|
||||||
|
nav.NavigateTo(QueryHelpers.AddQueryString("/profile/seeking", query));
|
||||||
|
await RetrieveProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retreive profiles matching the current search criteria
|
||||||
|
/// </summary>
|
||||||
|
private async Task RetrieveProfiles()
|
||||||
|
{
|
||||||
|
Searching = true;
|
||||||
|
|
||||||
|
var searchResult = await ServerApi.RetrieveMany<PublicSearchResult>(http,
|
||||||
|
QueryHelpers.AddQueryString("profile/public-search", SearchQuery()));
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a search query string from the currently-entered criteria
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The query string for the currently-entered criteria</returns>
|
||||||
|
private IDictionary<string, string?> SearchQuery()
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string?>();
|
||||||
|
if (Criteria.IsEmptySearch) return dict;
|
||||||
|
|
||||||
|
void part(string name, Func<PublicSearch, string?> func)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(func(Criteria))) dict.Add(name, func(Criteria));
|
||||||
|
}
|
||||||
|
|
||||||
|
part(nameof(Criteria.ContinentId), it => it.ContinentId);
|
||||||
|
part(nameof(Criteria.Region), it => it.Region);
|
||||||
|
part(nameof(Criteria.Skill), it => it.Skill);
|
||||||
|
part(nameof(Criteria.RemoteWork), it => it.RemoteWork);
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/JobsJobsJobs/Client/Pages/Profiles/View.razor
Normal file
45
src/JobsJobsJobs/Client/Pages/Profiles/View.razor
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@page "/profile/view/{Id}"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
<Loading OnLoad=@RetrieveProfile>
|
||||||
|
<PageTitle Title=@($"Employment Profile for {Citizen.CitizenName}") />
|
||||||
|
<h2><a href="@Citizen.ProfileUrl" target="_blank">@Citizen.CitizenName</a></h2>
|
||||||
|
<h4>@Profile.Continent!.Name, @Profile.Region</h4>
|
||||||
|
<p>@WorkTypes</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
@(new MarkupString(Profile.Biography.ToHtml()))
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@if (Profile.Skills.Count > 0)
|
||||||
|
{
|
||||||
|
<hr>
|
||||||
|
<h4>Skills</h4>
|
||||||
|
<ul>
|
||||||
|
@foreach (var skill in Profile.Skills)
|
||||||
|
{
|
||||||
|
var notes = skill.Notes == null ? "" : $" ({skill.Notes})";
|
||||||
|
<li>@skill.Description@notes</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Profile.Experience != null)
|
||||||
|
{
|
||||||
|
<hr>
|
||||||
|
<h4>Experience / Employment History</h4>
|
||||||
|
<div>
|
||||||
|
@(new MarkupString(Profile.Experience.ToHtml()))
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Id == state.User!.Id.ToString())
|
||||||
|
{
|
||||||
|
<hr>
|
||||||
|
<p><a href="/citizen/profile"><span class="oi oi-pencil"></span> Edit Your Profile</a></p>
|
||||||
|
}
|
||||||
|
</Loading>
|
||||||
@@ -1,27 +1,21 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using JobsJobsJobs.Shared;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Domain = JobsJobsJobs.Shared;
|
|
||||||
|
|
||||||
namespace JobsJobsJobs.Client.Pages.Profile
|
namespace JobsJobsJobs.Client.Pages.Profiles
|
||||||
{
|
{
|
||||||
public partial class View : ComponentBase
|
public partial class View : ComponentBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether data for this component is loading
|
|
||||||
/// </summary>
|
|
||||||
private bool IsLoading { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The citizen whose profile is being displayed
|
/// The citizen whose profile is being displayed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Domain.Citizen Citizen { get; set; } = default!;
|
private Citizen Citizen { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The profile to display
|
/// The profile to display
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Domain.Profile Profile { get; set; } = default!;
|
private Profile Profile { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The work types for the top of the page
|
/// The work types for the top of the page
|
||||||
@@ -48,22 +42,21 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Error messages from data retrieval
|
|
||||||
/// </summary>
|
|
||||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ID of the citizen whose profile should be displayed
|
/// The ID of the citizen whose profile should be displayed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Id { get; set; } = default!;
|
public string Id { get; set; } = default!;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
/// <summary>
|
||||||
|
/// Retrieve the requested profile
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errors">A collection to report errors that may occur</param>
|
||||||
|
public async Task RetrieveProfile(ICollection<string> errors)
|
||||||
{
|
{
|
||||||
ServerApi.SetJwt(http, state);
|
ServerApi.SetJwt(http, state);
|
||||||
var citizenTask = ServerApi.RetrieveOne<Domain.Citizen>(http, $"citizen/get/{Id}");
|
var citizenTask = ServerApi.RetrieveOne<Citizen>(http, $"citizen/get/{Id}");
|
||||||
var profileTask = ServerApi.RetrieveOne<Domain.Profile>(http, $"profile/get/{Id}");
|
var profileTask = ServerApi.RetrieveOne<Profile>(http, $"profile/get/{Id}");
|
||||||
|
|
||||||
await Task.WhenAll(citizenTask, profileTask);
|
await Task.WhenAll(citizenTask, profileTask);
|
||||||
|
|
||||||
@@ -73,11 +66,11 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
|||||||
}
|
}
|
||||||
else if (citizenTask.Result.IsOk)
|
else if (citizenTask.Result.IsOk)
|
||||||
{
|
{
|
||||||
ErrorMessages.Add("Citizen not found");
|
errors.Add("Citizen not found");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ErrorMessages.Add(citizenTask.Result.Error);
|
errors.Add(citizenTask.Result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileTask.Result.IsOk && profileTask.Result.Ok != null)
|
if (profileTask.Result.IsOk && profileTask.Result.Ok != null)
|
||||||
@@ -86,14 +79,12 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
|||||||
}
|
}
|
||||||
else if (profileTask.Result.IsOk)
|
else if (profileTask.Result.IsOk)
|
||||||
{
|
{
|
||||||
ErrorMessages.Add("Profile not found");
|
errors.Add("Profile not found");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ErrorMessages.Add(profileTask.Result.Error);
|
errors.Add(profileTask.Result.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLoading = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
39
src/JobsJobsJobs/Client/Pages/SoLong/Options.razor
Normal file
39
src/JobsJobsJobs/Client/Pages/SoLong/Options.razor
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@page "/so-long/options"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@inject IToastService toast
|
||||||
|
|
||||||
|
<PageTitle Title="Account Deletion Options" />
|
||||||
|
|
||||||
|
<h3>Account Deletion Options</h3>
|
||||||
|
|
||||||
|
<h4>Option 1 – Delete Your Profile</h4>
|
||||||
|
<p>
|
||||||
|
Utilizing this option will remove your current employment profile and skills. This will preserve any success stories
|
||||||
|
you may have written, and preserves this application’s knowledge of you. This is what you want to use if you
|
||||||
|
want to clear out your profile and start again (and remove the current one from others’ view).
|
||||||
|
</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<button class="btn btn-danger" @onclick=@DeleteProfile>Delete Your Profile</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>Option 2 – Delete Your Account</h4>
|
||||||
|
<p>
|
||||||
|
This option will make it like you never visited this site. It will delete your profile, skills, success stories, and
|
||||||
|
account. This is what you want to use if you want to disappear from this application. Clicking the button below
|
||||||
|
<strong>will not</strong> affect your No Agenda Social account in any way; its effects are limited to Jobs, Jobs,
|
||||||
|
Jobs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
(This will not revoke this application’s permissions on No Agenda Social; you will have to remove this
|
||||||
|
yourself. The confirmation message has a link where you can do this; once the page loads, find the
|
||||||
|
<strong>Jobs, Jobs, Jobs</strong> entry, and click the <strong>× Revoke</strong> link for that entry.)
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<button class="btn btn-danger" @onclick=@DeleteAccount>Delete Your Entire Account</button>
|
||||||
|
</p>
|
||||||
54
src/JobsJobsJobs/Client/Pages/SoLong/Options.razor.cs
Normal file
54
src/JobsJobsJobs/Client/Pages/SoLong/Options.razor.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.SoLong
|
||||||
|
{
|
||||||
|
public partial class Options : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extract an error phrase from a response similar to <code>404 - Not Found</code>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="response">The HTTP response</param>
|
||||||
|
/// <returns>The formatted error code</returns>
|
||||||
|
private static string ErrorPhrase(HttpResponseMessage response) =>
|
||||||
|
$"{response.StatusCode}{(string.IsNullOrEmpty(response.ReasonPhrase) ? "" : $" - {response.ReasonPhrase }")}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete the profile only; redirect to home page on success
|
||||||
|
/// </summary>
|
||||||
|
private async Task DeleteProfile()
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, state);
|
||||||
|
var result = await http.DeleteAsync("/api/profile/");
|
||||||
|
if (result.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
toast.ShowSuccess("Profile Deleted Successfully");
|
||||||
|
nav.NavigateTo("/citizen/dashboard");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toast.ShowError(ErrorPhrase(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete everything pertaining to the user's account
|
||||||
|
/// </summary>
|
||||||
|
private async Task DeleteAccount()
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, state);
|
||||||
|
var result = await http.DeleteAsync("/api/citizen/");
|
||||||
|
if (result.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
state.Jwt = "";
|
||||||
|
state.User = null;
|
||||||
|
nav.NavigateTo("/so-long/success");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toast.ShowError(ErrorPhrase(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/JobsJobsJobs/Client/Pages/SoLong/Success.razor
Normal file
15
src/JobsJobsJobs/Client/Pages/SoLong/Success.razor
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@page "/so-long/success"
|
||||||
|
|
||||||
|
<PageTitle Title="Account Deletion Success" />
|
||||||
|
|
||||||
|
<h3>Account Deletion Success</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your account has been successfully deleted. To revoke the permissions you have previously granted to this
|
||||||
|
application, find it in <a href="https://noagendasocial.com/oauth/authorized_applications">this list</a> and click
|
||||||
|
<strong>× Revoke</strong>. Otherwise, clicking “Log On” will create a new, empty account without
|
||||||
|
prompting you further.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Thank you for participating, and thank you for your courage. #GitmoNation
|
||||||
|
</p>
|
||||||
49
src/JobsJobsJobs/Client/Pages/SuccessStory/EditStory.razor
Normal file
49
src/JobsJobsJobs/Client/Pages/SuccessStory/EditStory.razor
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@page "/success-story/add"
|
||||||
|
@page "/success-story/edit/{Id}"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
@inject NavigationManager nav
|
||||||
|
@inject IToastService toast
|
||||||
|
|
||||||
|
<PageTitle Title=@Title />
|
||||||
|
<h3>@Title</h3>
|
||||||
|
|
||||||
|
<Loading OnLoad=@RetrieveStory>
|
||||||
|
@if (IsNew)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came about; tell us
|
||||||
|
about it below! <em>(These will be visible to other users, but not to the general public.)</em>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<EditForm Model=@Form OnValidSubmit=@SaveStory>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-check">
|
||||||
|
<InputCheckbox id="fromHere" class="form-check-input" @bind-Value=@Form.FromHere />
|
||||||
|
<label for="fromHere" class="form-check-label">I found my employment here</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="story" class="jjj-label">The Success Story</label>
|
||||||
|
<MarkdownEditor Id="story" @bind-Text=@Form.Story />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<br>
|
||||||
|
<button type="submit" class="btn btn-outline-primary">Save</button>
|
||||||
|
@if (IsNew)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<em>(Saving this will set “Seeking Employment” to “No” on your profile.)</em>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
</Loading>
|
||||||
124
src/JobsJobsJobs/Client/Pages/SuccessStory/EditStory.razor.cs
Normal file
124
src/JobsJobsJobs/Client/Pages/SuccessStory/EditStory.razor.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.SuccessStory
|
||||||
|
{
|
||||||
|
public partial class EditStory : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the success story being edited
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The page title / header
|
||||||
|
/// </summary>
|
||||||
|
public string Title => IsNew ? "Tell Your Success Story" : "Edit Success Story";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The form with information for the success story
|
||||||
|
/// </summary>
|
||||||
|
private StoryForm Form { get; set; } = new StoryForm();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convenience property for showing new
|
||||||
|
/// </summary>
|
||||||
|
private bool IsNew => Form.Id == "new";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the story
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errors">A collection to use in reporting errors that may occur</param>
|
||||||
|
public async Task RetrieveStory(ICollection<string> errors)
|
||||||
|
{
|
||||||
|
if (Id != null)
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, state);
|
||||||
|
var story = await ServerApi.RetrieveOne<Success>(http, $"success/{Id}");
|
||||||
|
if (story.IsOk && story.Ok != null)
|
||||||
|
{
|
||||||
|
if (story.Ok.CitizenId == state.User!.Id)
|
||||||
|
{
|
||||||
|
Form = new StoryForm
|
||||||
|
{
|
||||||
|
Id = story.Ok.Id.ToString(),
|
||||||
|
FromHere = story.Ok.FromHere,
|
||||||
|
Story = story.Ok.Story?.Text ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add("Stop messing with the URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (story.IsOk)
|
||||||
|
{
|
||||||
|
errors.Add($"The success story {Id} does not exist");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add(story.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save the success story
|
||||||
|
/// </summary>
|
||||||
|
private async Task SaveStory()
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, state);
|
||||||
|
var res = await http.PostAsJsonAsync("/api/success/save", Form);
|
||||||
|
|
||||||
|
if (res.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if (IsNew)
|
||||||
|
{
|
||||||
|
res = await http.PatchAsync("/api/profile/employment-found", new StringContent(""));
|
||||||
|
if (res.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
SaveSuccessful();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SaveFailed(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SaveSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SaveFailed(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle success notifications if saving succeeded
|
||||||
|
/// </summary>
|
||||||
|
private void SaveSuccessful()
|
||||||
|
{
|
||||||
|
toast.ShowSuccess("Story Saved Successfully");
|
||||||
|
nav.NavigateTo("/success-story/list");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle failure notifications is saving was not successful
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="res">The HTTP response</param>
|
||||||
|
private async Task SaveFailed(HttpResponseMessage res)
|
||||||
|
{
|
||||||
|
var error = await res.Content.ReadAsStringAsync();
|
||||||
|
if (!string.IsNullOrEmpty(error)) error = $"- {error}";
|
||||||
|
toast.ShowError($"{(int)res.StatusCode} {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/JobsJobsJobs/Client/Pages/SuccessStory/ListStories.razor
Normal file
59
src/JobsJobsJobs/Client/Pages/SuccessStory/ListStories.razor
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
@page "/success-story/list"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
<PageTitle Title="Success Stories" />
|
||||||
|
<h3>Success Stories</h3>
|
||||||
|
|
||||||
|
<Loading OnLoad=@LoadStories>
|
||||||
|
@if (Stories.Any())
|
||||||
|
{
|
||||||
|
<table class="table table-sm table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Story</th>
|
||||||
|
<th scope="col">From</th>
|
||||||
|
<th scope="col">Found Here?</th>
|
||||||
|
<th scope="col">Recorded On</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var story in Stories)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
@if (story.HasStory)
|
||||||
|
{
|
||||||
|
<a href="/success-story/view/@story.Id">View</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<em>None</em>
|
||||||
|
}
|
||||||
|
@if (story.CitizenId == state.User!.Id)
|
||||||
|
{
|
||||||
|
<text> ~ </text><a href="/success-story/edit/@story.Id">Edit</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>@story.CitizenName</td>
|
||||||
|
<td>
|
||||||
|
@if (story.FromHere)
|
||||||
|
{
|
||||||
|
<strong>Yes</strong>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<text>No</text>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td><FullDate TheDate=@story.RecordedOn /></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p>There are no success stories recorded <em>(yet)</em></p>
|
||||||
|
}
|
||||||
|
</Loading>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.SuccessStory
|
||||||
|
{
|
||||||
|
public partial class ListStories : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The story entries
|
||||||
|
/// </summary>
|
||||||
|
private IEnumerable<StoryEntry> Stories { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load all success stories
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errors">The collection into which errors can be reported</param>
|
||||||
|
public async Task LoadStories(ICollection<string> errors)
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, state);
|
||||||
|
var stories = await ServerApi.RetrieveMany<StoryEntry>(http, "success/list");
|
||||||
|
|
||||||
|
if (stories.IsOk)
|
||||||
|
{
|
||||||
|
Stories = stories.Ok;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add(stories.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/JobsJobsJobs/Client/Pages/SuccessStory/ViewStory.razor
Normal file
19
src/JobsJobsJobs/Client/Pages/SuccessStory/ViewStory.razor
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@page "/success-story/view/{Id}"
|
||||||
|
@inject HttpClient http
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
<PageTitle Title="Success Story" />
|
||||||
|
|
||||||
|
<Loading OnLoad=@RetrieveStory>
|
||||||
|
<h3>@Citizen.CitizenName’s Success Story</h3>
|
||||||
|
<h4><FullDateTime TheDate=@Story.RecordedOn /></h4>
|
||||||
|
@if (Story.FromHere)
|
||||||
|
{
|
||||||
|
<p><em><strong>Found via Jobs, Jobs, Jobs</strong></em></p>
|
||||||
|
}
|
||||||
|
<hr>
|
||||||
|
@if (Story.Story != null)
|
||||||
|
{
|
||||||
|
<div>@(new MarkupString(Story.Story.ToHtml()))</div>
|
||||||
|
}
|
||||||
|
</Loading>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.SuccessStory
|
||||||
|
{
|
||||||
|
public partial class ViewStory : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the success story to display
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string Id { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The success story to be displayed
|
||||||
|
/// </summary>
|
||||||
|
private Success Story { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The citizen who authorized this success story
|
||||||
|
/// </summary>
|
||||||
|
private Citizen Citizen { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve the success story
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errors">The error collection via which errors will be reported</param>
|
||||||
|
public async Task RetrieveStory(ICollection<string> errors)
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(http, state);
|
||||||
|
var story = await ServerApi.RetrieveOne<Success>(http, $"success/{Id}");
|
||||||
|
|
||||||
|
if (story.IsOk)
|
||||||
|
{
|
||||||
|
if (story.Ok == null)
|
||||||
|
{
|
||||||
|
errors.Add($"Success story {Id} not found");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Story = story.Ok;
|
||||||
|
var citizen = await ServerApi.RetrieveOne<Citizen>(http, $"citizen/get/{Story.CitizenId}");
|
||||||
|
if (citizen.IsOk)
|
||||||
|
{
|
||||||
|
if (citizen.Ok == null)
|
||||||
|
{
|
||||||
|
errors.Add($"Citizen ID {Story.CitizenId} not found");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Citizen = citizen.Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add(citizen.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add(story.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/JobsJobsJobs/Client/Pages/TermsOfService.razor
Normal file
38
src/JobsJobsJobs/Client/Pages/TermsOfService.razor
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@page "/terms-of-service"
|
||||||
|
|
||||||
|
<PageTitle Title="Terms of Service" />
|
||||||
|
|
||||||
|
<h3>Terms of Service</h3>
|
||||||
|
<p><em>(as of February 6<sup>th</sup>, 2021)</em></p>
|
||||||
|
|
||||||
|
<h4>Acceptance of Terms</h4>
|
||||||
|
<p>
|
||||||
|
By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you are responsible
|
||||||
|
to ensure that your use of this site complies with all applicable laws. Your continued use of this site implies your
|
||||||
|
acceptance of these terms.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Description of Service and Registration</h4>
|
||||||
|
<p>
|
||||||
|
Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access to
|
||||||
|
the details of these profiles to other users of <a href="https://noagendasocial.com">No Agenda Social</a>.
|
||||||
|
Registration is accomplished by allowing Jobs, Jobs, Jobs to read one’s No Agenda Social profile. See our
|
||||||
|
<a href="/privacy-policy">privacy policy</a> for details on the personal (user) information we maintain.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Liability</h4>
|
||||||
|
<p>
|
||||||
|
This service is provided “as is”, and no warranty (express or implied) exists. The service and its
|
||||||
|
developers may not be held liable for any damages that may arise through the use of this service.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Updates to Terms</h4>
|
||||||
|
<p>
|
||||||
|
These terms and conditions may be updated at any time. When these terms are updated, users will be notified via a
|
||||||
|
notice on the dashboard page. Additionally, the date at the top of this page will be updated, and any substantive
|
||||||
|
updates will also be accompanied by a summary of those changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>You may also wish to review our <a href="/privacy-policy">privacy policy</a> to learn how we handle your data.</p>
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using JobsJobsJobs.Shared.Api;
|
using JobsJobsJobs.Shared.Api;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -29,7 +28,6 @@ namespace JobsJobsJobs.Client
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
static ServerApi()
|
static ServerApi()
|
||||||
{
|
{
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
|||||||
25
src/JobsJobsJobs/Client/Shared/Collapsible.razor
Normal file
25
src/JobsJobsJobs/Client/Shared/Collapsible.razor
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<a href="#" class="@(Collapsed ? "jjj-c-collapsed" : "jjj-c-open")"
|
||||||
|
@onclick=@Toggle @onclick:preventDefault>
|
||||||
|
@HeaderText
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@if (!Collapsed)
|
||||||
|
{
|
||||||
|
<div class="card-body">@ChildContent</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment ChildContent { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool Collapsed { get; set; } = false;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string HeaderText { get; set; } = "Toggle";
|
||||||
|
|
||||||
|
private void Toggle() => Collapsed = !Collapsed;
|
||||||
|
}
|
||||||
16
src/JobsJobsJobs/Client/Shared/Collapsible.razor.css
Normal file
16
src/JobsJobsJobs/Client/Shared/Collapsible.razor.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
a.jjj-c-collapsed,
|
||||||
|
a.jjj-c-open {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
a.jjj-c-collapsed:hover,
|
||||||
|
a.jjj-c-open:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.jjj-c-collapsed::before {
|
||||||
|
content: '\2b9e \00a0';
|
||||||
|
}
|
||||||
|
.jjj-c-open::before {
|
||||||
|
content: '\2b9f \00a0';
|
||||||
|
}
|
||||||
4
src/JobsJobsJobs/Client/Shared/FullDate.razor
Normal file
4
src/JobsJobsJobs/Client/Shared/FullDate.razor
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@inject IJSRuntime js
|
||||||
|
@inject AppState state
|
||||||
|
|
||||||
|
@Translated
|
||||||
30
src/JobsJobsJobs/Client/Shared/FullDate.razor.cs
Normal file
30
src/JobsJobsJobs/Client/Shared/FullDate.razor.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Shared
|
||||||
|
{
|
||||||
|
public partial class FullDate : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The pattern with which dates will be formatted
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ZonedDateTimePattern Pattern =
|
||||||
|
ZonedDateTimePattern.CreateWithCurrentCulture("ld<MMMM d, yyyy>", DateTimeZoneProviders.Tzdb);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date to be formatted
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Instant TheDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The formatted date
|
||||||
|
/// </summary>
|
||||||
|
private string Translated { get; set; } = "";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() =>
|
||||||
|
Translated = Pattern.Format(TheDate.InZone(await state.GetTimeZone(js)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,4 @@
|
|||||||
@using NodaTime
|
@inject IJSRuntime js
|
||||||
@using NodaTime.Text
|
@inject AppState state
|
||||||
@using System.Globalization
|
|
||||||
|
|
||||||
@Translated
|
@Translated
|
||||||
|
|
||||||
@code {
|
|
||||||
/// <summary>
|
|
||||||
/// The pattern with which dates will be formatted
|
|
||||||
/// </summary>
|
|
||||||
private static InstantPattern pattern = InstantPattern.Create("ld<D> ' at ' lt<t>", 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);
|
|
||||||
}
|
|
||||||
|
|||||||
30
src/JobsJobsJobs/Client/Shared/FullDateTime.razor.cs
Normal file
30
src/JobsJobsJobs/Client/Shared/FullDateTime.razor.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Shared
|
||||||
|
{
|
||||||
|
public partial class FullDateTime : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The pattern with which dates will be formatted
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ZonedDateTimePattern Pattern =
|
||||||
|
ZonedDateTimePattern.CreateWithCurrentCulture("ld<D> ' at ' lt<t>", DateTimeZoneProviders.Tzdb);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date to be formatted
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Instant TheDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The formatted date
|
||||||
|
/// </summary>
|
||||||
|
private string Translated { get; set; } = "";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() =>
|
||||||
|
Translated = Pattern.Format(TheDate.InZone(await state.GetTimeZone(js)));
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/JobsJobsJobs/Client/Shared/Loading.razor
Normal file
18
src/JobsJobsJobs/Client/Shared/Loading.razor
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<p>@Message</p>
|
||||||
|
}
|
||||||
|
else if (ErrorMessages.Count > 0)
|
||||||
|
{
|
||||||
|
<p>The following error@(ErrorMessages.Count == 1 ? "" : "s") occurred:</p>
|
||||||
|
<ul>
|
||||||
|
@foreach (var msg in ErrorMessages)
|
||||||
|
{
|
||||||
|
<li><pre>@msg</pre></li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@ChildContent
|
||||||
|
}
|
||||||
56
src/JobsJobsJobs/Client/Shared/Loading.razor.cs
Normal file
56
src/JobsJobsJobs/Client/Shared/Loading.razor.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Shared
|
||||||
|
{
|
||||||
|
public partial class Loading : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate to call to load the data for this page
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<ICollection<string>> OnLoad { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message to display when the page is loading (optional)
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public MarkupString Message { get; set; } = new MarkupString("Loading…");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The content to be displayed once the data has been loaded
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment ChildContent { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error messages that may arise from the data loading delegate
|
||||||
|
/// </summary>
|
||||||
|
private ICollection<string> ErrorMessages { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we are currently loading data
|
||||||
|
/// </summary>
|
||||||
|
private bool IsLoading { get; set; } = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (OnLoad.HasDelegate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await OnLoad.InvokeAsync(ErrorMessages);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@using System.Reflection
|
|
||||||
@inject IJSRuntime js
|
|
||||||
@using Blazored.Toast.Configuration
|
@using Blazored.Toast.Configuration
|
||||||
|
@inject IJSRuntime js
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="top-row px-4">
|
<div class="top-row px-4">
|
||||||
<em>(...and Jobs - <a class="audio" @onclick="PlayJobs">Let's Vote for Jobs!</a>)</em>
|
<em>(…and Jobs - <a class="audio" @onclick="PlayJobs">Let's Vote for Jobs!</a>)</em>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content px-4">
|
<div class="content px-4">
|
||||||
@@ -20,22 +19,25 @@
|
|||||||
<source src="/audio/pelosi-jobs.mp3">
|
<source src="/audio/pelosi-jobs.mp3">
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<div class="app-version">Jobs, Jobs, Jobs @Version</div>
|
<div class="app-version">
|
||||||
|
Jobs, Jobs, Jobs @AppState.Version.Value • <a href="/privacy-policy">Privacy Policy</a>
|
||||||
|
• <a href="/terms-of-service">Terms of Service</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (InitToasts)
|
||||||
|
{
|
||||||
<BlazoredToasts Position="ToastPosition.BottomRight"
|
<BlazoredToasts Position="ToastPosition.BottomRight"
|
||||||
ShowProgressBar="true" />
|
ShowProgressBar="true" />
|
||||||
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
bool InitToasts = false;
|
||||||
async void PlayJobs() => await js.InvokeVoidAsync("Audio.play", "pelosijobs");
|
async void PlayJobs() => await js.InvokeVoidAsync("Audio.play", "pelosijobs");
|
||||||
|
|
||||||
private string Version { get; set; } = "";
|
protected override void OnAfterRender(bool firstRender)
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
{
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
base.OnAfterRender(firstRender);
|
||||||
Version = $"v{version.Major}.{version.Minor}";
|
if (!InitToasts) InitToasts = true;
|
||||||
if (version.Revision > 0) Version += $".{version.Revision}";
|
|
||||||
base.OnInitialized();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,3 +77,10 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
}
|
}
|
||||||
|
.app-version a:link,
|
||||||
|
.app-version a:visited {
|
||||||
|
color: rgba(0, 0, 0, .25);
|
||||||
|
}
|
||||||
|
.app-version a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,20 +4,25 @@
|
|||||||
|
|
||||||
<div class="top-row pl-4 navbar navbar-dark">
|
<div class="top-row pl-4 navbar navbar-dark">
|
||||||
<a class="navbar-brand" href="">Jobs, Jobs, Jobs</a>
|
<a class="navbar-brand" href="">Jobs, Jobs, Jobs</a>
|
||||||
<button class="navbar-toggler" @onclick="ToggleNavMenu">
|
<button class="navbar-toggler" @onclick=@ToggleNavMenu>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
<div class="@NavMenuCssClass" @onclick=@ToggleNavMenu>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
|
@if (state.User == null)
|
||||||
|
{
|
||||||
<li class="nav-item px-3">
|
<li class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
<NavLink class="nav-link" href="" Match=@NavLinkMatch.All>
|
||||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
@if (state.User == null)
|
<li class="nav-item px-3">
|
||||||
{
|
<NavLink class="nav-link" href="/profile/seeking">
|
||||||
|
<span class="oi oi-spreadsheet" aria-hidden="true"></span> Job Seekers
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
<li class="nav-item px-3">
|
<li class="nav-item px-3">
|
||||||
<a class="nav-link" href="@AuthUrl">
|
<a class="nav-link" href="@AuthUrl">
|
||||||
<span class="oi oi-account-login" aria-hidden="true"></span> Log On
|
<span class="oi oi-account-login" aria-hidden="true"></span> Log On
|
||||||
@@ -33,15 +38,30 @@
|
|||||||
</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="/success-story/list">
|
||||||
|
<span class="oi oi-graph" aria-hidden="true"></span> Success Stories
|
||||||
|
</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>
|
||||||
}
|
}
|
||||||
|
<li class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="/how-it-works">
|
||||||
|
<span class="oi oi-question-mark" aria-hidden="true"></span> How It Works
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/JobsJobsJobs/Client/Shared/ProfileSearchForm.razor
Normal file
60
src/JobsJobsJobs/Client/Shared/ProfileSearchForm.razor
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@using JobsJobsJobs.Shared.Api
|
||||||
|
|
||||||
|
<EditForm Model=@Criteria OnValidSubmit=@OnSearch>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<label for="continentId" class="jjj-label">Continent</label>
|
||||||
|
<InputSelect id="continentId" @bind-Value=@Criteria.ContinentId class="form-control form-control-sm">
|
||||||
|
<option value="">– Any –</option>
|
||||||
|
@foreach (var (id, name) in Continents)
|
||||||
|
{
|
||||||
|
<option value="@id">@name</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 offset-md-2 col-lg-3 offset-lg-0">
|
||||||
|
<label class="jjj-label">Seeking Remote Work?</label><br>
|
||||||
|
<InputRadioGroup @bind-Value=@Criteria.RemoteWork>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteNull" Value=@("") class="form-check-input" />
|
||||||
|
<label for="remoteNull" class="form-check-label">No Selection</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteYes" Value=@("yes") class="form-check-input" />
|
||||||
|
<label for="remoteYes" class="form-check-label">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteNo" Value=@("no") class="form-check-input" />
|
||||||
|
<label for="remoteNo" class="form-check-label">No</label>
|
||||||
|
</div>
|
||||||
|
</InputRadioGroup>
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 col-lg-3">
|
||||||
|
<label for="skillSearch" class="jjj-label">Skill</label>
|
||||||
|
<InputText id="skillSearch" @bind-Value=@Criteria.Skill class="form-control form-control-sm"
|
||||||
|
placeholder="(free-form text)" />
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 col-lg-3">
|
||||||
|
<label for="bioSearch" class="jjj-label">Bio / Experience</label>
|
||||||
|
<InputText id="bioSearch" @bind-Value=@Criteria.BioExperience class="form-control form-control-sm"
|
||||||
|
placeholder="(free-form text)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<br>
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public ProfileSearch Criteria { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnSearch { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<Continent> Continents { get; set; } = default!;
|
||||||
|
}
|
||||||
60
src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor
Normal file
60
src/JobsJobsJobs/Client/Shared/PublicSearchForm.razor
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@using JobsJobsJobs.Shared.Api
|
||||||
|
|
||||||
|
<EditForm Model=@Criteria OnValidSubmit=@OnSearch>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<label for="continentId" class="jjj-label">Continent</label>
|
||||||
|
<InputSelect id="continentId" @bind-Value=@Criteria.ContinentId class="form-control form-control-sm">
|
||||||
|
<option value="">– Any –</option>
|
||||||
|
@foreach (var (id, name) in Continents)
|
||||||
|
{
|
||||||
|
<option value="@id">@name</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<label for="region" class="jjj-label">Region</label>
|
||||||
|
<InputText id="region" @bind-Value=@Criteria.Region class="form-control form-control-sm"
|
||||||
|
placeholder="(free-form text)" />
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 offset-md-2 col-lg-3 offset-lg-0">
|
||||||
|
<label class="jjj-label">Seeking Remote Work?</label><br>
|
||||||
|
<InputRadioGroup @bind-Value=@Criteria.RemoteWork>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteNull" Value=@("") class="form-check-input" />
|
||||||
|
<label for="remoteNull" class="form-check-label">No Selection</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteYes" Value=@("yes") class="form-check-input" />
|
||||||
|
<label for="remoteYes" class="form-check-label">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<InputRadio id="remoteNo" Value=@("no") class="form-check-input" />
|
||||||
|
<label for="remoteNo" class="form-check-label">No</label>
|
||||||
|
</div>
|
||||||
|
</InputRadioGroup>
|
||||||
|
</div>
|
||||||
|
<div class="col col-12 col-sm-6 col-lg-3">
|
||||||
|
<label for="skillSearch" class="jjj-label">Skill</label>
|
||||||
|
<InputText id="skillSearch" @bind-Value=@Criteria.Skill class="form-control form-control-sm"
|
||||||
|
placeholder="(free-form text)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="col">
|
||||||
|
<br>
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public PublicSearch Criteria { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback OnSearch { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<Continent> Continents { get; set; } = default!;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-xs-10 col-sm-10 col-md-6">
|
<div class="col col-xs-10 col-sm-10 col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="skillDesc@(Skill.Id)">Skill</label>
|
<label for="skillDesc@(Skill.Id)" class="jjj-label">Skill</label>
|
||||||
<input type="text" id="skillDesc@(Skill.Id)" @bind="@Skill.Description" class="form-control" maxlength="100"
|
<input type="text" id="skillDesc@(Skill.Id)" @bind="@Skill.Description" class="form-control" maxlength="100"
|
||||||
placeholder="A skill (language, design technique, process, etc.)">
|
placeholder="A skill (language, design technique, process, etc.)">
|
||||||
<ValidationMessage For="@(() => Skill.Description)" />
|
<ValidationMessage For="@(() => Skill.Description)" />
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-xs-12 col-sm-12 col-md-5">
|
<div class="col col-xs-12 col-sm-12 col-md-5">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="skillNotes@(Skill.Id)">Notes</label>
|
<label for="skillNotes@(Skill.Id)" class="jjj-label">Notes</label>
|
||||||
<input type="text" id="skillNotes@(Skill.Id)" @bind="@Skill.Notes" class="form-control" maxlength="100"
|
<input type="text" id="skillNotes@(Skill.Id)" @bind="@Skill.Notes" class="form-control" maxlength="100"
|
||||||
placeholder="A further description of the skill (100 characters max)">
|
placeholder="A further description of the skill (100 characters max)">
|
||||||
<ValidationMessage For="@(() => Skill.Notes)" />
|
<ValidationMessage For="@(() => Skill.Notes)" />
|
||||||
|
|||||||
@@ -65,3 +65,8 @@ label.jjj-required::after {
|
|||||||
color: red;
|
color: red;
|
||||||
content: ' *';
|
content: ' *';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.jjj-label,
|
||||||
|
::placeholder {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
function setPageTitle(theTitle) {
|
function setPageTitle(theTitle) {
|
||||||
document.title = theTitle
|
document.title = theTitle
|
||||||
}
|
}
|
||||||
|
function getTimeZone() {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
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>1.0.1.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.0.1.0</FileVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||||
@@ -44,6 +45,11 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current citizen ID
|
||||||
|
/// </summary>
|
||||||
|
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||||
|
|
||||||
[HttpGet("log-on/{authCode}")]
|
[HttpGet("log-on/{authCode}")]
|
||||||
public async Task<IActionResult> LogOn([FromRoute] string authCode)
|
public async Task<IActionResult> LogOn([FromRoute] string authCode)
|
||||||
{
|
{
|
||||||
@@ -59,7 +65,8 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
var citizen = await _db.FindCitizenByNAUser(account.Username);
|
var citizen = await _db.FindCitizenByNAUser(account.Username);
|
||||||
if (citizen == null)
|
if (citizen == null)
|
||||||
{
|
{
|
||||||
citizen = new Citizen(await CitizenId.Create(), account.Username, account.DisplayName, account.Url,
|
citizen = new Citizen(await CitizenId.Create(), account.Username,
|
||||||
|
string.IsNullOrWhiteSpace(account.DisplayName) ? null : account.DisplayName, null, account.Url,
|
||||||
now, now);
|
now, now);
|
||||||
await _db.AddCitizen(citizen);
|
await _db.AddCitizen(citizen);
|
||||||
}
|
}
|
||||||
@@ -67,7 +74,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
{
|
{
|
||||||
citizen = citizen with
|
citizen = citizen with
|
||||||
{
|
{
|
||||||
DisplayName = account.DisplayName,
|
DisplayName = string.IsNullOrWhiteSpace(account.DisplayName) ? null : account.DisplayName,
|
||||||
LastSeenOn = now
|
LastSeenOn = now
|
||||||
};
|
};
|
||||||
_db.UpdateCitizen(citizen);
|
_db.UpdateCitizen(citizen);
|
||||||
@@ -77,7 +84,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
// Step 3 - Generate JWT
|
// Step 3 - Generate JWT
|
||||||
var jwt = Auth.CreateJwt(citizen, _config);
|
var jwt = Auth.CreateJwt(citizen, _config);
|
||||||
|
|
||||||
return new JsonResult(new LogOnSuccess(jwt, citizen.Id.ToString(), citizen.DisplayName));
|
return new JsonResult(new LogOnSuccess(jwt, citizen.Id.ToString(), citizen.CitizenName));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@@ -87,5 +94,15 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
var citizen = await _db.FindCitizenById(CitizenId.Parse(id));
|
var citizen = await _db.FindCitizenById(CitizenId.Parse(id));
|
||||||
return citizen == null ? NotFound() : Ok(citizen);
|
return citizen == null ? NotFound() : Ok(citizen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpDelete("")]
|
||||||
|
public async Task<IActionResult> Remove()
|
||||||
|
{
|
||||||
|
await _db.DeleteCitizen(CurrentCitizenId);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using JobsJobsJobs.Server.Data;
|
using JobsJobsJobs.Server.Data;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
/// API endpoint for continent information
|
/// API endpoint for continent information
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[Authorize]
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class ContinentController : ControllerBase
|
public class ContinentController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="db">The data context to use for this request</param>
|
/// <param name="db">The data context to use for this request</param>
|
||||||
|
/// <param name="clock">The clock instance to use for this request</param>
|
||||||
public ProfileController(JobsDbContext db, IClock clock)
|
public ProfileController(JobsDbContext db, IClock clock)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -45,7 +46,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||||
|
|
||||||
// This returns 204 to indicate that there is no profile data for the current citizen (if, of course, that is
|
// This returns 204 to indicate that there is no profile data for the current citizen (if, of course, that is
|
||||||
// the case. The version where an ID is specified returns 404, which is an error condition, as it should not
|
// the case). The version where an ID is specified returns 404, which is an error condition, as it should not
|
||||||
// occur unless someone is messing with a URL.
|
// occur unless someone is messing with a URL.
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<IActionResult> Get()
|
public async Task<IActionResult> Get()
|
||||||
@@ -88,14 +89,14 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
|||||||
foreach (var skill in skills) await _db.SaveSkill(skill);
|
foreach (var skill in skills) await _db.SaveSkill(skill);
|
||||||
await _db.DeleteMissingSkills(CurrentCitizenId, skills.Select(s => s.Id));
|
await _db.DeleteMissingSkills(CurrentCitizenId, skills.Select(s => s.Id));
|
||||||
|
|
||||||
|
// Real Name
|
||||||
|
_db.Update((await _db.FindCitizenById(CurrentCitizenId))!
|
||||||
|
with { RealName = string.IsNullOrWhiteSpace(form.RealName) ? null : form.RealName });
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("skills")]
|
|
||||||
public async Task<IActionResult> GetSkills() =>
|
|
||||||
Ok(await _db.FindSkillsByCitizen(CurrentCitizenId));
|
|
||||||
|
|
||||||
[HttpGet("count")]
|
[HttpGet("count")]
|
||||||
public async Task<IActionResult> GetProfileCount() =>
|
public async Task<IActionResult> GetProfileCount() =>
|
||||||
Ok(new Count(await _db.CountProfiles()));
|
Ok(new Count(await _db.CountProfiles()));
|
||||||
@@ -110,5 +111,37 @@ 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([FromQuery] ProfileSearch search) =>
|
||||||
|
Ok(await _db.SearchProfiles(search));
|
||||||
|
|
||||||
|
[HttpGet("public-search")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> SearchPublic([FromQuery] PublicSearch search) =>
|
||||||
|
Ok(await _db.SearchPublicProfiles(search));
|
||||||
|
|
||||||
|
[HttpPatch("employment-found")]
|
||||||
|
public async Task<IActionResult> EmploymentFound()
|
||||||
|
{
|
||||||
|
var profile = await _db.FindProfileByCitizen(CurrentCitizenId);
|
||||||
|
if (profile == null) return NotFound();
|
||||||
|
|
||||||
|
var updated = profile with { SeekingEmployment = false };
|
||||||
|
_db.Update(updated);
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("")]
|
||||||
|
public async Task<IActionResult> Remove()
|
||||||
|
{
|
||||||
|
await _db.DeleteProfileByCitizen(CurrentCitizenId);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using JobsJobsJobs.Server.Data;
|
||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NodaTime;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API controller for success stories
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class SuccessController : Controller
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data context
|
||||||
|
/// </summary>
|
||||||
|
private readonly JobsDbContext _db;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The NodaTime clock instance
|
||||||
|
/// </summary>
|
||||||
|
private readonly IClock _clock;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="db">The data context to use for this request</param>
|
||||||
|
/// <param name="clock">The clock instance to use for this request</param>
|
||||||
|
public SuccessController(JobsDbContext db, IClock clock)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current citizen ID
|
||||||
|
/// </summary>
|
||||||
|
private CitizenId CurrentCitizenId => CitizenId.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> Retrieve(string id) =>
|
||||||
|
Ok(await _db.FindSuccessById(SuccessId.Parse(id)));
|
||||||
|
|
||||||
|
[HttpPost("save")]
|
||||||
|
public async Task<IActionResult> Save([FromBody] StoryForm form)
|
||||||
|
{
|
||||||
|
if (form.Id == "new")
|
||||||
|
{
|
||||||
|
var story = new Success(await SuccessId.Create(), CurrentCitizenId, _clock.GetCurrentInstant(),
|
||||||
|
form.FromHere, string.IsNullOrWhiteSpace(form.Story) ? null : new MarkdownString(form.Story));
|
||||||
|
await _db.AddAsync(story);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var story = await _db.FindSuccessById(SuccessId.Parse(form.Id));
|
||||||
|
if (story == null) return NotFound();
|
||||||
|
var updated = story with
|
||||||
|
{
|
||||||
|
FromHere = form.FromHere,
|
||||||
|
Story = string.IsNullOrWhiteSpace(form.Story) ? null : new MarkdownString(form.Story)
|
||||||
|
};
|
||||||
|
_db.Update(updated);
|
||||||
|
}
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<IActionResult> List() =>
|
||||||
|
Ok(await _db.AllStories());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,7 @@ namespace JobsJobsJobs.Server
|
|||||||
Subject = new ClaimsIdentity(new[]
|
Subject = new ClaimsIdentity(new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.NameIdentifier, citizen.Id.ToString()),
|
new Claim(ClaimTypes.NameIdentifier, citizen.Id.ToString()),
|
||||||
new Claim(ClaimTypes.Name, citizen.DisplayName),
|
new Claim(ClaimTypes.Name, citizen.CitizenName),
|
||||||
}),
|
}),
|
||||||
Expires = DateTime.UtcNow.AddHours(2),
|
Expires = DateTime.UtcNow.AddHours(2),
|
||||||
Issuer = "https://noagendacareers.com",
|
Issuer = "https://noagendacareers.com",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="citizen">The citizen to be added</param>
|
/// <param name="citizen">The citizen to be added</param>
|
||||||
public static async Task AddCitizen(this JobsDbContext db, Citizen citizen) =>
|
public static async Task AddCitizen(this JobsDbContext db, Citizen citizen) =>
|
||||||
await db.Citizens.AddAsync(citizen);
|
await db.Citizens.AddAsync(citizen).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update a citizen after they have logged on (update last seen, sync display name)
|
/// Update a citizen after they have logged on (update last seen, sync display name)
|
||||||
@@ -42,5 +42,20 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
/// <param name="citizen">The updated citizen</param>
|
/// <param name="citizen">The updated citizen</param>
|
||||||
public static void UpdateCitizen(this JobsDbContext db, Citizen citizen) =>
|
public static void UpdateCitizen(this JobsDbContext db, Citizen citizen) =>
|
||||||
db.Entry(citizen).State = EntityState.Modified;
|
db.Entry(citizen).State = EntityState.Modified;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a citizen
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="citizenId">The ID of the citizen to be deleted</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task DeleteCitizen(this JobsDbContext db, CitizenId citizenId)
|
||||||
|
{
|
||||||
|
var id = citizenId.ToString();
|
||||||
|
await db.DeleteProfileByCitizen(citizenId).ConfigureAwait(false);
|
||||||
|
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.success WHERE citizen_id = {id}")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.citizen WHERE id = {id}")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,38 +12,36 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
/// Citizen ID converter
|
/// Citizen ID converter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ValueConverter<CitizenId, string> CitizenIdConverter =
|
public static readonly ValueConverter<CitizenId, string> CitizenIdConverter =
|
||||||
new ValueConverter<CitizenId, string>(v => v.ToString(), v => CitizenId.Parse(v));
|
new(v => v.ToString(), v => CitizenId.Parse(v));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Continent ID converter
|
/// Continent ID converter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ValueConverter<ContinentId, string> ContinentIdConverter =
|
public static readonly ValueConverter<ContinentId, string> ContinentIdConverter =
|
||||||
new ValueConverter<ContinentId, string>(v => v.ToString(), v => ContinentId.Parse(v));
|
new(v => v.ToString(), v => ContinentId.Parse(v));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Markdown converter
|
/// Markdown converter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ValueConverter<MarkdownString, string> MarkdownStringConverter =
|
public static readonly ValueConverter<MarkdownString, string> MarkdownStringConverter =
|
||||||
new ValueConverter<MarkdownString, string>(v => v.Text, v => new MarkdownString(v));
|
new(v => v.Text, v => new MarkdownString(v));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Markdown converter for possibly-null values
|
/// Markdown converter for possibly-null values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ValueConverter<MarkdownString?, string?> OptionalMarkdownStringConverter =
|
public static readonly ValueConverter<MarkdownString?, string?> OptionalMarkdownStringConverter =
|
||||||
new ValueConverter<MarkdownString?, string?>(
|
new(v => v == null ? null : v.Text, v => v == null ? null : new MarkdownString(v));
|
||||||
v => v == null ? null : v.Text,
|
|
||||||
v => v == null ? null : new MarkdownString(v));
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skill ID converter
|
/// Skill ID converter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ValueConverter<SkillId, string> SkillIdConverter =
|
public static readonly ValueConverter<SkillId, string> SkillIdConverter =
|
||||||
new ValueConverter<SkillId, string>(v => v.ToString(), v => SkillId.Parse(v));
|
new(v => v.ToString(), v => SkillId.Parse(v));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Success ID converter
|
/// Success ID converter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly ValueConverter<SuccessId, string> SuccessIdConverter =
|
public static readonly ValueConverter<SuccessId, string> SuccessIdConverter =
|
||||||
new ValueConverter<SuccessId, string>(v => v.ToString(), v => SuccessId.Parse(v));
|
new(v => v.ToString(), v => SuccessId.Parse(v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,13 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
m.Property(e => e.Id).HasColumnName("id").IsRequired().HasMaxLength(12)
|
||||||
.HasConversion(Converters.CitizenIdConverter);
|
.HasConversion(Converters.CitizenIdConverter);
|
||||||
m.Property(e => e.NaUser).HasColumnName("na_user").IsRequired().HasMaxLength(50);
|
m.Property(e => e.NaUser).HasColumnName("na_user").IsRequired().HasMaxLength(50);
|
||||||
m.Property(e => e.DisplayName).HasColumnName("display_name").IsRequired().HasMaxLength(255);
|
m.Property(e => e.DisplayName).HasColumnName("display_name").HasMaxLength(255);
|
||||||
m.Property(e => e.ProfileUrl).HasColumnName("profile_url").IsRequired().HasMaxLength(1_024);
|
m.Property(e => e.ProfileUrl).HasColumnName("profile_url").IsRequired().HasMaxLength(1_024);
|
||||||
m.Property(e => e.JoinedOn).HasColumnName("joined_on").IsRequired();
|
m.Property(e => e.JoinedOn).HasColumnName("joined_on").IsRequired();
|
||||||
m.Property(e => e.LastSeenOn).HasColumnName("last_seen_on").IsRequired();
|
m.Property(e => e.LastSeenOn).HasColumnName("last_seen_on").IsRequired();
|
||||||
|
m.Property(e => e.RealName).HasColumnName("real_name").HasMaxLength(255);
|
||||||
m.HasIndex(e => e.NaUser).IsUnique();
|
m.HasIndex(e => e.NaUser).IsUnique();
|
||||||
|
m.Ignore(e => e.CitizenName);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Continent>(m =>
|
modelBuilder.Entity<Continent>(m =>
|
||||||
@@ -81,8 +83,12 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
m.Property(e => e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired();
|
m.Property(e => e.LastUpdatedOn).HasColumnName("last_updated_on").IsRequired();
|
||||||
m.Property(e => e.Experience).HasColumnName("experience")
|
m.Property(e => e.Experience).HasColumnName("experience")
|
||||||
.HasConversion(Converters.OptionalMarkdownStringConverter);
|
.HasConversion(Converters.OptionalMarkdownStringConverter);
|
||||||
m.Ignore(e => e.Continent);
|
m.HasOne(e => e.Continent)
|
||||||
m.Ignore(e => e.Skills);
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.ContinentId);
|
||||||
|
m.HasMany(e => e.Skills)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey(e => e.CitizenId);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Skill>(m =>
|
modelBuilder.Entity<Skill>(m =>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using JobsJobsJobs.Shared;
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Npgsql;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Server.Data
|
namespace JobsJobsJobs.Server.Data
|
||||||
@@ -19,24 +18,13 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="citizenId">The ID of the citizen whose profile should be retrieved</param>
|
/// <param name="citizenId">The ID of the citizen whose profile should be retrieved</param>
|
||||||
/// <returns>The profile, or null if it does not exist</returns>
|
/// <returns>The profile, or null if it does not exist</returns>
|
||||||
public static async Task<Profile?> FindProfileByCitizen(this JobsDbContext db, CitizenId citizenId)
|
public static async Task<Profile?> FindProfileByCitizen(this JobsDbContext db, CitizenId citizenId) =>
|
||||||
{
|
await db.Profiles.AsNoTracking()
|
||||||
var profile = await db.Profiles.AsNoTracking()
|
.Include(p => p.Continent)
|
||||||
|
.Include(p => p.Skills)
|
||||||
.SingleOrDefaultAsync(p => p.Id == citizenId)
|
.SingleOrDefaultAsync(p => p.Id == citizenId)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (profile != null)
|
|
||||||
{
|
|
||||||
return profile with
|
|
||||||
{
|
|
||||||
Continent = await db.FindContinentById(profile.ContinentId).ConfigureAwait(false),
|
|
||||||
Skills = (await db.FindSkillsByCitizen(citizenId).ConfigureAwait(false)).ToArray()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a profile
|
/// Save a profile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,16 +41,6 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve all skills for the given citizen
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="citizenId">The ID of the citizen whose skills should be retrieved</param>
|
|
||||||
/// <returns>The skills defined for this citizen</returns>
|
|
||||||
public static async Task<IEnumerable<Skill>> FindSkillsByCitizen(this JobsDbContext db, CitizenId citizenId) =>
|
|
||||||
await db.Skills.AsNoTracking()
|
|
||||||
.Where(s => s.CitizenId == citizenId)
|
|
||||||
.ToListAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Save a skill
|
/// Save a skill
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -90,7 +68,7 @@ namespace JobsJobsJobs.Server.Data
|
|||||||
if (!ids.Any()) return;
|
if (!ids.Any()) return;
|
||||||
|
|
||||||
db.Skills.RemoveRange(await db.Skills.AsNoTracking()
|
db.Skills.RemoveRange(await db.Skills.AsNoTracking()
|
||||||
.Where(s => !ids.Contains(s.Id)).ToListAsync()
|
.Where(s => s.CitizenId == citizenId && !ids.Contains(s.Id)).ToListAsync()
|
||||||
.ConfigureAwait(false));
|
.ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,5 +86,121 @@ 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>
|
||||||
|
/// <param name="search">The search parameters</param>
|
||||||
|
/// <returns>The information for profiles matching the criteria</returns>
|
||||||
|
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db,
|
||||||
|
ProfileSearch search)
|
||||||
|
{
|
||||||
|
var query = db.Profiles
|
||||||
|
.Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c });
|
||||||
|
|
||||||
|
var useIds = false;
|
||||||
|
var citizenIds = new List<CitizenId>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.ContinentId))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.Profile.ContinentId == ContinentId.Parse(search.ContinentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.RemoteWork))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.Profile.RemoteWork == (search.RemoteWork == "yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.Skill))
|
||||||
|
{
|
||||||
|
useIds = true;
|
||||||
|
citizenIds.AddRange(await db.Skills
|
||||||
|
.Where(s => s.Description.ToLower().Contains(search.Skill.ToLower()))
|
||||||
|
.Select(s => s.CitizenId)
|
||||||
|
.ToListAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.BioExperience))
|
||||||
|
{
|
||||||
|
useIds = true;
|
||||||
|
citizenIds.AddRange(await db.Profiles
|
||||||
|
.FromSqlRaw("SELECT citizen_id FROM profile WHERE biography ILIKE {0} OR experience ILIKE {0}",
|
||||||
|
$"%{search.BioExperience}%")
|
||||||
|
.Select(p => p.Id)
|
||||||
|
.ToListAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useIds)
|
||||||
|
{
|
||||||
|
query = query.Where(it => citizenIds.Contains(it.Citizen.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.CitizenName,
|
||||||
|
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
|
||||||
|
.ToListAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search public profiles by the given criteria
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="search">The search parameters</param>
|
||||||
|
/// <returns>The information for profiles matching the criteria</returns>
|
||||||
|
public static async Task<IEnumerable<PublicSearchResult>> SearchPublicProfiles(this JobsDbContext db,
|
||||||
|
PublicSearch search)
|
||||||
|
{
|
||||||
|
var query = db.Profiles
|
||||||
|
.Include(it => it.Continent)
|
||||||
|
.Include(it => it.Skills)
|
||||||
|
.Where(it => it.IsPublic);
|
||||||
|
|
||||||
|
var useIds = false;
|
||||||
|
var citizenIds = new List<CitizenId>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.ContinentId))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.ContinentId == ContinentId.Parse(search.ContinentId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.Region))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.Region.ToLower().Contains(search.Region.ToLower()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.RemoteWork))
|
||||||
|
{
|
||||||
|
query = query.Where(it => it.RemoteWork == (search.RemoteWork == "yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(search.Skill))
|
||||||
|
{
|
||||||
|
useIds = true;
|
||||||
|
citizenIds.AddRange(await db.Skills
|
||||||
|
.Where(s => s.Description.ToLower().Contains(search.Skill.ToLower()))
|
||||||
|
.Select(s => s.CitizenId)
|
||||||
|
.ToListAsync().ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useIds)
|
||||||
|
{
|
||||||
|
query = query.Where(it => citizenIds.Contains(it.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.Select(x => new PublicSearchResult(x.Continent!.Name, x.Region, x.RemoteWork,
|
||||||
|
x.Skills.Select(sk => $"{sk.Description} ({sk.Notes})")))
|
||||||
|
.ToListAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete skills and profile for the given citizen
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="citizenId">The ID of the citizen whose profile should be deleted</param>
|
||||||
|
public static async Task DeleteProfileByCitizen(this JobsDbContext db, CitizenId citizenId)
|
||||||
|
{
|
||||||
|
var id = citizenId.ToString();
|
||||||
|
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.skill WHERE citizen_id = {id}")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM jjj.profile WHERE citizen_id = {id}")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/JobsJobsJobs/Server/Data/SuccessExtensions.cs
Normal file
35
src/JobsJobsJobs/Server/Data/SuccessExtensions.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Server.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extensions to JobsDbContext to support manipulation of success stories
|
||||||
|
/// </summary>
|
||||||
|
public static class SuccessExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get a success story by its ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the story to retrieve</param>
|
||||||
|
/// <returns>The success story, if found</returns>
|
||||||
|
public static async Task<Success?> FindSuccessById(this JobsDbContext db, SuccessId id) =>
|
||||||
|
await db.Successes.AsNoTracking().SingleOrDefaultAsync(s => s.Id == id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a list of success stories, with the information needed for the list page
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A list of success stories, citizen names, and dates</returns>
|
||||||
|
public static async Task<IEnumerable<StoryEntry>> AllStories(this JobsDbContext db) =>
|
||||||
|
await db.Successes
|
||||||
|
.Join(db.Citizens, s => s.CitizenId, c => c.Id, (s, c) => new { Success = s, Citizen = c })
|
||||||
|
.OrderByDescending(it => it.Success.RecordedOn)
|
||||||
|
.Select(it => new StoryEntry(it.Success.Id, it.Citizen.Id, it.Citizen.CitizenName,
|
||||||
|
it.Success.RecordedOn, it.Success.FromHere, it.Success.Story != null))
|
||||||
|
.ToListAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<ActiveDebugProfile>JobsJobsJobs.Server</ActiveDebugProfile>
|
<ActiveDebugProfile>JobsJobsJobs.Server</ActiveDebugProfile>
|
||||||
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||||
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
|
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
|
||||||
<NameOfLastUsedPublishProfile>FolderProfile</NameOfLastUsedPublishProfile>
|
<NameOfLastUsedPublishProfile>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||||
|
|||||||
@@ -13,10 +13,13 @@
|
|||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="css/app.css" rel="stylesheet" />
|
<link href="css/app.css" rel="stylesheet" />
|
||||||
<link href="JobsJobsJobs.Client.styles.css" rel="stylesheet" />
|
<link href="JobsJobsJobs.Client.styles.css" rel="stylesheet" />
|
||||||
|
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet">
|
||||||
</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 +27,19 @@
|
|||||||
<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
|
||||||
|
}
|
||||||
|
function getTimeZone() {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\bin\Release\net5.0\publish\</_PublishTargetUrl>
|
<_PublishTargetUrl>C:\Users\danie\Documents\sandbox\jobs-jobs-jobs\src\JobsJobsJobs\Server\bin\Release\net5.0\publish\</_PublishTargetUrl>
|
||||||
|
<History>True|2021-06-15T02:08:33.4261507Z;True|2021-06-14T21:58:04.2622487-04:00;True|2021-03-16T19:34:57.2747439-04:00;</History>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
|
using Blazored.Toast;
|
||||||
using JobsJobsJobs.Server.Data;
|
using JobsJobsJobs.Server.Data;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
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 +34,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();
|
||||||
@@ -43,6 +44,7 @@ namespace JobsJobsJobs.Server
|
|||||||
services.AddRazorPages()
|
services.AddRazorPages()
|
||||||
.AddJsonOptions(options =>
|
.AddJsonOptions(options =>
|
||||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
|
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
|
||||||
|
services.AddBlazoredToast();
|
||||||
services.AddAuthentication(options =>
|
services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
@@ -98,7 +100,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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ namespace JobsJobsJobs.Shared.Api
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPublic { get; set; }
|
public bool IsPublic { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's real name
|
||||||
|
/// </summary>
|
||||||
|
[StringLength(255)]
|
||||||
|
public string RealName { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ID of the continent on which the citizen is located
|
/// The ID of the continent on which the citizen is located
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
37
src/JobsJobsJobs/Shared/Api/ProfileSearch.cs
Normal file
37
src/JobsJobsJobs/Shared/Api/ProfileSearch.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The various ways profiles can be searched
|
||||||
|
/// </summary>
|
||||||
|
public class ProfileSearch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve citizens from this continent
|
||||||
|
/// </summary>
|
||||||
|
public string? ContinentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text for a search within a citizen's skills
|
||||||
|
/// </summary>
|
||||||
|
public string? Skill { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text for a search with a citizen's professional biography and experience fields
|
||||||
|
/// </summary>
|
||||||
|
public string? BioExperience { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to retrieve citizens who do or do not want remote work
|
||||||
|
/// </summary>
|
||||||
|
public string RemoteWork { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the search empty?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmptySearch =>
|
||||||
|
string.IsNullOrEmpty(ContinentId)
|
||||||
|
&& string.IsNullOrEmpty(Skill)
|
||||||
|
&& string.IsNullOrEmpty(BioExperience)
|
||||||
|
&& string.IsNullOrEmpty(RemoteWork);
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
37
src/JobsJobsJobs/Shared/Api/PublicSearch.cs
Normal file
37
src/JobsJobsJobs/Shared/Api/PublicSearch.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The parameters for a public job search
|
||||||
|
/// </summary>
|
||||||
|
public class PublicSearch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve citizens from this continent
|
||||||
|
/// </summary>
|
||||||
|
public string? ContinentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve citizens from this region
|
||||||
|
/// </summary>
|
||||||
|
public string? Region { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text for a search within a citizen's skills
|
||||||
|
/// </summary>
|
||||||
|
public string? Skill { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to retrieve citizens who do or do not want remote work
|
||||||
|
/// </summary>
|
||||||
|
public string RemoteWork { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the search empty?
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEmptySearch =>
|
||||||
|
string.IsNullOrEmpty(ContinentId)
|
||||||
|
&& string.IsNullOrEmpty(Region)
|
||||||
|
&& string.IsNullOrEmpty(Skill)
|
||||||
|
&& string.IsNullOrEmpty(RemoteWork);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/JobsJobsJobs/Shared/Api/PublicSearchResult.cs
Normal file
13
src/JobsJobsJobs/Shared/Api/PublicSearchResult.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A public profile search result
|
||||||
|
/// </summary>
|
||||||
|
public record PublicSearchResult(
|
||||||
|
string Continent,
|
||||||
|
string Region,
|
||||||
|
bool RemoteWork,
|
||||||
|
IEnumerable<string> Skills);
|
||||||
|
}
|
||||||
15
src/JobsJobsJobs/Shared/Api/StoryEntry.cs
Normal file
15
src/JobsJobsJobs/Shared/Api/StoryEntry.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An entry in the list of success stories
|
||||||
|
/// </summary>
|
||||||
|
public record StoryEntry(
|
||||||
|
SuccessId Id,
|
||||||
|
CitizenId CitizenId,
|
||||||
|
string CitizenName,
|
||||||
|
Instant RecordedOn,
|
||||||
|
bool FromHere,
|
||||||
|
bool HasStory);
|
||||||
|
}
|
||||||
23
src/JobsJobsJobs/Shared/Api/StoryForm.cs
Normal file
23
src/JobsJobsJobs/Shared/Api/StoryForm.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data required to provide a success story
|
||||||
|
/// </summary>
|
||||||
|
public class StoryForm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of this story
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = "new";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
||||||
|
/// </summary>
|
||||||
|
public bool FromHere { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The success story
|
||||||
|
/// </summary>
|
||||||
|
public string Story { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,15 @@ namespace JobsJobsJobs.Shared
|
|||||||
public record Citizen(
|
public record Citizen(
|
||||||
CitizenId Id,
|
CitizenId Id,
|
||||||
string NaUser,
|
string NaUser,
|
||||||
string DisplayName,
|
string? DisplayName,
|
||||||
|
string? RealName,
|
||||||
string ProfileUrl,
|
string ProfileUrl,
|
||||||
Instant JoinedOn,
|
Instant JoinedOn,
|
||||||
Instant LastSeenOn);
|
Instant LastSeenOn)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user's name by which they should be known
|
||||||
|
/// </summary>
|
||||||
|
public string CitizenName => RealName ?? DisplayName ?? NaUser;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Shared
|
namespace JobsJobsJobs.Shared
|
||||||
{
|
{
|
||||||
@@ -26,6 +26,6 @@ namespace JobsJobsJobs.Shared
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convenience property for skills associated with a profile
|
/// Convenience property for skills associated with a profile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Skill[] Skills { get; set; } = Array.Empty<Skill>();
|
public ICollection<Skill> Skills { get; set; } = new List<Skill>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace JobsJobsJobs.Shared
|
using System;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Shared
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A result with two different possibilities
|
/// A result with two different possibilities
|
||||||
@@ -60,5 +62,24 @@
|
|||||||
/// <param name="error">The error message</param>
|
/// <param name="error">The error message</param>
|
||||||
/// <returns>The Error result</returns>
|
/// <returns>The Error result</returns>
|
||||||
public static Result<TOk> AsError(string error) => new Result<TOk>(false) { Error = error };
|
public static Result<TOk> AsError(string error) => new Result<TOk>(false) { Error = error };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transform a result if it is OK, passing the error along if it is an error
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="f">The transforming function</param>
|
||||||
|
/// <param name="result">The existing result</param>
|
||||||
|
/// <returns>The resultant result</returns>
|
||||||
|
public static Result<TOk> Bind(Func<TOk, Result<TOk>> f, Result<TOk> result) =>
|
||||||
|
result.IsOk ? f(result.Ok) : result;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transform a result to a different type if it is OK, passing the error along if it is an error
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TOther">The type to which the result is transformed</typeparam>
|
||||||
|
/// <param name="f">The transforming function</param>
|
||||||
|
/// <param name="result">The existing result</param>
|
||||||
|
/// <returns>The resultant result</returns>
|
||||||
|
public static Result<TOther> Map<TOther>(Func<TOk, Result<TOther>> f, Result<TOk> result) =>
|
||||||
|
result.IsOk ? f(result.Ok) : Result<TOther>.AsError(result.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/database/12-add-real-name.sql
Normal file
8
src/database/12-add-real-name.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE jjj.citizen ALTER COLUMN display_name DROP NOT NULL;
|
||||||
|
ALTER TABLE jjj.citizen ADD COLUMN real_name VARCHAR(255);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN jjj.citizen.real_name
|
||||||
|
IS 'The real name of the user';
|
||||||
|
|
||||||
|
-- This can be run as often as needed
|
||||||
|
UPDATE jjj.citizen SET display_name = NULL WHERE display_name = na_user;
|
||||||
Reference in New Issue
Block a user