Migrate job listing search
This commit is contained in:
parent
9de255d25a
commit
df7299ccb1
|
@ -1,77 +0,0 @@
|
||||||
<template>
|
|
||||||
<form class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
|
|
||||||
<continent-list v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent" />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-sm-6 col-lg-3">
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="text" id="regionSearch" class="form-control" placeholder="(free-form text)"
|
|
||||||
:value="criteria.region" @input="updateValue('region', $event.target.value)">
|
|
||||||
<label for="regionSearch">Region</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">(free-form text)</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-sm-6 col-offset-md-2 col-lg-3 col-offset-lg-0">
|
|
||||||
<label class="jjj-label">Remote Work Opportunity?</label>
|
|
||||||
<br>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input type="radio" id="remoteNull" class="form-check-input" name="remoteWork"
|
|
||||||
:checked="criteria.remoteWork === ''" @click="updateValue('remoteWork', '')">
|
|
||||||
<label class="form-check-label" for="remoteNull">No Selection</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input type="radio" id="remoteYes" class="form-check-input" name="remoteWork"
|
|
||||||
:checked="criteria.remoteWork === 'yes'" @click="updateValue('remoteWork', 'yes')">
|
|
||||||
<label class="form-check-label" for="remoteYes">Yes</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input type="radio" id="remoteNo" class="form-check-input" name="remoteWork"
|
|
||||||
:checked="criteria.remoteWork === 'no'" @click="updateValue('remoteWork', 'no')">
|
|
||||||
<label class="form-check-label" for="remoteNo">No</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-sm-6 col-lg-3">
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="text" id="textSearch" class="form-control" placeholder="(free-form text)" :value="criteria.text"
|
|
||||||
@input="updateValue('text', $event.target.value)">
|
|
||||||
<label for="textSearch">Job Listing Text</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-text">(free-form text)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<br>
|
|
||||||
<button class="btn btn-outline-primary" type="submit" @click.prevent="$emit('search')">Search</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ListingSearch } from "@/api"
|
|
||||||
import { ref } from "vue"
|
|
||||||
import ContinentList from "./ContinentList.vue"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: ListingSearch
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "search") : void
|
|
||||||
(e: "update:modelValue", value : ListingSearch) : void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
|
||||||
const criteria = ref({ ...props.modelValue })
|
|
||||||
|
|
||||||
/** Emit a value update */
|
|
||||||
const updateValue = (key : string, value : string) => {
|
|
||||||
criteria.value = { ...criteria.value, [key]: value }
|
|
||||||
emit("update:modelValue", criteria.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Update the continent ID */
|
|
||||||
const updateContinent = (c : string) => updateValue("continentId", c)
|
|
||||||
</script>
|
|
|
@ -1,128 +0,0 @@
|
||||||
<template>
|
|
||||||
<article>
|
|
||||||
<h3 class="pb-3">Help Wanted</h3>
|
|
||||||
<p v-if="!searched">
|
|
||||||
Enter relevant criteria to find results, or just click “Search” to see all current job listings.
|
|
||||||
</p>
|
|
||||||
<collapse-panel headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse">
|
|
||||||
<listing-search-form v-model="criteria" @search="doSearch" />
|
|
||||||
</collapse-panel>
|
|
||||||
<error-list :errors="errors">
|
|
||||||
<p v-if="searching" class="pt-3">Searching job listings…</p>
|
|
||||||
<template v-else>
|
|
||||||
<table v-if="results.length > 0" class="table table-sm table-hover pt-3">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Listing</th>
|
|
||||||
<th scope="col">Title</th>
|
|
||||||
<th scope="col">Location</th>
|
|
||||||
<th class="text-center" scope="col">Remote?</th>
|
|
||||||
<th class="text-center" scope="col">Needed By</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="it in results" :key="it.listing.id">
|
|
||||||
<td><router-link :to="`/listing/${it.listing.id}/view`">View</router-link></td>
|
|
||||||
<td>{{it.listing.title}}</td>
|
|
||||||
<td>{{it.continent.name}} / {{it.listing.region}}</td>
|
|
||||||
<td class="text-center">{{yesOrNo(it.listing.remoteWork)}}</td>
|
|
||||||
<td class="text-center" v-if="it.listing.neededBy">{{formatNeededBy(it.listing.neededBy)}}</td>
|
|
||||||
<td class="text-center" v-else>N/A</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p v-else-if="searched" class="pt-3">No job listings found for the specified criteria</p>
|
|
||||||
</template>
|
|
||||||
</error-list>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, Ref, watch } from "vue"
|
|
||||||
import { useRoute, useRouter } from "vue-router"
|
|
||||||
|
|
||||||
import { formatNeededBy } from "./"
|
|
||||||
import { yesOrNo } from "@/App.vue"
|
|
||||||
import api, { ListingForView, ListingSearch, LogOnSuccess } from "@/api"
|
|
||||||
import { queryValue } from "@/router"
|
|
||||||
import { useStore } from "@/store"
|
|
||||||
|
|
||||||
import CollapsePanel from "@/components/CollapsePanel.vue"
|
|
||||||
import ErrorList from "@/components/ErrorList.vue"
|
|
||||||
import ListingSearchForm from "@/components/ListingSearchForm.vue"
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** Any errors encountered while retrieving data */
|
|
||||||
const errors : Ref<string[]> = ref([])
|
|
||||||
|
|
||||||
/** Whether we are currently searching (retrieving data) */
|
|
||||||
const searching = ref(false)
|
|
||||||
|
|
||||||
/** Whether a search has been performed on this page since it has been loaded */
|
|
||||||
const searched = ref(false)
|
|
||||||
|
|
||||||
/** An empty set of search criteria */
|
|
||||||
const emptyCriteria = {
|
|
||||||
continentId: "",
|
|
||||||
region: undefined,
|
|
||||||
remoteWork: "",
|
|
||||||
text: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The search criteria being built from the page */
|
|
||||||
const criteria : Ref<ListingSearch> = ref(emptyCriteria)
|
|
||||||
|
|
||||||
/** The current search results */
|
|
||||||
const results : Ref<ListingForView[]> = ref([])
|
|
||||||
|
|
||||||
/** Whether the search criteria should be collapsed */
|
|
||||||
const isCollapsed = ref(searched.value && results.value.length > 0)
|
|
||||||
|
|
||||||
/** Set up the page to match its requested state */
|
|
||||||
const setUpPage = async () => {
|
|
||||||
if (queryValue(route, "searched") === "true") {
|
|
||||||
searched.value = true
|
|
||||||
try {
|
|
||||||
searching.value = true
|
|
||||||
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
|
|
||||||
const contId = queryValue(route, "continentId")
|
|
||||||
const searchParams : ListingSearch = {
|
|
||||||
continentId: contId === "" ? undefined : contId,
|
|
||||||
region: queryValue(route, "region"),
|
|
||||||
remoteWork: queryValue(route, "remoteWork") ?? "",
|
|
||||||
text: queryValue(route, "text")
|
|
||||||
}
|
|
||||||
const searchResult = await api.listings.search(searchParams, store.state.user as LogOnSuccess)
|
|
||||||
if (typeof searchResult === "string") {
|
|
||||||
errors.value.push(searchResult)
|
|
||||||
} else if (searchResult === undefined) {
|
|
||||||
errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
|
|
||||||
} else {
|
|
||||||
results.value = searchResult
|
|
||||||
searchParams.continentId = searchParams.continentId ?? ""
|
|
||||||
criteria.value = searchParams
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
searching.value = false
|
|
||||||
}
|
|
||||||
isCollapsed.value = searched.value && results.value.length > 0
|
|
||||||
} else {
|
|
||||||
searched.value = false
|
|
||||||
criteria.value = emptyCriteria
|
|
||||||
errors.value = []
|
|
||||||
results.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Refresh the page when the query string changes */
|
|
||||||
watch(() => route.query, setUpPage, { immediate: true })
|
|
||||||
|
|
||||||
/** Show or hide the search parameter panel */
|
|
||||||
const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
|
|
||||||
|
|
||||||
/** Execute a search */
|
|
||||||
const doSearch = () => router.push({ query: { searched: "true", ...criteria.value } })
|
|
||||||
</script>
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -373,19 +373,16 @@ module Listings =
|
||||||
connection () |> saveDocument Table.Listing (ListingId.toString listing.Id) <| mkDoc listing
|
connection () |> saveDocument Table.Listing (ListingId.toString listing.Id) <| mkDoc listing
|
||||||
|
|
||||||
/// Search job listings
|
/// Search job listings
|
||||||
let search (search : ListingSearch) =
|
let search (search : ListingSearchForm) =
|
||||||
let searches = [
|
let searches = [
|
||||||
match search.ContinentId with
|
if search.ContinentId <> "" then
|
||||||
| Some contId -> "l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string contId ]
|
"l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string search.ContinentId ]
|
||||||
| None -> ()
|
if search.Region <> "" then
|
||||||
match search.Region with
|
"l.data ->> 'region' ILIKE @region", [ "@region", like search.Region ]
|
||||||
| Some region -> "l.data ->> 'region' ILIKE @region", [ "@region", like region ]
|
|
||||||
| None -> ()
|
|
||||||
if search.RemoteWork <> "" then
|
if search.RemoteWork <> "" then
|
||||||
"l.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ]
|
"l.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ]
|
||||||
match search.Text with
|
if search.Text <> "" then
|
||||||
| Some text -> "l.data ->> 'text' ILIKE @text", [ "@text", like text ]
|
"l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ]
|
||||||
| None -> ()
|
|
||||||
]
|
]
|
||||||
connection ()
|
connection ()
|
||||||
|> Sql.query $"
|
|> Sql.query $"
|
||||||
|
|
|
@ -82,19 +82,19 @@ type ListingForView =
|
||||||
|
|
||||||
|
|
||||||
/// The various ways job listings can be searched
|
/// The various ways job listings can be searched
|
||||||
[<CLIMutable>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type ListingSearch =
|
type ListingSearchForm =
|
||||||
{ /// Retrieve job listings for this continent
|
{ /// Retrieve job listings for this continent
|
||||||
ContinentId : string option
|
ContinentId : string
|
||||||
|
|
||||||
/// Text for a search within a region
|
/// Text for a search within a region
|
||||||
Region : string option
|
Region : string
|
||||||
|
|
||||||
/// Whether to retrieve job listings for remote work
|
/// Whether to retrieve job listings for remote work
|
||||||
RemoteWork : string
|
RemoteWork : string
|
||||||
|
|
||||||
/// Text for a search with the job listing description
|
/// Text for a search with the job listing description
|
||||||
Text : string option
|
Text : string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -457,7 +457,7 @@ module Home =
|
||||||
renderHandler "Terms of Service" Home.termsOfService
|
renderHandler "Terms of Service" Home.termsOfService
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /listing[s] routes
|
/// Handlers for /listing[s] routes (and /help-wanted)
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Listing =
|
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<ListingSearchForm> () 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
|
// GET: /listing/[id]/view
|
||||||
let view listingId : HttpHandler = requireUser >=> fun next ctx -> task {
|
let view listingId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
match! Listings.findByIdForView (ListingId listingId) with
|
match! Listings.findByIdForView (ListingId listingId) with
|
||||||
|
@ -580,18 +596,6 @@ module Listing =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /api/listing[s] routes
|
|
||||||
[<RequireQualifiedAccess>]
|
|
||||||
module ListingApi =
|
|
||||||
|
|
||||||
// GET: /api/listing/search
|
|
||||||
let search : HttpHandler = authorize >=> fun next ctx -> task {
|
|
||||||
let search = ctx.BindQueryString<ListingSearch> ()
|
|
||||||
let! results = Listings.search search
|
|
||||||
return! json results next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /profile routes
|
/// Handlers for /profile routes
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Profile =
|
module Profile =
|
||||||
|
@ -780,7 +784,13 @@ open Giraffe.EndpointRouting
|
||||||
|
|
||||||
/// All available endpoints for the application
|
/// All available endpoints for the application
|
||||||
let allEndpoints = [
|
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" [
|
subRoute "/citizen" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/confirm/%s" Citizen.confirm
|
routef "/confirm/%s" Citizen.confirm
|
||||||
|
@ -797,7 +807,6 @@ let allEndpoints = [
|
||||||
route "/register" Citizen.doRegistration
|
route "/register" Citizen.doRegistration
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/how-it-works" Home.howItWorks ]
|
|
||||||
subRoute "/listing" [
|
subRoute "/listing" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
route "s/mine" Listing.mine
|
route "s/mine" Listing.mine
|
||||||
|
@ -810,7 +819,6 @@ let allEndpoints = [
|
||||||
route "/save" Listing.save
|
route "/save" Listing.save
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ]
|
|
||||||
subRoute "/profile" [
|
subRoute "/profile" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/%O/view" Profile.view
|
routef "/%O/view" Profile.view
|
||||||
|
@ -823,7 +831,6 @@ let allEndpoints = [
|
||||||
route "/save" Profile.save
|
route "/save" Profile.save
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/terms-of-service" Home.termsOfService ]
|
|
||||||
|
|
||||||
subRoute "/api" [
|
subRoute "/api" [
|
||||||
subRoute "/citizen" [
|
subRoute "/citizen" [
|
||||||
|
@ -832,11 +839,6 @@ let allEndpoints = [
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
GET_HEAD [ route "/continents" Continent.all ]
|
||||||
subRoute "/listing" [
|
|
||||||
GET_HEAD [
|
|
||||||
route "/search" ListingApi.search
|
|
||||||
]
|
|
||||||
]
|
|
||||||
POST [ route "/markdown-preview" Api.markdownPreview ]
|
POST [ route "/markdown-preview" Api.markdownPreview ]
|
||||||
subRoute "/profile" [
|
subRoute "/profile" [
|
||||||
PATCH [ route "/employment-found" ProfileApi.employmentFound ]
|
PATCH [ route "/employment-found" ProfileApi.employmentFound ]
|
||||||
|
|
|
@ -133,6 +133,89 @@ let mine (listings : ListingForView list) tz =
|
||||||
|
|
||||||
open NodaTime.Text
|
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
|
/// The job listing view page
|
||||||
let view (it : ListingForView) =
|
let view (it : ListingForView) =
|
||||||
article [] [
|
article [] [
|
||||||
|
@ -150,9 +233,8 @@ let view (it : ListingForView) =
|
||||||
p [] [
|
p [] [
|
||||||
match it.Listing.NeededBy with
|
match it.Listing.NeededBy with
|
||||||
| Some needed ->
|
| Some needed ->
|
||||||
let format dt =
|
strong [] [ em [] [ rawText "NEEDED BY "; str ((neededBy needed).ToUpperInvariant ()) ] ]
|
||||||
(LocalDatePattern.CreateWithCurrentCulture "MMMM d, yyyy").Format(dt).ToUpperInvariant ()
|
rawText " • "
|
||||||
strong [] [ em [] [ rawText "NEEDED BY "; str (format needed) ] ]; rawText " • "
|
|
||||||
| None -> ()
|
| None -> ()
|
||||||
rawText "Listed by "; str it.ListedBy //<!-- a :href="profileUrl" target="_blank" -->{{citizenName(citizen)}}<!-- /a -->
|
rawText "Listed by "; str it.ListedBy //<!-- a :href="profileUrl" target="_blank" -->{{citizenName(citizen)}}<!-- /a -->
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user