From 7096368b6ea1ed27b9f3637cc758555ebc06b99a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 12 Jan 2023 22:14:38 -0500 Subject: [PATCH] WIP on profile/skill edit --- src/JobsJobsJobs/Server/Handlers.fs | 12 ++++---- src/JobsJobsJobs/Server/ViewModels.fs | 5 ++-- src/JobsJobsJobs/Server/Views/Profile.fs | 37 ++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/JobsJobsJobs/Server/Handlers.fs b/src/JobsJobsJobs/Server/Handlers.fs index 7739a9c..cd12ace 100644 --- a/src/JobsJobsJobs/Server/Handlers.fs +++ b/src/JobsJobsJobs/Server/Handlers.fs @@ -77,6 +77,7 @@ module Helpers = open System.Security.Claims open System.Text.Json + open System.Text.RegularExpressions open Microsoft.AspNetCore.Antiforgery open Microsoft.Extensions.Configuration open Microsoft.Extensions.DependencyInjection @@ -198,15 +199,14 @@ module Helpers = /// Require a user to be logged on for a route 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 - let redirectToGet url next ctx = task { + let redirectToGet (url : string) next ctx = task { do! saveSession ctx let action = - if Option.isSome (noneIfEmpty url) - // "/" 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 Option.isSome (noneIfEmpty url) && isLocal.IsMatch url then if isHtmx ctx then withHxRedirect url else redirectTo false url else RequestErrors.BAD_REQUEST "Invalid redirect URL" return! action next ctx diff --git a/src/JobsJobsJobs/Server/ViewModels.fs b/src/JobsJobsJobs/Server/ViewModels.fs index dcf4fe1..f710a5e 100644 --- a/src/JobsJobsJobs/Server/ViewModels.fs +++ b/src/JobsJobsJobs/Server/ViewModels.fs @@ -44,7 +44,7 @@ type EditProfileViewModel = Experience : string option /// The skills for the user - Skills : SkillForm list + Skills : SkillForm array } /// Support functions for the ProfileForm type @@ -60,7 +60,7 @@ module EditProfileViewModel = FullTime = false Biography = "" Experience = None - Skills = [ { Id = ""; Description = ""; Notes = None } ] + Skills = [||] } /// Create an instance of this form from the given profile @@ -79,6 +79,7 @@ module EditProfileViewModel = Description = s.Description Notes = s.Notes }) + |> Array.ofList } diff --git a/src/JobsJobsJobs/Server/Views/Profile.fs b/src/JobsJobsJobs/Server/Views/Profile.fs index 29e998e..43ae76f 100644 --- a/src/JobsJobsJobs/Server/Views/Profile.fs +++ b/src/JobsJobsJobs/Server/Views/Profile.fs @@ -7,6 +7,40 @@ open Giraffe.ViewEngine open Giraffe.ViewEngine.Htmx 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 let edit (m : EditProfileViewModel) continents isNew csrf = article [] [ @@ -68,8 +102,7 @@ let edit (m : EditProfileViewModel) continents isNew csrf = ] ] ] - // + yield! skillEdit m.Skills div [ _class "col-12" ] [ hr [] h4 [] [ rawText "Experience" ]