Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5c88a1034 | |||
| 46882bdfc6 | |||
| a6fd891cc5 | |||
| 7f7eb191fb | |||
| 340b93c6d7 | |||
| 4155072990 | |||
| feb3c5fd4a | |||
| 7839b8eb57 |
@@ -1,5 +1,11 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using Microsoft.JSInterop;
|
||||
using NodaTime;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client
|
||||
{
|
||||
@@ -13,6 +19,17 @@ namespace JobsJobsJobs.Client
|
||||
/// </summary>
|
||||
public class AppState
|
||||
{
|
||||
/// <summary>
|
||||
/// The application version, as a nice display string
|
||||
/// </summary>
|
||||
public static Lazy<string> Version => new Lazy<string>(() =>
|
||||
{
|
||||
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 = () => { };
|
||||
|
||||
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() { }
|
||||
|
||||
private void NotifyChanged() => OnChange.Invoke();
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
|
||||
<h3>Welcome, @state.User!.Name!</h3>
|
||||
|
||||
@if (RetrievingData)
|
||||
{
|
||||
<p>Retrieving your employment profile...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ErrorList Errors=@ErrorMessages>
|
||||
<Loading OnLoad=@LoadProfile Message=@(new MarkupString("Retrieving your employment profile…"))>
|
||||
@if (Profile != null)
|
||||
{
|
||||
<p>
|
||||
@@ -20,6 +14,13 @@ else
|
||||
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>
|
||||
@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
|
||||
{
|
||||
@@ -28,74 +29,19 @@ else
|
||||
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>
|
||||
<text>Take a look around and see if you can help them find work!</text>
|
||||
}
|
||||
</p> */
|
||||
}
|
||||
</ErrorList>
|
||||
}
|
||||
</p>
|
||||
</Loading>
|
||||
<hr>
|
||||
<h4>Phase 3 – What Works <small><em>(<span class="text-uppercase">In Progress</span> ~~ Last Updated January 10<sup>th</sup>, 2021)</em></small></h4>
|
||||
<p>
|
||||
The “View Profiles” link at the side does not have any search capabilities, but it does provide a list of
|
||||
citizens who have filled out profiles, along with a way to view those profiles.
|
||||
To see what is currently done, and how this application works, check out “How It Works” in the sidebar.
|
||||
The application now has 4 of 5 phases complete towards version 1.0; the documentation was last updated January
|
||||
31<sup>st</sup>, 2021.
|
||||
</p>
|
||||
<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>
|
||||
@@ -11,11 +11,6 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
/// </summary>
|
||||
public partial class Dashboard : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the data is being retrieved
|
||||
/// </summary>
|
||||
private bool RetrievingData { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The user's profile
|
||||
/// </summary>
|
||||
@@ -27,11 +22,10 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
private int ProfileCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Error messages from data access
|
||||
/// Load the user's profile information
|
||||
/// </summary>
|
||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
/// <param name="errors">A collection to report errors that may be encountered</param>
|
||||
public async Task LoadProfile(ICollection<string> errors)
|
||||
{
|
||||
if (state.User != null)
|
||||
{
|
||||
@@ -47,7 +41,7 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(profileTask.Result.Error);
|
||||
errors.Add(profileTask.Result.Error);
|
||||
}
|
||||
|
||||
if (profileCountTask.Result.IsOk)
|
||||
@@ -56,10 +50,8 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(profileCountTask.Result.Error);
|
||||
errors.Add(profileCountTask.Result.Error);
|
||||
}
|
||||
|
||||
RetrievingData = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
|
||||
<h3>Employment Profile</h3>
|
||||
|
||||
@if (AllLoaded)
|
||||
{
|
||||
<ErrorList Errors=@ErrorMessages>
|
||||
<Loading OnLoad=@SetUpProfile Message=@(new MarkupString("Loading Your Profile…"))>
|
||||
<EditForm Model=@ProfileForm OnValidSubmit=@SaveProfile>
|
||||
<DataAnnotationsValidator />
|
||||
<div class="form-row">
|
||||
@@ -18,6 +16,12 @@
|
||||
<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>
|
||||
@@ -111,9 +115,4 @@
|
||||
<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>
|
||||
}
|
||||
</Loading>
|
||||
|
||||
@@ -19,9 +19,10 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
private int _newSkillCounter = 0;
|
||||
|
||||
/// <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>
|
||||
private bool AllLoaded { get; set; } = false;
|
||||
private bool IsSeeking { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The form for this page
|
||||
@@ -39,26 +40,18 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
private bool IsNew { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Error messages from API access
|
||||
/// Set up the data needed to add or edit the user's profile
|
||||
/// </summary>
|
||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
/// <param name="errors">The collection where errors can be reported</param>
|
||||
public async Task SetUpProfile(ICollection<string> errors)
|
||||
{
|
||||
ServerApi.SetJwt(http, state);
|
||||
var continentTask = ServerApi.RetrieveMany<Continent>(http, "continent/all");
|
||||
var continentTask = state.GetContinents(http);
|
||||
var profileTask = ServerApi.RetrieveProfile(http, state);
|
||||
|
||||
await Task.WhenAll(continentTask, profileTask);
|
||||
|
||||
if (continentTask.Result.IsOk)
|
||||
{
|
||||
Continents = continentTask.Result.Ok;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(continentTask.Result.Error);
|
||||
}
|
||||
Continents = continentTask.Result;
|
||||
|
||||
if (profileTask.Result.IsOk)
|
||||
{
|
||||
@@ -70,15 +63,14 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
else
|
||||
{
|
||||
ProfileForm = ProfileForm.FromProfile(profileTask.Result.Ok);
|
||||
IsSeeking = profileTask.Result.Ok.SeekingEmployment;
|
||||
}
|
||||
if (ProfileForm.Skills.Count == 0) AddNewSkill();
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(profileTask.Result.Error);
|
||||
errors.Add(profileTask.Result.Error);
|
||||
}
|
||||
|
||||
AllLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,6 +110,5 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||
toast.ShowError($"{(int)res.StatusCode} {error}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
95
src/JobsJobsJobs/Client/Pages/HowItWorks.razor
Normal file
95
src/JobsJobsJobs/Client/Pages/HowItWorks.razor
Normal file
@@ -0,0 +1,95 @@
|
||||
@page "/how-it-works"
|
||||
|
||||
<PageTitle Title="How It Works" />
|
||||
|
||||
<h3>How It Works</h3>
|
||||
|
||||
<p>
|
||||
Phase 4 is complete, which means that the entire logged-in version of the application is now available. There are
|
||||
GitHub issues for each one
|
||||
(<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/1" target="_blank">Phase 1</a> •
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/2" target="_blank">Phase 2</a> •
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/3" target="_blank">Phase 3</a> •
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/4" target="_blank">Phase 4</a>), and if you run into any
|
||||
issues with it, feel free to
|
||||
<a href="https://github.com/bit-badger/jobs-jobs-jobs/issues" target="_blank">let us know on GitHub</a>, or look up
|
||||
@("@")danieljsummers on No Agenda Social.
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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
|
||||
<em><small>(coming in <a href="https://github.com/bit-badger/jobs-jobs-jobs/issues/5" target="_blank">Phase 5</a>)</small></em>
|
||||
</h4>
|
||||
<p>
|
||||
The “public” page for profile information will display the following information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>A count of profiles where the citizen is seeking employment</li>
|
||||
<li>The citizen’s continent and region</li>
|
||||
<li>The citizen’s skills and notes</li>
|
||||
</ul>
|
||||
<p>
|
||||
This information will be pullled only from profiles where citizens have said can it be publicly available
|
||||
<strong>and</strong> are currently seeking employment.
|
||||
</p>
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/profile/search"
|
||||
@inject HttpClient http
|
||||
@inject NavigationManager nav
|
||||
@inject AppState state
|
||||
|
||||
<PageTitle Title="Search Profiles" />
|
||||
@@ -12,6 +13,14 @@
|
||||
}
|
||||
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">
|
||||
@@ -40,5 +49,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else if (Searched)
|
||||
{
|
||||
<p>No results found for the specified criteria</p>
|
||||
}
|
||||
}
|
||||
</ErrorList>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Client.Pages.Profile
|
||||
@@ -20,6 +22,11 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
/// </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>
|
||||
@@ -37,19 +44,36 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ServerApi.SetJwt(http, state);
|
||||
var continentResult = await ServerApi.RetrieveMany<Continent>(http, "continent/all");
|
||||
Continents = await state.GetContinents(http);
|
||||
|
||||
if (continentResult.IsOk)
|
||||
// Determine if we have searched before
|
||||
var query = QueryHelpers.ParseQuery(nav.ToAbsoluteUri(nav.Uri).Query);
|
||||
|
||||
if (query.TryGetValue("Searched", out var searched))
|
||||
{
|
||||
Continents = continentResult.Ok;
|
||||
Searched = Convert.ToBoolean(searched);
|
||||
void setPart(string part, Action<string> func)
|
||||
{
|
||||
if (query.TryGetValue(part, out var partValue)) func(partValue);
|
||||
}
|
||||
setPart("ContinentId", x => Criteria.ContinentId = x);
|
||||
setPart("Skill", x => Criteria.Skill = x);
|
||||
setPart("BioExperience", x => Criteria.BioExperience = x);
|
||||
setPart("RemoteWork", x => Criteria.RemoteWork = x);
|
||||
|
||||
await RetrieveProfiles();
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(continentResult.Error);
|
||||
}
|
||||
|
||||
// TODO: remove this call once the filter is ready
|
||||
/// <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();
|
||||
}
|
||||
|
||||
@@ -60,8 +84,8 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
{
|
||||
Searching = true;
|
||||
|
||||
// TODO: send a filter with this request
|
||||
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http, "profile/search");
|
||||
var searchResult = await ServerApi.RetrieveMany<ProfileSearchResult>(http,
|
||||
QueryHelpers.AddQueryString("profile/search", SearchQuery()));
|
||||
|
||||
if (searchResult.IsOk)
|
||||
{
|
||||
@@ -85,5 +109,27 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
/// <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("ContinentId", it => it.ContinentId);
|
||||
part("Skill", it => it.Skill);
|
||||
part("BioExperience", it => it.BioExperience);
|
||||
part("RemoteWork", it => it.RemoteWork);
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,8 @@
|
||||
@inject HttpClient http
|
||||
@inject AppState state
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<p>Loading profile...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading OnLoad=@RetrieveProfile>
|
||||
<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>
|
||||
@@ -48,5 +42,4 @@ else
|
||||
<hr>
|
||||
<p><a href="/citizen/profile"><span class="oi oi-pencil"></span> Edit Your Profile</a></p>
|
||||
}
|
||||
</ErrorList>
|
||||
}
|
||||
</Loading>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Domain = JobsJobsJobs.Shared;
|
||||
|
||||
@@ -8,11 +7,6 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
{
|
||||
public partial class View : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether data for this component is loading
|
||||
/// </summary>
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The citizen whose profile is being displayed
|
||||
/// </summary>
|
||||
@@ -48,18 +42,17 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error messages from data retrieval
|
||||
/// </summary>
|
||||
private IList<string> ErrorMessages { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the citizen whose profile should be displayed
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
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);
|
||||
var citizenTask = ServerApi.RetrieveOne<Domain.Citizen>(http, $"citizen/get/{Id}");
|
||||
@@ -73,11 +66,11 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
}
|
||||
else if (citizenTask.Result.IsOk)
|
||||
{
|
||||
ErrorMessages.Add("Citizen not found");
|
||||
errors.Add("Citizen not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(citizenTask.Result.Error);
|
||||
errors.Add(citizenTask.Result.Error);
|
||||
}
|
||||
|
||||
if (profileTask.Result.IsOk && profileTask.Result.Ok != null)
|
||||
@@ -86,14 +79,12 @@ namespace JobsJobsJobs.Client.Pages.Profile
|
||||
}
|
||||
else if (profileTask.Result.IsOk)
|
||||
{
|
||||
ErrorMessages.Add("Profile not found");
|
||||
errors.Add("Profile not found");
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessages.Add(profileTask.Result.Error);
|
||||
errors.Add(profileTask.Result.Error);
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.DisplayName’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 Microsoft.AspNetCore.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Domain = JobsJobsJobs.Shared;
|
||||
|
||||
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 Domain.Success Story { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The citizen who authorized this success story
|
||||
/// </summary>
|
||||
private Domain.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<Domain.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<Domain.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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ namespace JobsJobsJobs.Client
|
||||
/// </summary>
|
||||
static ServerApi()
|
||||
{
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
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';
|
||||
}
|
||||
@@ -1,23 +1,4 @@
|
||||
@using NodaTime
|
||||
@using NodaTime.Text
|
||||
@using System.Globalization
|
||||
@inject IJSRuntime js
|
||||
@inject AppState state
|
||||
|
||||
@Translated
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The pattern with which dates will be formatted
|
||||
/// </summary>
|
||||
private static InstantPattern pattern = InstantPattern.Create("ld<MMMM d, yyyy>", CultureInfo.CurrentCulture);
|
||||
|
||||
/// <summary>
|
||||
/// The date to be formatted
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Instant TheDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The formatted date
|
||||
/// </summary>
|
||||
private string Translated => pattern.Format(TheDate);
|
||||
}
|
||||
|
||||
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
|
||||
@using NodaTime.Text
|
||||
@using System.Globalization
|
||||
@inject IJSRuntime js
|
||||
@inject AppState state
|
||||
|
||||
@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
|
||||
@using System.Reflection
|
||||
@inject IJSRuntime js
|
||||
@using Blazored.Toast.Configuration
|
||||
@inject IJSRuntime js
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
@@ -10,7 +9,7 @@
|
||||
|
||||
<div class="main">
|
||||
<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 class="content px-4">
|
||||
@@ -20,7 +19,7 @@
|
||||
<source src="/audio/pelosi-jobs.mp3">
|
||||
</audio>
|
||||
|
||||
<div class="app-version">Jobs, Jobs, Jobs @Version</div>
|
||||
<div class="app-version">Jobs, Jobs, Jobs @AppState.Version.Value</div>
|
||||
</div>
|
||||
</div>
|
||||
<BlazoredToasts Position="ToastPosition.BottomRight"
|
||||
@@ -28,14 +27,4 @@
|
||||
|
||||
@code {
|
||||
async void PlayJobs() => await js.InvokeVoidAsync("Audio.play", "pelosijobs");
|
||||
|
||||
private string Version { get; set; } = "";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||
Version = $"v{version.Major}.{version.Minor}";
|
||||
if (version.Build > 0) Version += $".{version.Build}";
|
||||
base.OnInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
|
||||
<div class="top-row pl-4 navbar navbar-dark">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||
<div class="@NavMenuCssClass" @onclick=@ToggleNavMenu>
|
||||
<ul class="nav flex-column">
|
||||
@if (state.User == null)
|
||||
{
|
||||
<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
|
||||
</NavLink>
|
||||
</li>
|
||||
@if (state.User == null)
|
||||
{
|
||||
<li class="nav-item px-3">
|
||||
<a class="nav-link" href="@AuthUrl">
|
||||
<span class="oi oi-account-login" aria-hidden="true"></span> Log On
|
||||
@@ -41,12 +41,22 @@
|
||||
<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
|
||||
</NavLink>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
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!;
|
||||
}
|
||||
@@ -65,3 +65,7 @@ label.jjj-required::after {
|
||||
color: red;
|
||||
content: ' *';
|
||||
}
|
||||
|
||||
label.jjj-label {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
function setPageTitle(theTitle) {
|
||||
document.title = theTitle
|
||||
}
|
||||
function getTimeZone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyVersion>0.7.1.0</AssemblyVersion>
|
||||
<FileVersion>0.7.1.0</FileVersion>
|
||||
<AssemblyVersion>0.9.0.0</AssemblyVersion>
|
||||
<FileVersion>0.9.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
/// 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 ProfileController(JobsDbContext db, IClock clock)
|
||||
{
|
||||
_db = db;
|
||||
@@ -112,9 +113,22 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> Search()
|
||||
public async Task<IActionResult> Search([FromQuery] ProfileSearch search) =>
|
||||
Ok(await _db.SearchProfiles(search));
|
||||
|
||||
[HttpPatch("employment-found")]
|
||||
public async Task<IActionResult> EmploymentFound()
|
||||
{
|
||||
return Ok(await _db.SearchProfiles());
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
using JobsJobsJobs.Shared;
|
||||
using JobsJobsJobs.Shared.Api;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JobsJobsJobs.Server.Data
|
||||
@@ -115,12 +113,51 @@ namespace JobsJobsJobs.Server.Data
|
||||
/// </summary>
|
||||
// TODO: A criteria parameter!
|
||||
/// <returns>The information for profiles matching the criteria</returns>
|
||||
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db)
|
||||
public static async Task<IEnumerable<ProfileSearchResult>> SearchProfiles(this JobsDbContext db,
|
||||
ProfileSearch search)
|
||||
{
|
||||
return await db.Profiles
|
||||
.Join(db.Citizens, p => p.Id, c => c.Id, (p, c) => new { Profile = p, Citizen = c })
|
||||
.Select(x => new ProfileSearchResult(x.Citizen.Id, x.Citizen.DisplayName, x.Profile.SeekingEmployment,
|
||||
x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
|
||||
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.DisplayName,
|
||||
x.Profile.SeekingEmployment, x.Profile.RemoteWork, x.Profile.FullTime, x.Profile.LastUpdatedOn))
|
||||
.ToListAsync().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.DisplayName,
|
||||
it.Success.RecordedOn, it.Success.FromHere, it.Success.Story != null))
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@
|
||||
function setPageTitle(theTitle) {
|
||||
document.title = theTitle
|
||||
}
|
||||
function getTimeZone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
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/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; } = "";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace JobsJobsJobs.Shared
|
||||
using System;
|
||||
|
||||
namespace JobsJobsJobs.Shared
|
||||
{
|
||||
/// <summary>
|
||||
/// A result with two different possibilities
|
||||
@@ -60,5 +62,24 @@
|
||||
/// <param name="error">The error message</param>
|
||||
/// <returns>The Error result</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user