WIP on job listing search (#17)
This commit is contained in:
parent
227713fa26
commit
69e386d91b
|
@ -193,6 +193,7 @@ let regexContains (it : string) =
|
||||||
System.Text.RegularExpressions.Regex.Escape it
|
System.Text.RegularExpressions.Regex.Escape it
|
||||||
|> sprintf "(?i).*%s.*"
|
|> sprintf "(?i).*%s.*"
|
||||||
|
|
||||||
|
open JobsJobsJobs.Domain
|
||||||
open JobsJobsJobs.Domain.SharedTypes
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
open RethinkDb.Driver.Ast
|
open RethinkDb.Driver.Ast
|
||||||
|
|
||||||
|
@ -200,8 +201,6 @@ open RethinkDb.Driver.Ast
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Profile =
|
module Profile =
|
||||||
|
|
||||||
open JobsJobsJobs.Domain
|
|
||||||
|
|
||||||
let count conn =
|
let count conn =
|
||||||
withReconn(conn).ExecuteAsync(fun () ->
|
withReconn(conn).ExecuteAsync(fun () ->
|
||||||
r.Table(Table.Profile)
|
r.Table(Table.Profile)
|
||||||
|
@ -430,25 +429,15 @@ module Continent =
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Listing =
|
module Listing =
|
||||||
|
|
||||||
/// This is how RethinkDB is going to return our listing/continent combo
|
|
||||||
// fsharplint:disable RecordFieldNames
|
|
||||||
[<CLIMutable; NoEquality; NoComparison>]
|
|
||||||
type ListingAndContinent = {
|
|
||||||
left : Listing
|
|
||||||
right : Continent
|
|
||||||
}
|
|
||||||
// fsharplint:enable
|
|
||||||
|
|
||||||
/// Find all job listings posted by the given citizen
|
/// Find all job listings posted by the given citizen
|
||||||
let findByCitizen (citizenId : CitizenId) conn =
|
let findByCitizen (citizenId : CitizenId) conn =
|
||||||
withReconn(conn).ExecuteAsync(fun () -> task {
|
withReconn(conn).ExecuteAsync(fun () ->
|
||||||
let! both =
|
|
||||||
r.Table(Table.Listing)
|
r.Table(Table.Listing)
|
||||||
.GetAll(citizenId).OptArg("index", nameof citizenId)
|
.GetAll(citizenId).OptArg("index", nameof citizenId)
|
||||||
.EqJoin("continentId", r.Table(Table.Continent))
|
.EqJoin("continentId", r.Table(Table.Continent))
|
||||||
.RunResultAsync<ListingAndContinent list> conn
|
.Merge(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
||||||
return both |> List.map (fun b -> { listing = b.left; continent = b.right})
|
.Pluck("listing", "continent")
|
||||||
})
|
.RunResultAsync<ListingForView list> conn)
|
||||||
|
|
||||||
/// Find a listing by its ID
|
/// Find a listing by its ID
|
||||||
let findById (listingId : ListingId) conn =
|
let findById (listingId : ListingId) conn =
|
||||||
|
@ -481,6 +470,38 @@ module Listing =
|
||||||
()
|
()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/// Search job listings
|
||||||
|
let search (srch : ListingSearch) conn =
|
||||||
|
withReconn(conn).ExecuteAsync(fun () ->
|
||||||
|
(seq {
|
||||||
|
match srch.continentId with
|
||||||
|
| Some conId ->
|
||||||
|
yield (fun (q : ReqlExpr) ->
|
||||||
|
q.Filter(r.HashMap(nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||||
|
| None -> ()
|
||||||
|
match srch.region with
|
||||||
|
| Some rgn ->
|
||||||
|
yield (fun q ->
|
||||||
|
q.Filter(ReqlFunction1(fun s -> upcast s.G("region").Match(regexContains rgn))) :> ReqlExpr)
|
||||||
|
| None -> ()
|
||||||
|
match srch.remoteWork with
|
||||||
|
| "" -> ()
|
||||||
|
| _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||||
|
match srch.text with
|
||||||
|
| Some text ->
|
||||||
|
yield (fun q ->
|
||||||
|
q.Filter(ReqlFunction1(fun it -> upcast it.G("text").Match(regexContains text))) :> ReqlExpr)
|
||||||
|
| None -> ()
|
||||||
|
}
|
||||||
|
|> Seq.toList
|
||||||
|
|> List.fold
|
||||||
|
(fun q f -> f q)
|
||||||
|
(r.Table(Table.Listing)
|
||||||
|
.EqJoin("continentId", r.Table(Table.Continent)) :> ReqlExpr))
|
||||||
|
.Merge(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
||||||
|
.Pluck("listing", "continent")
|
||||||
|
.RunResultAsync<ListingForView list> conn)
|
||||||
|
|
||||||
|
|
||||||
/// Success story data access functions
|
/// Success story data access functions
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
|
|
|
@ -239,6 +239,16 @@ module Listing =
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /api/listing/search
|
||||||
|
let search : HttpHandler =
|
||||||
|
authorize
|
||||||
|
>=> fun next ctx -> task {
|
||||||
|
let search = ctx.BindQueryString<ListingSearch> ()
|
||||||
|
let! results = Data.Listing.search search (conn ctx)
|
||||||
|
return! json results next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /api/profile routes
|
/// Handlers for /api/profile routes
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Profile =
|
module Profile =
|
||||||
|
@ -442,6 +452,7 @@ let allEndpoints = [
|
||||||
subRoute "/listing" [
|
subRoute "/listing" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/%O" Listing.get
|
routef "/%O" Listing.get
|
||||||
|
route "/search" Listing.search
|
||||||
route "s/mine" Listing.mine
|
route "s/mine" Listing.mine
|
||||||
]
|
]
|
||||||
POST [
|
POST [
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Listing,
|
Listing,
|
||||||
ListingForm,
|
ListingForm,
|
||||||
ListingForView,
|
ListingForView,
|
||||||
|
ListingSearch,
|
||||||
LogOnSuccess,
|
LogOnSuccess,
|
||||||
Profile,
|
Profile,
|
||||||
ProfileForm,
|
ProfileForm,
|
||||||
|
@ -171,6 +172,23 @@ export default {
|
||||||
retreive: async (id : string, user : LogOnSuccess) : Promise<Listing | undefined | string> =>
|
retreive: async (id : string, user : LogOnSuccess) : Promise<Listing | undefined | string> =>
|
||||||
apiResult<Listing>(await fetch(apiUrl(`listing/${id}`), reqInit('GET', user)), 'retrieving job listing'),
|
apiResult<Listing>(await fetch(apiUrl(`listing/${id}`), reqInit('GET', user)), 'retrieving job listing'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for job listings using the given parameters
|
||||||
|
*
|
||||||
|
* @param query The listing search parameters
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns The matching job listings (if found), undefined (if API returns 404), or an error string
|
||||||
|
*/
|
||||||
|
search: async (query : ListingSearch, user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (query.continentId) params.append('continentId', query.continentId)
|
||||||
|
if (query.region) params.append('skill', query.region)
|
||||||
|
params.append('remoteWork', query.remoteWork)
|
||||||
|
if (query.text) params.append('text', query.text)
|
||||||
|
return apiResult<ListingForView[]>(await fetch(apiUrl(`listing/search?${params.toString()}`),
|
||||||
|
reqInit('GET', user)), 'searching job listings')
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing job listing
|
* Update an existing job listing
|
||||||
*
|
*
|
||||||
|
|
|
@ -85,6 +85,18 @@ export interface ListingForView {
|
||||||
continent : Continent
|
continent : Continent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The various ways job listings can be searched */
|
||||||
|
export interface ListingSearch {
|
||||||
|
/** Retrieve opportunities from this continent */
|
||||||
|
continentId : string | undefined
|
||||||
|
/** Text for a search for a specific region */
|
||||||
|
region : string | undefined
|
||||||
|
/** Whether to retrieve job listings for remote work */
|
||||||
|
remoteWork : string
|
||||||
|
/** Text to search with a job's full description */
|
||||||
|
text : string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/** A successful logon */
|
/** A successful logon */
|
||||||
export interface LogOnSuccess {
|
export interface LogOnSuccess {
|
||||||
/** The JSON Web Token (JWT) to use for API access */
|
/** The JSON Web Token (JWT) to use for API access */
|
||||||
|
|
80
src/JobsJobsJobs/App/src/components/ListingSearchForm.vue
Normal file
80
src/JobsJobsJobs/App/src/components/ListingSearchForm.vue
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<template>
|
||||||
|
<form class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-xs-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<continent-list v-model="criteria.continentId" topLabel="Any"
|
||||||
|
@update:modelValue="(c) => updateValue('continentId', c)" />
|
||||||
|
</div>
|
||||||
|
<div class="col col-xs-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" class="jjj-label">Region</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">(free-form text)</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-xs-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" name="remoteWork" class="form-check-input"
|
||||||
|
:checked="criteria.remoteWork === ''" @click="updateValue('remoteWork', '')">
|
||||||
|
<label for="remoteNull" class="form-check-label">No Selection</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type="radio" id="remoteYes" name="remoteWork" class="form-check-input"
|
||||||
|
:checked="criteria.remoteWork === 'yes'" @click="updateValue('remoteWork', 'yes')">
|
||||||
|
<label for="remoteYes" class="form-check-label">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input type="radio" id="remoteNo" name="remoteWork" class="form-check-input"
|
||||||
|
:checked="criteria.remoteWork === 'no'" @click="updateValue('remoteWork', 'no')">
|
||||||
|
<label for="remoteNo" class="form-check-label">No</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-xs-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" class="jjj-label">Job Listing Text</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">(free-form text)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-xs-12">
|
||||||
|
<br>
|
||||||
|
<button type="submit" class="btn btn-outline-primary" @click.prevent="$emit('search')">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ListingSearch } from '@/api'
|
||||||
|
import { defineComponent, ref, Ref } from 'vue'
|
||||||
|
import ContinentList from './ContinentList.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ListingSearchForm',
|
||||||
|
components: { ContinentList },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['search', 'update:modelValue'],
|
||||||
|
setup (props, { emit }) {
|
||||||
|
/** The initial search criteria passed; this is what we'll update and emit when data changes */
|
||||||
|
const criteria : Ref<ListingSearch> = ref({ ...props.modelValue as ListingSearch })
|
||||||
|
|
||||||
|
return {
|
||||||
|
criteria,
|
||||||
|
updateValue: (key : string, value : string) => {
|
||||||
|
criteria.value = { ...criteria.value, [key]: value }
|
||||||
|
emit('update:modelValue', criteria.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -9,10 +9,10 @@
|
||||||
<router-link to="/profile/search" class="separator">
|
<router-link to="/profile/search" class="separator">
|
||||||
<icon icon="view-list-outline" /> View Profiles
|
<icon icon="view-list-outline" /> View Profiles
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/listings/mine"><icon icon="sign-text" /> My Job Listings</router-link>
|
<router-link to="/help-wanted">
|
||||||
<router-link to="/listings/search" class="separator">
|
<icon icon="newspaper-variant-multiple-outline" /> Help Wanted!
|
||||||
<icon icon="newspaper-variant-multiple-outline" /> View Listings
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link to="/listings/mine" class="separator"><icon icon="sign-text" /> My Job Listings</router-link>
|
||||||
<router-link to="/success-story/list"><icon icon="thumb-up" /> Success Stories</router-link>
|
<router-link to="/success-story/list"><icon icon="thumb-up" /> Success Stories</router-link>
|
||||||
<router-link to="/citizen/log-off"><icon icon="logout-variant" /> Log Off</router-link>
|
<router-link to="/citizen/log-off"><icon icon="logout-variant" /> Log Off</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -73,21 +73,26 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue')
|
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue')
|
||||||
},
|
},
|
||||||
// Job Listing URLs
|
// Job Listing URLs
|
||||||
|
{
|
||||||
|
path: '/help-wanted',
|
||||||
|
name: 'HelpWanted',
|
||||||
|
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/HelpWanted.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/listing/:id/edit',
|
path: '/listing/:id/edit',
|
||||||
name: 'EditListing',
|
name: 'EditListing',
|
||||||
component: () => import(/* webpackChunkName: "jobedit" */ '../views/listing/ListingEdit.vue')
|
component: () => import(/* webpackChunkName: "jobedit" */ '../views/listing/ListingEdit.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/listing/:id/view',
|
||||||
|
name: 'ViewListing',
|
||||||
|
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/ListingView.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/listings/mine',
|
path: '/listings/mine',
|
||||||
name: 'MyListings',
|
name: 'MyListings',
|
||||||
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/MyListings.vue')
|
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/MyListings.vue')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/listings/search',
|
|
||||||
name: 'SearchListings',
|
|
||||||
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/ListingSearch.vue')
|
|
||||||
},
|
|
||||||
// Profile URLs
|
// Profile URLs
|
||||||
{
|
{
|
||||||
path: '/profile/:id/view',
|
path: '/profile/:id/view',
|
||||||
|
|
145
src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue
Normal file
145
src/JobsJobsJobs/App/src/views/listing/HelpWanted.vue
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<page-title title="Help Wanted" />
|
||||||
|
<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 scope="col" class="text-center">Remote?</th>
|
||||||
|
<th scope="col" class="text-center">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 v-if="it.listing.neededBy" class="text-center">{{format(Date.parse(it.listing.neededBy), 'PPP')}}</td>
|
||||||
|
<td v-else class="text-center">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 lang="ts">
|
||||||
|
import { defineComponent, ref, Ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
CollapsePanel,
|
||||||
|
ErrorList,
|
||||||
|
ListingSearchForm
|
||||||
|
},
|
||||||
|
setup () {
|
||||||
|
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 = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => route.query, setUpPage, { immediate: true })
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors,
|
||||||
|
criteria,
|
||||||
|
isCollapsed,
|
||||||
|
toggleCollapse: (it : boolean) => { isCollapsed.value = it },
|
||||||
|
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
|
||||||
|
searching,
|
||||||
|
searched,
|
||||||
|
results,
|
||||||
|
yesOrNo,
|
||||||
|
format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<p>TODO: write this</p>
|
|
||||||
</template>
|
|
3
src/JobsJobsJobs/App/src/views/listing/ListingView.vue
Normal file
3
src/JobsJobsJobs/App/src/views/listing/ListingView.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: write</p>
|
||||||
|
</template>
|
|
@ -34,6 +34,20 @@ type ListingForView = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The various ways job listings can be searched
|
||||||
|
[<CLIMutable>]
|
||||||
|
type ListingSearch = {
|
||||||
|
/// Retrieve job listings for this continent
|
||||||
|
continentId : string option
|
||||||
|
/// Text for a search within a region
|
||||||
|
region : string option
|
||||||
|
/// Whether to retrieve job listings for remote work
|
||||||
|
remoteWork : string
|
||||||
|
/// Text for a search with the job listing description
|
||||||
|
text : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A successful logon
|
/// A successful logon
|
||||||
type LogOnSuccess = {
|
type LogOnSuccess = {
|
||||||
/// The JSON Web Token (JWT) to use for API access
|
/// The JSON Web Token (JWT) to use for API access
|
||||||
|
@ -122,17 +136,6 @@ type ProfileSearch = {
|
||||||
remoteWork : string
|
remoteWork : string
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Support functions for profile searches
|
|
||||||
module ProfileSearch =
|
|
||||||
/// Is the search empty?
|
|
||||||
let isEmptySearch search =
|
|
||||||
[ search.continentId
|
|
||||||
search.skill
|
|
||||||
search.bioExperience
|
|
||||||
match search.remoteWork with "" -> Some search.remoteWork | _ -> None
|
|
||||||
]
|
|
||||||
|> List.exists Option.isSome
|
|
||||||
|
|
||||||
|
|
||||||
/// A user matching the profile search
|
/// A user matching the profile search
|
||||||
type ProfileSearchResult = {
|
type ProfileSearchResult = {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user