From df7299ccb16ab7603aceda951d4e9af010466d4f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 15 Jan 2023 22:17:33 -0500 Subject: [PATCH] Migrate job listing search --- .../App/src/components/ListingSearchForm.vue | 77 ----------- .../App/src/views/listing/HelpWanted.vue | 128 ------------------ .../App/src/views/listing/index.ts | 11 -- src/JobsJobsJobs/Data/Data.fs | 17 +-- src/JobsJobsJobs/Domain/SharedTypes.fs | 10 +- src/JobsJobsJobs/Server/Handlers.fs | 46 ++++--- src/JobsJobsJobs/Server/Views/Listing.fs | 88 +++++++++++- 7 files changed, 121 insertions(+), 256 deletions(-) delete mode 100644 src/JobsJobsJobs/App/src/components/ListingSearchForm.vue delete mode 100644 src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue delete mode 100644 src/JobsJobsJobs/App/src/views/listing/index.ts diff --git a/src/JobsJobsJobs/App/src/components/ListingSearchForm.vue b/src/JobsJobsJobs/App/src/components/ListingSearchForm.vue deleted file mode 100644 index dc5c62f..0000000 --- a/src/JobsJobsJobs/App/src/components/ListingSearchForm.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - diff --git a/src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue b/src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue deleted file mode 100644 index 40ffc18..0000000 --- a/src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - diff --git a/src/JobsJobsJobs/App/src/views/listing/index.ts b/src/JobsJobsJobs/App/src/views/listing/index.ts deleted file mode 100644 index 5d35d8c..0000000 --- a/src/JobsJobsJobs/App/src/views/listing/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { format } from "date-fns" - -/** - * Format the needed by date for display - * - * @param neededBy The defined needed by date - * @returns The date to display - */ -export function formatNeededBy (neededBy : string) : string { - return format(Date.parse(`${neededBy}T00:00:00`), "PPP") -} diff --git a/src/JobsJobsJobs/Data/Data.fs b/src/JobsJobsJobs/Data/Data.fs index 881d291..3d4dbd2 100644 --- a/src/JobsJobsJobs/Data/Data.fs +++ b/src/JobsJobsJobs/Data/Data.fs @@ -373,19 +373,16 @@ module Listings = connection () |> saveDocument Table.Listing (ListingId.toString listing.Id) <| mkDoc listing /// Search job listings - let search (search : ListingSearch) = + let search (search : ListingSearchForm) = let searches = [ - match search.ContinentId with - | Some contId -> "l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ] - | None -> () - match search.Region with - | Some region -> "l.data ->> 'region' ILIKE @region", [ "@region", like region ] - | None -> () + if search.ContinentId <> "" then + "l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string search.ContinentId ] + if search.Region <> "" then + "l.data ->> 'region' ILIKE @region", [ "@region", like search.Region ] if search.RemoteWork <> "" then "l.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ] - match search.Text with - | Some text -> "l.data ->> 'text' ILIKE @text", [ "@text", like text ] - | None -> () + if search.Text <> "" then + "l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ] ] connection () |> Sql.query $" diff --git a/src/JobsJobsJobs/Domain/SharedTypes.fs b/src/JobsJobsJobs/Domain/SharedTypes.fs index a8a4626..17b6fb7 100644 --- a/src/JobsJobsJobs/Domain/SharedTypes.fs +++ b/src/JobsJobsJobs/Domain/SharedTypes.fs @@ -82,19 +82,19 @@ type ListingForView = /// The various ways job listings can be searched -[] -type ListingSearch = +[] +type ListingSearchForm = { /// Retrieve job listings for this continent - ContinentId : string option + ContinentId : string /// Text for a search within a region - Region : string option + Region : string /// Whether to retrieve job listings for remote work RemoteWork : string /// Text for a search with the job listing description - Text : string option + Text : string } diff --git a/src/JobsJobsJobs/Server/Handlers.fs b/src/JobsJobsJobs/Server/Handlers.fs index 0e09e0b..73bd375 100644 --- a/src/JobsJobsJobs/Server/Handlers.fs +++ b/src/JobsJobsJobs/Server/Handlers.fs @@ -457,7 +457,7 @@ module Home = renderHandler "Terms of Service" Home.termsOfService -/// Handlers for /listing[s] routes +/// Handlers for /listing[s] routes (and /help-wanted) [] module Listing = @@ -572,6 +572,22 @@ module Listing = } + // GET: /help-wanted + let search : HttpHandler = requireUser >=> fun next ctx -> task { + let! continents = Continents.all () + let form = + match ctx.TryBindQueryString () with + | Ok f -> f + | Error _ -> { ContinentId = ""; Region = ""; RemoteWork = ""; Text = "" } + let! results = task { + if string ctx.Request.Query["searched"] = "true" then + let! it = Listings.search form + return Some it + else return None + } + return! Listing.search form continents results |> render "Help Wanted" next ctx + } + // GET: /listing/[id]/view let view listingId : HttpHandler = requireUser >=> fun next ctx -> task { match! Listings.findByIdForView (ListingId listingId) with @@ -580,18 +596,6 @@ module Listing = } -/// Handlers for /api/listing[s] routes -[] -module ListingApi = - - // GET: /api/listing/search - let search : HttpHandler = authorize >=> fun next ctx -> task { - let search = ctx.BindQueryString () - let! results = Listings.search search - return! json results next ctx - } - - /// Handlers for /profile routes [] module Profile = @@ -780,7 +784,13 @@ open Giraffe.EndpointRouting /// All available endpoints for the application let allEndpoints = [ - GET_HEAD [ route "/" Home.home ] + GET_HEAD [ + route "/" Home.home + route "/help-wanted" Listing.search + route "/how-it-works" Home.howItWorks + route "/privacy-policy" Home.privacyPolicy + route "/terms-of-service" Home.termsOfService + ] subRoute "/citizen" [ GET_HEAD [ routef "/confirm/%s" Citizen.confirm @@ -797,7 +807,6 @@ let allEndpoints = [ route "/register" Citizen.doRegistration ] ] - GET_HEAD [ route "/how-it-works" Home.howItWorks ] subRoute "/listing" [ GET_HEAD [ route "s/mine" Listing.mine @@ -810,7 +819,6 @@ let allEndpoints = [ route "/save" Listing.save ] ] - GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ] subRoute "/profile" [ GET_HEAD [ routef "/%O/view" Profile.view @@ -823,7 +831,6 @@ let allEndpoints = [ route "/save" Profile.save ] ] - GET_HEAD [ route "/terms-of-service" Home.termsOfService ] subRoute "/api" [ subRoute "/citizen" [ @@ -832,11 +839,6 @@ let allEndpoints = [ ] ] GET_HEAD [ route "/continents" Continent.all ] - subRoute "/listing" [ - GET_HEAD [ - route "/search" ListingApi.search - ] - ] POST [ route "/markdown-preview" Api.markdownPreview ] subRoute "/profile" [ PATCH [ route "/employment-found" ProfileApi.employmentFound ] diff --git a/src/JobsJobsJobs/Server/Views/Listing.fs b/src/JobsJobsJobs/Server/Views/Listing.fs index e94ce8c..a9ef157 100644 --- a/src/JobsJobsJobs/Server/Views/Listing.fs +++ b/src/JobsJobsJobs/Server/Views/Listing.fs @@ -133,6 +133,89 @@ let mine (listings : ListingForView list) tz = open NodaTime.Text +/// Format the needed by date +let private neededBy dt = + (LocalDatePattern.CreateWithCurrentCulture "MMMM d, yyyy").Format dt + +let search (m : ListingSearchForm) continents (listings : ListingForView list option) = + article [] [ + h3 [ _class "pb-3" ] [ rawText "Help Wanted" ] + if Option.isNone listings then + p [] [ + rawText "Enter relevant criteria to find results, or just click “Search” to see all " + rawText "current job listings." + ] + collapsePanel "Search Criteria" [ + form [ _class "container"; _method "GET"; _action "/help-wanted" ] [ + input [ _type "hidden"; _name "searched"; _value "true" ] + div [ _class "row" ] [ + div [ _class "col-12 col-sm-6 col-md-4 col-lg-3" ] [ + continentList [] "ContinentId" continents (Some "Any") m.ContinentId false + ] + div [ _class "col-12 col-sm-6 col-md-4 col-lg-3" ] [ + textBox [ _maxlength "1000" ] (nameof m.Region) m.Region "Region" false + div [ _class "form-text" ] [ rawText "(free-form text)" ] + ] + div [ _class "col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0" ] [ + label [ _class "jjj-label" ] [ rawText "Seeking Remote Work?" ]; br [] + div [ _class "form-check form-check-inline" ] [ + input [ _type "radio"; _id "remoteNull"; _name (nameof m.RemoteWork); _value "" + _class "form-check-input"; if m.RemoteWork = "" then _checked ] + label [ _class "form-check-label"; _for "remoteNull" ] [ rawText "No Selection" ] + ] + div [ _class "form-check form-check-inline" ] [ + input [ _type "radio"; _id "remoteYes"; _name (nameof m.RemoteWork); _value "yes" + _class "form-check-input"; if m.RemoteWork = "yes" then _checked ] + label [ _class "form-check-label"; _for "remoteYes" ] [ rawText "Yes" ] + ] + div [ _class "form-check form-check-inline" ] [ + input [ _type "radio"; _id "remoteNo"; _name (nameof m.RemoteWork); _value "no" + _class "form-check-input"; if m.RemoteWork = "no" then _checked ] + label [ _class "form-check-label"; _for "remoteNo" ] [ rawText "No" ] + ] + ] + div [ _class "col-12 col-sm-6 col-lg-3" ] [ + textBox [ _maxlength "1000" ] (nameof m.Text) m.Text "Job Listing Text" false + div [ _class "form-text" ] [ rawText "(free-form text)" ] + ] + ] + div [ _class "row" ] [ + div [ _class "col" ] [ + br [] + button [ _type "submit"; _class "btn btn-outline-primary" ] [ rawText "Search" ] + ] + ] + ] + ] + match listings with + | Some r when List.isEmpty r -> + p [ _class "pt-3" ] [ rawText "No job listings found for the specified criteria" ] + | Some r -> + table [ _class "table table-sm table-hover pt-3" ] [ + thead [] [ + tr [] [ + th [ _scope "col" ] [ rawText "Listing" ] + th [ _scope "col" ] [ rawText "Title" ] + th [ _scope "col" ] [ rawText "Location" ] + th [ _scope "col"; _class "text-center" ] [ rawText "Remote?" ] + th [ _scope "col"; _class "text-center" ] [ rawText "Needed By" ] + ] + ] + r |> List.map (fun it -> + tr [] [ + td [] [ a [ _href $"/listing/{ListingId.toString it.Listing.Id}/view" ] [ rawText "View" ] ] + td [] [ str it.Listing.Title ] + td [] [ str it.ContinentName; rawText " / "; str it.Listing.Region ] + td [ _class "text-center" ] [ str (yesOrNo it.Listing.IsRemote) ] + td [ _class "text-center" ] [ + match it.Listing.NeededBy with Some needed -> str (neededBy needed) | None -> rawText "N/A" + ] + ]) + |> tbody [] + ] + | None -> () + ] + /// The job listing view page let view (it : ListingForView) = article [] [ @@ -150,9 +233,8 @@ let view (it : ListingForView) = p [] [ match it.Listing.NeededBy with | Some needed -> - let format dt = - (LocalDatePattern.CreateWithCurrentCulture "MMMM d, yyyy").Format(dt).ToUpperInvariant () - strong [] [ em [] [ rawText "NEEDED BY "; str (format needed) ] ]; rawText " • " + strong [] [ em [] [ rawText "NEEDED BY "; str ((neededBy needed).ToUpperInvariant ()) ] ] + rawText " • " | None -> () rawText "Listed by "; str it.ListedBy //{{citizenName(citizen)}} ]