Version 3 #40

Merged
danieljsummers merged 67 commits from version-2-3 into main 2023-02-02 23:47:28 +00:00
11 changed files with 176 additions and 73 deletions
Showing only changes of commit 77ea839b9c - Show all commits

View File

@ -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;

View File

@ -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

View File

@ -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
]
]

View File

@ -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

View File

@ -68,11 +68,14 @@ let emptyP =
p [] [ txt "&nbsp;" ]
/// 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 " &ndash; "
str contact.Value; br []
]
| Email ->
[ i [ _class "mdi mdi-sm mdi-email-outline" ] []; txt " "; str (defaultArg contact.Name "E-mail")
txt " &ndash; "; 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 " &ndash; " | 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" ] [

View File

@ -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
}

View File

@ -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
]

View File

@ -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

View File

@ -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 " &nbsp; &nbsp; "
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 &bull; "
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 " &nbsp;("; 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 "&nbsp; Edit Your Profile"
]
txt " &nbsp; &nbsp; "
a [ _href $"/profile/{CitizenId.toString it.Profile.Id}/print"; _target "_blank"
_class "btn btn-outline-secondary" ] [
i [ _class "mdi mdi-printer-outline" ] []; txt "&nbsp; 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 "&nbsp; Print"
]
]

View File

@ -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

View File

@ -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