Version 3 #40
|
@ -24,6 +24,11 @@ label[for]:hover {
|
|||
.material-icons {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
@media print {
|
||||
.jjj-hide-from-printer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Material Design Icon / Bootstrap styling */
|
||||
.mdi::before {
|
||||
font-size: 24px;
|
||||
|
|
|
@ -64,7 +64,9 @@ module private Auth =
|
|||
let account : HttpHandler = fun next ctx -> task {
|
||||
match! Data.findById (currentCitizenId ctx) with
|
||||
| Some citizen ->
|
||||
return! Views.account (AccountProfileForm.fromCitizen citizen) (csrf ctx) |> render "Account Profile" next ctx
|
||||
return!
|
||||
Views.account (AccountProfileForm.fromCitizen citizen) (isHtmx ctx) (csrf ctx)
|
||||
|> render "Account Profile" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
@ -176,7 +178,7 @@ let register next ctx =
|
|||
q2Index <- System.Random.Shared.Next(0, 5)
|
||||
let qAndA = Auth.questions ctx
|
||||
Views.register (fst qAndA[q1Index]) (fst qAndA[q2Index])
|
||||
{ RegisterForm.empty with Question1Index = q1Index; Question2Index = q2Index } (csrf ctx)
|
||||
{ RegisterForm.empty with Question1Index = q1Index; Question2Index = q2Index } (isHtmx ctx) (csrf ctx)
|
||||
|> render "Register" next ctx
|
||||
|
||||
// POST: /citizen/register
|
||||
|
@ -199,7 +201,7 @@ let doRegistration : HttpHandler = validateCsrf >=> fun next ctx -> task {
|
|||
]
|
||||
let refreshPage () =
|
||||
Views.register (fst qAndA[form.Question1Index]) (fst qAndA[form.Question2Index]) { form with Password = "" }
|
||||
(csrf ctx)
|
||||
(isHtmx ctx) (csrf ctx)
|
||||
|> renderHandler "Register"
|
||||
|
||||
if badForm then
|
||||
|
@ -246,7 +248,8 @@ let resetPassword token : HttpHandler = fun next ctx -> task {
|
|||
match! Data.trySecurityByToken token with
|
||||
| Some security ->
|
||||
return!
|
||||
Views.resetPassword { Id = CitizenId.toString security.Id; Token = token; Password = "" } (csrf ctx)
|
||||
Views.resetPassword { Id = CitizenId.toString security.Id; Token = token; Password = "" } (isHtmx ctx)
|
||||
(csrf ctx)
|
||||
|> render "Reset Password" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
@ -273,7 +276,7 @@ let doResetPassword : HttpHandler = validateCsrf >=> fun next ctx -> task {
|
|||
| None -> return! Error.notFound next ctx
|
||||
else
|
||||
do! addErrors errors ctx
|
||||
return! Views.resetPassword form (csrf ctx) |> render "Reset Password" next ctx
|
||||
return! Views.resetPassword form (isHtmx ctx) (csrf ctx) |> render "Reset Password" next ctx
|
||||
}
|
||||
|
||||
// POST: /citizen/save-account
|
||||
|
@ -314,7 +317,7 @@ let saveAccount : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx ->
|
|||
| None -> return! Error.notFound next ctx
|
||||
else
|
||||
do! addErrors errors ctx
|
||||
return! Views.account form (csrf ctx) |> render "Account Profile" next ctx
|
||||
return! Views.account form (isHtmx ctx) (csrf ctx) |> render "Account Profile" next ctx
|
||||
}
|
||||
|
||||
// GET: /citizen/so-long
|
||||
|
|
|
@ -61,7 +61,7 @@ let contactEdit (contacts : OtherContactForm array) =
|
|||
:: (contacts |> Array.mapi mapToInputs |> List.ofArray)
|
||||
|
||||
/// The account edit page
|
||||
let account (m : AccountProfileForm) csrf =
|
||||
let account (m : AccountProfileForm) isHtmx csrf =
|
||||
pageWithTitle "Account Profile" [
|
||||
p [] [
|
||||
txt "This information is visible to all fellow logged-on citizens. For publicly-visible employment "
|
||||
|
@ -107,7 +107,7 @@ let account (m : AccountProfileForm) csrf =
|
|||
]
|
||||
jsOnLoad $"
|
||||
jjj.citizen.nextIndex = {m.Contacts.Length}
|
||||
jjj.citizen.validatePasswords('{nameof m.NewPassword}', '{nameof m.NewPasswordConfirm}', false)"
|
||||
jjj.citizen.validatePasswords('{nameof m.NewPassword}', '{nameof m.NewPasswordConfirm}', false)" isHtmx
|
||||
]
|
||||
|
||||
|
||||
|
@ -305,7 +305,7 @@ let logOn (m : LogOnForm) csrf =
|
|||
]
|
||||
|
||||
/// The registration page
|
||||
let register q1 q2 (m : RegisterForm) csrf =
|
||||
let register q1 q2 (m : RegisterForm) isHtmx csrf =
|
||||
pageWithTitle "Register" [
|
||||
form [ _class "row g-3"; _hxPost "/citizen/register" ] [
|
||||
antiForgery csrf
|
||||
|
@ -343,7 +343,7 @@ let register q1 q2 (m : RegisterForm) csrf =
|
|||
input [ _type "hidden"; _name (nameof m.Question2Index); _value (string m.Question2Index ) ]
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "content-save-outline" "Save" ]
|
||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)"
|
||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)" isHtmx
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -376,7 +376,7 @@ let resetCanceled wasCanceled =
|
|||
|
||||
|
||||
/// The password reset page
|
||||
let resetPassword (m : ResetPasswordForm) csrf =
|
||||
let resetPassword (m : ResetPasswordForm) isHtmx csrf =
|
||||
pageWithTitle "Reset Password" [
|
||||
p [] [ txt "Enter your new password in the fields below" ]
|
||||
form [ _class "row g-3"; _method "POST"; _action "/citizen/reset-password" ] [
|
||||
|
@ -390,6 +390,6 @@ let resetPassword (m : ResetPasswordForm) csrf =
|
|||
textBox [ _type "password"; _minlength "8" ] "ConfirmPassword" "" "Confirm New Password" true
|
||||
]
|
||||
div [ _class "col-12" ] [ submitButton "lock-reset" "Reset Password" ]
|
||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)"
|
||||
jsOnLoad $"jjj.citizen.validatePasswords('{nameof m.Password}', 'ConfirmPassword', true)" isHtmx
|
||||
]
|
||||
]
|
||||
|
|
|
@ -7,7 +7,7 @@ open Microsoft.AspNetCore.Http
|
|||
open Microsoft.Extensions.Logging
|
||||
|
||||
[<AutoOpen>]
|
||||
module private HtmxHelpers =
|
||||
module HtmxHelpers =
|
||||
|
||||
/// Is the request from htmx?
|
||||
let isHtmx (ctx : HttpContext) =
|
||||
|
@ -145,27 +145,32 @@ let addErrors (errors : string list) ctx = task {
|
|||
|
||||
open JobsJobsJobs.Common.Views
|
||||
|
||||
/// Render a page-level view
|
||||
let render pageTitle (_ : HttpFunc) (ctx : HttpContext) content = task {
|
||||
let! messages = popMessages ctx
|
||||
let renderCtx : Layout.PageRenderContext = {
|
||||
IsLoggedOn = Option.isSome (tryUser ctx)
|
||||
/// Create the render context for an HTML response
|
||||
let private createContext (ctx : HttpContext) pageTitle content messages : Layout.PageRenderContext =
|
||||
{ IsLoggedOn = Option.isSome (tryUser ctx)
|
||||
CurrentUrl = ctx.Request.Path.Value
|
||||
PageTitle = pageTitle
|
||||
Content = content
|
||||
Messages = messages
|
||||
}
|
||||
let renderFunc = if isHtmx ctx then Layout.partial else Layout.full
|
||||
|
||||
/// Render a page-level view
|
||||
let render pageTitle (_ : HttpFunc) (ctx : HttpContext) content = task {
|
||||
let! messages = popMessages ctx
|
||||
let renderCtx = createContext ctx pageTitle content messages
|
||||
let renderFunc = if isHtmx ctx then Layout.partial else Layout.full
|
||||
return! ctx.WriteHtmlViewAsync (renderFunc renderCtx)
|
||||
}
|
||||
|
||||
/// Render a printable view (content with styles, but no layout)
|
||||
let renderPrint pageTitle (_ : HttpFunc) (ctx : HttpContext) content =
|
||||
createContext ctx pageTitle content []
|
||||
|> Layout.print
|
||||
|> ctx.WriteHtmlViewAsync
|
||||
|
||||
/// Render a bare (component) view
|
||||
let renderBare (_ : HttpFunc) (ctx : HttpContext) content =
|
||||
({ IsLoggedOn = Option.isSome (tryUser ctx)
|
||||
CurrentUrl = ctx.Request.Path.Value
|
||||
PageTitle = ""
|
||||
Content = content
|
||||
Messages = []
|
||||
} : Layout.PageRenderContext)
|
||||
createContext ctx "" content []
|
||||
|> Layout.bare
|
||||
|> ctx.WriteHtmlViewAsync
|
||||
|
||||
|
|
|
@ -68,11 +68,14 @@ let emptyP =
|
|||
p [] [ txt " " ]
|
||||
|
||||
/// Register JavaScript code to run in the DOMContentLoaded event on the page
|
||||
let jsOnLoad js =
|
||||
script [] [ txt """document.addEventListener("DOMContentLoaded", function () { """; txt js; txt " })" ]
|
||||
let jsOnLoad js isHtmx =
|
||||
script [] [
|
||||
let (target, event) = if isHtmx then "document.body", "htmx:afterSettle" else "document", "DOMContentLoaded"
|
||||
txt (sprintf """%s.addEventListener("%s", () => { %s }, { once: true })""" target event js)
|
||||
]
|
||||
|
||||
/// Create a Markdown editor
|
||||
let markdownEditor attrs name value editorLabel =
|
||||
let markdownEditor attrs name value editorLabel isHtmx =
|
||||
div [ _class "col-12"; _id $"{name}EditRow" ] [
|
||||
nav [ _class "nav nav-pills pb-1" ] [
|
||||
button [ _type "button"; _id $"{name}EditButton"; _class "btn btn-primary btn-sm rounded-pill" ] [
|
||||
|
@ -93,7 +96,7 @@ let markdownEditor attrs name value editorLabel =
|
|||
]
|
||||
label [ _for name ] [ txt editorLabel ]
|
||||
]
|
||||
jsOnLoad $"jjj.markdownOnLoad('{name}')"
|
||||
jsOnLoad $"jjj.markdownOnLoad('{name}')" isHtmx
|
||||
]
|
||||
|
||||
/// Wrap content in a collapsing panel
|
||||
|
@ -124,22 +127,44 @@ let contactInfo citizen isPublic =
|
|||
|> List.collect (fun contact ->
|
||||
match contact.ContactType with
|
||||
| Website ->
|
||||
[ i [ _class "mdi mdi-sm mdi-web" ] []; rawText " "
|
||||
[ i [ _class "mdi mdi-sm mdi-web" ] []; txt " "
|
||||
a [ _href contact.Value; _target "_blank"; _rel "noopener"; _class "me-4" ] [
|
||||
str (defaultArg contact.Name "Website")
|
||||
]
|
||||
]
|
||||
| Email ->
|
||||
[ i [ _class "mdi mdi-sm mdi-email-outline" ] []; rawText " "
|
||||
[ i [ _class "mdi mdi-sm mdi-email-outline" ] []; txt " "
|
||||
a [ _href $"mailto:{contact.Value}"; _class "me-4" ] [ str (defaultArg contact.Name "E-mail") ]
|
||||
]
|
||||
| Phone ->
|
||||
[ span [ _class "me-4" ] [
|
||||
i [ _class "mdi mdi-sm mdi-phone" ] []; rawText " "; str contact.Value
|
||||
i [ _class "mdi mdi-sm mdi-phone" ] []; txt " "; str contact.Value
|
||||
match contact.Name with Some name -> str $" ({name})" | None -> ()
|
||||
]
|
||||
])
|
||||
|
||||
/// Display a citizen's contact information
|
||||
let contactInfoPrint citizen isPublic =
|
||||
citizen.OtherContacts
|
||||
|> List.filter (fun it -> (isPublic && it.IsPublic) || not isPublic)
|
||||
|> List.collect (fun contact ->
|
||||
match contact.ContactType with
|
||||
| Website ->
|
||||
[ i [ _class "mdi mdi-sm mdi-web" ] []; txt " "; str (defaultArg contact.Name "Website"); txt " – "
|
||||
str contact.Value; br []
|
||||
]
|
||||
| Email ->
|
||||
[ i [ _class "mdi mdi-sm mdi-email-outline" ] []; txt " "; str (defaultArg contact.Name "E-mail")
|
||||
txt " – "; str contact.Value; br []
|
||||
]
|
||||
| Phone ->
|
||||
[ span [ _class "me-4" ] [
|
||||
i [ _class "mdi mdi-sm mdi-phone" ] []; rawText " "
|
||||
match contact.Name with Some name -> str name; txt " – " | None -> ()
|
||||
str contact.Value; br []
|
||||
]
|
||||
])
|
||||
|
||||
open NodaTime
|
||||
open NodaTime.Text
|
||||
|
||||
|
@ -353,6 +378,13 @@ module Layout =
|
|||
]
|
||||
]
|
||||
|
||||
/// Render a print view (styles, but no other layout)
|
||||
let print ctx =
|
||||
html [ _lang "en" ] [
|
||||
htmlHead ctx
|
||||
body [ _class "m-1" ] [ ctx.Content ]
|
||||
]
|
||||
|
||||
/// Render a bare view (used for components)
|
||||
let bare ctx =
|
||||
html [ _lang "en" ] [
|
||||
|
|
|
@ -23,7 +23,7 @@ let edit listId : HttpHandler = requireUser >=> fun next ctx -> task {
|
|||
| Some listing when listing.CitizenId = citizenId ->
|
||||
let! continents = Common.Data.Continents.all ()
|
||||
return!
|
||||
Views.edit (EditListingForm.fromListing listing listId) continents (listId = "new") (csrf ctx)
|
||||
Views.edit (EditListingForm.fromListing listing listId) continents (listId = "new") (isHtmx ctx) (csrf ctx)
|
||||
|> render $"""{if listId = "new" then "Add a" else "Edit"} Job Listing""" next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
|
@ -38,7 +38,7 @@ let expire listingId : HttpHandler = requireUser >=> fun next ctx -> task {
|
|||
return! redirectToGet "/listings/mine" next ctx
|
||||
else
|
||||
let form = { Id = ListingId.toString listing.Id; FromHere = false; SuccessStory = "" }
|
||||
return! Views.expire form listing (csrf ctx) |> render "Expire Job Listing" next ctx
|
||||
return! Views.expire form listing (isHtmx ctx) (csrf ctx) |> render "Expire Job Listing" next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ open JobsJobsJobs.Listings.Domain
|
|||
|
||||
|
||||
/// Job listing edit page
|
||||
let edit (m : EditListingForm) continents isNew csrf =
|
||||
let edit (m : EditListingForm) continents isNew isHtmx csrf =
|
||||
pageWithTitle $"""{if isNew then "Add a" else "Edit"} Job Listing""" [
|
||||
form [ _class "row g-3"; _method "POST"; _action "/listing/save" ] [
|
||||
antiForgery csrf
|
||||
|
@ -29,7 +29,7 @@ let edit (m : EditListingForm) continents isNew csrf =
|
|||
div [ _class "col-12" ] [
|
||||
checkBox [] (nameof m.RemoteWork) m.RemoteWork "This opportunity is for remote work"
|
||||
]
|
||||
markdownEditor [ _required ] (nameof m.Text) m.Text "Job Description"
|
||||
markdownEditor [ _required ] (nameof m.Text) m.Text "Job Description" isHtmx
|
||||
div [ _class "col-12 col-md-4" ] [
|
||||
textBox [ _type "date" ] (nameof m.NeededBy) m.NeededBy "Needed By" false
|
||||
]
|
||||
|
@ -41,7 +41,7 @@ let edit (m : EditListingForm) continents isNew csrf =
|
|||
open System.Net
|
||||
|
||||
/// Page to expire a job listing
|
||||
let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
||||
let expire (m : ExpireListingForm) (listing : Listing) isHtmx csrf =
|
||||
pageWithTitle $"Expire Job Listing ({WebUtility.HtmlEncode listing.Title})" [
|
||||
p [ _class "fst-italic" ] [
|
||||
txt "Expiring this listing will remove it from search results. You will be able to see it via your "
|
||||
|
@ -60,10 +60,10 @@ let expire (m : ExpireListingForm) (listing : Listing) csrf =
|
|||
txt "visible to logged-on users here, but not to the general public."
|
||||
]
|
||||
]
|
||||
markdownEditor [] (nameof m.SuccessStory) m.SuccessStory "Your Success Story"
|
||||
markdownEditor [] (nameof m.SuccessStory) m.SuccessStory "Your Success Story" isHtmx
|
||||
div [ _class "col-12" ] [ submitButton "text-box-remove-outline" "Expire Listing" ]
|
||||
]
|
||||
jsOnLoad "jjj.listing.toggleFromHere()"
|
||||
jsOnLoad "jjj.listing.toggleFromHere()" isHtmx
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@ let editGeneralInfo : HttpHandler = requireUser >=> fun next ctx -> task {
|
|||
let! continents = Common.Data.Continents.all ()
|
||||
let form = if Option.isNone profile then EditProfileForm.empty else EditProfileForm.fromProfile profile.Value
|
||||
return!
|
||||
Views.editGeneralInfo form continents (csrf ctx) |> render "General Information | Employment Profile" next ctx
|
||||
Views.editGeneralInfo form continents (isHtmx ctx) (csrf ctx)
|
||||
|> render "General Information | Employment Profile" next ctx
|
||||
}
|
||||
|
||||
// POST: /profile/save
|
||||
|
@ -65,7 +66,7 @@ let saveGeneralInfo : HttpHandler = requireUser >=> fun next ctx -> task {
|
|||
do! addErrors errors ctx
|
||||
let! continents = Common.Data.Continents.all ()
|
||||
return!
|
||||
Views.editGeneralInfo form continents (csrf ctx)
|
||||
Views.editGeneralInfo form continents (isHtmx ctx) (csrf ctx)
|
||||
|> render "General Information | Employment Profile" next ctx
|
||||
}
|
||||
|
||||
|
@ -157,14 +158,16 @@ let deleteSkill idx : HttpHandler = requireUser >=> validateCsrf >=> fun next ct
|
|||
let history : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
match! Data.findById (currentCitizenId ctx) with
|
||||
| Some profile ->
|
||||
return! Views.history profile.History (csrf ctx) |> render "Employment History | Employment Profile" next ctx
|
||||
return!
|
||||
Views.history profile.History (isHtmx ctx) (csrf ctx)
|
||||
|> render "Employment History | Employment Profile" next ctx
|
||||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
// GET: /profile/edit/history/list
|
||||
let historyList : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
match! Data.findById (currentCitizenId ctx) with
|
||||
| Some profile -> return! Views.historyTable profile.History None (csrf ctx) |> renderBare next ctx
|
||||
| Some profile -> return! Views.historyTable profile.History None (isHtmx ctx) (csrf ctx) |> renderBare next ctx
|
||||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
|
@ -173,7 +176,7 @@ let editHistory idx : HttpHandler = requireUser >=> fun next ctx -> task {
|
|||
match! Data.findById (currentCitizenId ctx) with
|
||||
| Some profile ->
|
||||
if idx < -1 || idx >= List.length profile.History then return! notFound ctx
|
||||
else return! Views.editHistory profile.History idx (csrf ctx) |> renderBare next ctx
|
||||
else return! Views.editHistory profile.History idx (isHtmx ctx) (csrf ctx) |> renderBare next ctx
|
||||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
|
@ -190,7 +193,7 @@ let saveHistory idx : HttpHandler = requireUser >=> validateCsrf >=> fun next ct
|
|||
else profile.History |> List.mapi (fun histIdx it -> if histIdx = idx then entry else it)
|
||||
|> List.sortByDescending (fun it -> defaultArg it.EndDate NodaTime.LocalDate.MaxIsoValue)
|
||||
do! Data.save { profile with History = history }
|
||||
return! Views.historyTable history None (csrf ctx) |> renderBare next ctx
|
||||
return! Views.historyTable history None (isHtmx ctx) (csrf ctx) |> renderBare next ctx
|
||||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
|
@ -202,7 +205,7 @@ let deleteHistory idx : HttpHandler = requireUser >=> validateCsrf >=> fun next
|
|||
else
|
||||
let history = profile.History |> List.indexed |> List.filter (fun it -> fst it <> idx) |> List.map snd
|
||||
do! Data.save { profile with History = history }
|
||||
return! Views.historyTable history None (csrf ctx) |> renderBare next ctx
|
||||
return! Views.historyTable history None (isHtmx ctx) (csrf ctx) |> renderBare next ctx
|
||||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
|
@ -220,6 +223,20 @@ let view citizenId : HttpHandler = fun next ctx -> task {
|
|||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
// GET: /profile/[id]/print
|
||||
let print citizenId : HttpHandler = fun next ctx -> task {
|
||||
let citId = CitizenId citizenId
|
||||
match! Data.findByIdForView citId with
|
||||
| Some profile ->
|
||||
let currentCitizen = tryUser ctx |> Option.map CitizenId.ofString
|
||||
if not (profile.Profile.Visibility = Public) && Option.isNone currentCitizen then
|
||||
return! Error.notAuthorized next ctx
|
||||
else
|
||||
let pageTitle = $"Employment Profile for {Citizen.name profile.Citizen}"
|
||||
return! Views.print profile (Option.isNone currentCitizen) |> renderPrint pageTitle next ctx
|
||||
| None -> return! notFound ctx
|
||||
}
|
||||
|
||||
|
||||
open Giraffe.EndpointRouting
|
||||
|
||||
|
@ -228,6 +245,7 @@ let endpoints =
|
|||
subRoute "/profile" [
|
||||
GET_HEAD [
|
||||
routef "/%O/view" view
|
||||
routef "/%O/print" print
|
||||
route "/edit" edit
|
||||
route "/edit/general" editGeneralInfo
|
||||
routef "/edit/history/%i" editHistory
|
||||
|
|
|
@ -62,7 +62,7 @@ let backToEdit =
|
|||
|
||||
|
||||
/// The profile edit page
|
||||
let editGeneralInfo (m : EditProfileForm) continents csrf =
|
||||
let editGeneralInfo (m : EditProfileForm) continents isHtmx csrf =
|
||||
pageWithTitle "Employment Profile: General Information" [
|
||||
backToEdit
|
||||
form [ _class "row g-3"; _action "/profile/save"; _hxPost "/profile/save" ] [
|
||||
|
@ -88,7 +88,7 @@ let editGeneralInfo (m : EditProfileForm) continents csrf =
|
|||
div [ _class "col-12 col-md-4" ] [
|
||||
checkBox [] (nameof m.FullTime) m.FullTime "I am interested in full-time work"
|
||||
]
|
||||
markdownEditor [ _required ] (nameof m.Biography) m.Biography "Professional Biography"
|
||||
markdownEditor [ _required ] (nameof m.Biography) m.Biography "Professional Biography" isHtmx
|
||||
div [ _class "col-12" ] [
|
||||
hr []
|
||||
h4 [] [ txt "Experience" ]
|
||||
|
@ -98,7 +98,7 @@ let editGeneralInfo (m : EditProfileForm) continents csrf =
|
|||
txt "entries, provide closing notes, etc."
|
||||
]
|
||||
]
|
||||
markdownEditor [] (nameof m.Experience) (defaultArg m.Experience "") "Experience"
|
||||
markdownEditor [] (nameof m.Experience) (defaultArg m.Experience "") "Experience" isHtmx
|
||||
div [ _class "col-12" ] [
|
||||
hr []
|
||||
h4 [] [ txt "Visibility" ]
|
||||
|
@ -230,7 +230,7 @@ let editSkill (skills : Skill list) idx csrf =
|
|||
open NodaTime
|
||||
|
||||
/// Render the employment history edit template
|
||||
let historyForm (m : HistoryForm) isNew =
|
||||
let historyForm (m : HistoryForm) isNew isHtmx =
|
||||
let maxDate = HistoryForm.dateFormat.Format (LocalDate.FromDateTime System.DateTime.Today)
|
||||
[ h4 [] [ txt $"""{if isNew then "Add" else "Edit"} Employment History""" ]
|
||||
div [ _class "col-12 col-md-6" ] [
|
||||
|
@ -239,7 +239,7 @@ let historyForm (m : HistoryForm) isNew =
|
|||
div [ _class "col-12 col-md-6" ] [
|
||||
textBox [ _type "text"; _maxlength "200" ] (nameof m.Position) m.Position "Title or Position" true
|
||||
]
|
||||
p [ _class "col-12 text-center" ] [
|
||||
p [ _class "col-12 text-center mb-0" ] [
|
||||
txt "Select any date within the month; only the month and year will be displayed"
|
||||
]
|
||||
div [ _class "col-12 col-md-6 col-xl-4 offset-xl-1" ] [
|
||||
|
@ -248,7 +248,7 @@ let historyForm (m : HistoryForm) isNew =
|
|||
div [ _class "col-12 col-md-6 col-xl-4 offset-xl-2" ] [
|
||||
textBox [ _type "date"; _max maxDate ] (nameof m.EndDate) m.EndDate "End Date" false
|
||||
]
|
||||
markdownEditor [] (nameof m.Description) m.Description "Description"
|
||||
markdownEditor [] (nameof m.Description) m.Description "Description" isHtmx
|
||||
div [ _class "col-12" ] [
|
||||
submitButton "content-save-outline" "Save"; txt " "
|
||||
a [ _href "/profile/edit/history/list"; _hxGet "/profile/edit/history/list"; _hxTarget "#historyList"
|
||||
|
@ -256,19 +256,19 @@ let historyForm (m : HistoryForm) isNew =
|
|||
]
|
||||
]
|
||||
|
||||
let private monthAndYear = Text.LocalDatePattern.CreateWithInvariantCulture "MMMM yyyy"
|
||||
|
||||
/// List the employment history entries for an employment profile
|
||||
let historyTable (history : EmploymentHistory list) editIdx csrf =
|
||||
let editingIdx = defaultArg editIdx -2
|
||||
let isEditing = editingIdx >= -1
|
||||
let monthAndYear = Text.LocalDatePattern.CreateWithInvariantCulture "MMMM yyyy"
|
||||
let historyTable (history : EmploymentHistory list) editIdx isHtmx csrf =
|
||||
let editingIdx = defaultArg editIdx -2
|
||||
let isEditing = editingIdx >= -1
|
||||
let renderTable () =
|
||||
let editHistoryForm entry idx =
|
||||
tr [] [
|
||||
td [ _colspan "4" ] [
|
||||
form [ _class "row g-3"; _hxPost $"/profile/edit/history/{idx}"; _hxTarget "#historyList" ] [
|
||||
antiForgery csrf
|
||||
yield! historyForm (HistoryForm.fromHistory entry) (idx = -1)
|
||||
yield! historyForm (HistoryForm.fromHistory entry) (idx = -1) isHtmx
|
||||
]
|
||||
]
|
||||
]
|
||||
|
@ -314,7 +314,7 @@ let historyTable (history : EmploymentHistory list) editIdx csrf =
|
|||
form [ _id "historyList"; _hxTarget "this"; _hxPost "/profile/edit/history/-1"; _hxSwap HxSwap.OuterHtml
|
||||
_class "row g-3" ] [
|
||||
antiForgery csrf
|
||||
yield! historyForm (HistoryForm.fromHistory EmploymentHistory.empty) true
|
||||
yield! historyForm (HistoryForm.fromHistory EmploymentHistory.empty) true isHtmx
|
||||
]
|
||||
else if isEditing then div [ _id "historyList" ] [ renderTable () ]
|
||||
else // not editing, there is history to show
|
||||
|
@ -325,7 +325,7 @@ let historyTable (history : EmploymentHistory list) editIdx csrf =
|
|||
|
||||
|
||||
/// The employment history maintenance page
|
||||
let history (history : EmploymentHistory list) csrf =
|
||||
let history (history : EmploymentHistory list) isHtmx csrf =
|
||||
pageWithTitle "Employment Profile: Employment History" [
|
||||
backToEdit
|
||||
p [] [
|
||||
|
@ -334,7 +334,7 @@ let history (history : EmploymentHistory list) csrf =
|
|||
txt "Add an Employment History Entry"
|
||||
]
|
||||
]
|
||||
historyTable history None csrf
|
||||
historyTable history None isHtmx csrf
|
||||
]
|
||||
|
||||
|
||||
|
@ -507,11 +507,9 @@ let search (m : ProfileSearchForm) continents tz (results : ProfileSearchResult
|
|||
| None -> ()
|
||||
]
|
||||
|
||||
|
||||
/// Profile view template
|
||||
let view (it : ProfileForView) currentId =
|
||||
article [] [
|
||||
h2 [] [
|
||||
/// Display a profile
|
||||
let private displayProfile (it : ProfileForView) isPublic isPrint =
|
||||
[ h2 [] [
|
||||
str (Citizen.name it.Citizen)
|
||||
if it.Profile.IsSeekingEmployment then
|
||||
span [ _class "jjj-heading-label" ] [
|
||||
|
@ -519,17 +517,15 @@ let view (it : ProfileForView) currentId =
|
|||
]
|
||||
]
|
||||
h4 [] [ str $"{it.Continent.Name}, {it.Profile.Region}" ]
|
||||
contactInfo it.Citizen (Option.isNone currentId)
|
||||
(if isPrint then contactInfoPrint else contactInfo) it.Citizen isPublic
|
||||
|> div [ _class "pb-3" ]
|
||||
p [] [
|
||||
txt (if it.Profile.IsFullTime then "I" else "Not i"); txt "nterested in full-time employment • "
|
||||
txt (if it.Profile.IsRemote then "I" else "Not i"); txt "nterested in remote opportunities"
|
||||
]
|
||||
hr []
|
||||
div [] [ md2html it.Profile.Biography ]
|
||||
if not (List.isEmpty it.Profile.Skills) then
|
||||
hr []
|
||||
h4 [ _class "pb-3" ] [ txt "Skills" ]
|
||||
h4 [ _class "pb-3 border-top border-3" ] [ txt "Skills" ]
|
||||
it.Profile.Skills
|
||||
|> List.map (fun skill ->
|
||||
li [] [
|
||||
|
@ -537,12 +533,55 @@ let view (it : ProfileForView) currentId =
|
|||
match skill.Notes with Some notes -> txt " ("; str notes; txt ")" | None -> ()
|
||||
])
|
||||
|> ul []
|
||||
if not (List.isEmpty it.Profile.History) then
|
||||
h4 [ _class "mb-3 border-top border-3" ] [ txt "Employment History" ]
|
||||
yield!
|
||||
it.Profile.History
|
||||
|> List.indexed
|
||||
|> List.collect (fun (idx, entry) -> [
|
||||
let maybeBorder = if idx > 0 then " border-top" else ""
|
||||
div [ _class $"d-flex flex-row flex-wrap justify-content-between align-items-start mt-4 mb-2{maybeBorder}" ] [
|
||||
div [] [
|
||||
strong [] [ str entry.Employer ]
|
||||
match entry.Position with Some pos -> br []; str pos | None -> ()
|
||||
]
|
||||
div [ _class "text-end" ] [
|
||||
span [ _class "text-nowrap" ] [ str (monthAndYear.Format entry.StartDate) ]; txt " to "
|
||||
match entry.EndDate with
|
||||
| Some dt -> span [ _class "text-nowrap" ] [ str (monthAndYear.Format dt) ]
|
||||
| None -> txt "Present"
|
||||
]
|
||||
]
|
||||
match entry.Description with Some d -> div [] [ md2html d ] | None -> ()
|
||||
])
|
||||
match it.Profile.Experience with
|
||||
| Some exp -> hr []; h4 [ _class "pb-3" ] [ txt "Experience / Employment History" ]; div [] [ md2html exp ]
|
||||
| Some exp -> div [ _class "border-top border-3" ] [ md2html exp ]
|
||||
| None -> ()
|
||||
]
|
||||
|
||||
|
||||
/// Profile view template
|
||||
let view (it : ProfileForView) currentId =
|
||||
article [] [
|
||||
yield! displayProfile it (Option.isNone currentId) false
|
||||
if Option.isSome currentId && currentId.Value = it.Citizen.Id then
|
||||
br []; br []
|
||||
a [ _href "/profile/edit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-pencil" ] []; txt " Edit Your Profile"
|
||||
]
|
||||
txt " "
|
||||
a [ _href $"/profile/{CitizenId.toString it.Profile.Id}/print"; _target "_blank"
|
||||
_class "btn btn-outline-secondary" ] [
|
||||
i [ _class "mdi mdi-printer-outline" ] []; txt " View Print Version"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
/// Printable profile view template
|
||||
let print (it : ProfileForView) isPublic =
|
||||
article [] [
|
||||
yield! displayProfile it isPublic true
|
||||
button [ _type "button"; _class "btn btn-sm btn-secondary jjj-hide-from-printer"; _onclick "window.print()" ] [
|
||||
i [ _class "mdi mdi-printer-outline" ] []; txt " Print"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -19,7 +19,8 @@ let edit successId : HttpHandler = requireUser >=> fun next ctx -> task {
|
|||
| Some success when success.CitizenId = citizenId ->
|
||||
let pgTitle = $"""{if isNew then "Tell Your" else "Edit"} Success Story"""
|
||||
return!
|
||||
Views.edit (EditSuccessForm.fromSuccess success) (success.Id = SuccessId Guid.Empty) pgTitle (csrf ctx)
|
||||
Views.edit (EditSuccessForm.fromSuccess success) (success.Id = SuccessId Guid.Empty) pgTitle (isHtmx ctx)
|
||||
(csrf ctx)
|
||||
|> render pgTitle next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
|
|
|
@ -7,7 +7,7 @@ open JobsJobsJobs.Domain
|
|||
open JobsJobsJobs.SuccessStories.Domain
|
||||
|
||||
/// The add/edit success story page
|
||||
let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||
let edit (m : EditSuccessForm) isNew pgTitle isHtmx csrf =
|
||||
pageWithTitle pgTitle [
|
||||
if isNew then
|
||||
p [] [
|
||||
|
@ -21,7 +21,7 @@ let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
|||
div [ _class "col-12" ] [
|
||||
checkBox [] (nameof m.FromHere) m.FromHere "I found my employment here"
|
||||
]
|
||||
markdownEditor [] (nameof m.Story) m.Story "The Success Story"
|
||||
markdownEditor [] (nameof m.Story) m.Story "The Success Story" isHtmx
|
||||
div [ _class "col-12" ] [
|
||||
submitButton "content-save-outline" "Save"
|
||||
if isNew then
|
||||
|
|
Loading…
Reference in New Issue
Block a user