Version 3 #40
|
@ -77,6 +77,7 @@ module Helpers =
|
||||||
|
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
open System.Text.Json
|
open System.Text.Json
|
||||||
|
open System.Text.RegularExpressions
|
||||||
open Microsoft.AspNetCore.Antiforgery
|
open Microsoft.AspNetCore.Antiforgery
|
||||||
open Microsoft.Extensions.Configuration
|
open Microsoft.Extensions.Configuration
|
||||||
open Microsoft.Extensions.DependencyInjection
|
open Microsoft.Extensions.DependencyInjection
|
||||||
|
@ -198,15 +199,14 @@ module Helpers =
|
||||||
/// Require a user to be logged on for a route
|
/// Require a user to be logged on for a route
|
||||||
let requireUser = requiresAuthentication Error.notAuthorized
|
let requireUser = requiresAuthentication Error.notAuthorized
|
||||||
|
|
||||||
|
/// Regular expression to validate that a URL is a local URL
|
||||||
|
let isLocal = Regex """^/[^\/\\].*"""
|
||||||
|
|
||||||
/// Redirect to another page, saving the session before redirecting
|
/// Redirect to another page, saving the session before redirecting
|
||||||
let redirectToGet url next ctx = task {
|
let redirectToGet (url : string) next ctx = task {
|
||||||
do! saveSession ctx
|
do! saveSession ctx
|
||||||
let action =
|
let action =
|
||||||
if Option.isSome (noneIfEmpty url)
|
if Option.isSome (noneIfEmpty url) && isLocal.IsMatch url then
|
||||||
// "/" or "/foo" but not "//" or "/\"
|
|
||||||
&& ( (url[0] = '/' && (url.Length = 1 || (url[1] <> '/' && url[1] <> '\\')))
|
|
||||||
// "~/" or "~/foo"
|
|
||||||
|| (url.Length > 1 && url[0] = '~' && url[1] = '/')) then
|
|
||||||
if isHtmx ctx then withHxRedirect url else redirectTo false url
|
if isHtmx ctx then withHxRedirect url else redirectTo false url
|
||||||
else RequestErrors.BAD_REQUEST "Invalid redirect URL"
|
else RequestErrors.BAD_REQUEST "Invalid redirect URL"
|
||||||
return! action next ctx
|
return! action next ctx
|
||||||
|
|
|
@ -44,7 +44,7 @@ type EditProfileViewModel =
|
||||||
Experience : string option
|
Experience : string option
|
||||||
|
|
||||||
/// The skills for the user
|
/// The skills for the user
|
||||||
Skills : SkillForm list
|
Skills : SkillForm array
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Support functions for the ProfileForm type
|
/// Support functions for the ProfileForm type
|
||||||
|
@ -60,7 +60,7 @@ module EditProfileViewModel =
|
||||||
FullTime = false
|
FullTime = false
|
||||||
Biography = ""
|
Biography = ""
|
||||||
Experience = None
|
Experience = None
|
||||||
Skills = [ { Id = ""; Description = ""; Notes = None } ]
|
Skills = [||]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an instance of this form from the given profile
|
/// Create an instance of this form from the given profile
|
||||||
|
@ -79,6 +79,7 @@ module EditProfileViewModel =
|
||||||
Description = s.Description
|
Description = s.Description
|
||||||
Notes = s.Notes
|
Notes = s.Notes
|
||||||
})
|
})
|
||||||
|
|> Array.ofList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,40 @@ open Giraffe.ViewEngine
|
||||||
open Giraffe.ViewEngine.Htmx
|
open Giraffe.ViewEngine.Htmx
|
||||||
open JobsJobsJobs.ViewModels
|
open JobsJobsJobs.ViewModels
|
||||||
|
|
||||||
|
let skillEdit (skills : SkillForm array) =
|
||||||
|
let mapToInputs (idx : int) (skill : SkillForm) =
|
||||||
|
div [ _class "row pb-3" ] [
|
||||||
|
div [ _class "col-2 col-md-1 align-self-center" ] [
|
||||||
|
button [ _class "btn btn-sm btn-outline-danger rounded-pill"; _title "Delete"
|
||||||
|
_onclick $"jjj.removeSkill('{skill.Id}')" ] [
|
||||||
|
rawText " − "
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-10 col-md-6" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _id $"skillDesc{skill.Id}"; _name $"Skills[{idx}].Description"
|
||||||
|
_class "form-control"; _placeholder "A skill (language, design technique, process, etc.)"
|
||||||
|
_maxlength "200"; _value skill.Description; _required ]
|
||||||
|
label [ _class "jjj-label"; _for $"skillDesc{skill.Id}" ] [ rawText "Skill" ]
|
||||||
|
]
|
||||||
|
div [ _class "form-text" ] [ rawText "A skill (language, design technique, process, etc.)" ]
|
||||||
|
]
|
||||||
|
div [ _class "col-12 col-md-5" ] [
|
||||||
|
div [ _class "form-floating" ] [
|
||||||
|
input [ _type "text"; _id $"skillNotes{skill.Id}"; _name $"Skills[{idx}].Notes";
|
||||||
|
_class "form-control"; _maxlength "1000"
|
||||||
|
_placeholder "A further description of the skill (1,000 characters max)"
|
||||||
|
_value (defaultArg skill.Notes "") ]
|
||||||
|
label [ _class "jjj-label"; _for $"skillNotes{skill.Id}" ] [ rawText "Notes" ]
|
||||||
|
]
|
||||||
|
div [ _class "form-text" ] [ rawText "A further description of the skill" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
template [ _id "newSkill" ] [ mapToInputs -1 { Id = ""; Description = ""; Notes = None } ]
|
||||||
|
:: (skills
|
||||||
|
|> Array.mapi mapToInputs
|
||||||
|
|> List.ofArray)
|
||||||
|
|
||||||
/// The profile edit page
|
/// The profile edit page
|
||||||
let edit (m : EditProfileViewModel) continents isNew csrf =
|
let edit (m : EditProfileViewModel) continents isNew csrf =
|
||||||
article [] [
|
article [] [
|
||||||
|
@ -68,8 +102,7 @@ let edit (m : EditProfileViewModel) continents isNew csrf =
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
//<profile-skill-edit v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
|
yield! skillEdit m.Skills
|
||||||
// @remove="removeSkill(skill.id)" @input="v$.skills.$touch" />
|
|
||||||
div [ _class "col-12" ] [
|
div [ _class "col-12" ] [
|
||||||
hr []
|
hr []
|
||||||
h4 [] [ rawText "Experience" ]
|
h4 [] [ rawText "Experience" ]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user