Help wanted #23
|
@ -17,6 +17,7 @@ let configureApp (app : IApplicationBuilder) =
|
||||||
.UseRouting()
|
.UseRouting()
|
||||||
.UseAuthentication()
|
.UseAuthentication()
|
||||||
.UseAuthorization()
|
.UseAuthorization()
|
||||||
|
.UseGiraffeErrorHandler(Handlers.Error.unexpectedError)
|
||||||
.UseEndpoints(fun e ->
|
.UseEndpoints(fun e ->
|
||||||
e.MapGiraffeEndpoints Handlers.allEndpoints
|
e.MapGiraffeEndpoints Handlers.allEndpoints
|
||||||
e.MapFallbackToFile "index.html" |> ignore)
|
e.MapFallbackToFile "index.html" |> ignore)
|
||||||
|
|
|
@ -191,7 +191,7 @@ let withReconn (conn : IConnection) =
|
||||||
/// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries
|
/// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries
|
||||||
let regexContains (it : string) =
|
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
|
||||||
open JobsJobsJobs.Domain.SharedTypes
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
|
@ -435,8 +435,7 @@ module Listing =
|
||||||
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))
|
||||||
.Merge(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
.Map(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
||||||
.Pluck("listing", "continent")
|
|
||||||
.RunResultAsync<ListingForView list> conn)
|
.RunResultAsync<ListingForView list> conn)
|
||||||
|
|
||||||
/// Find a listing by its ID
|
/// Find a listing by its ID
|
||||||
|
@ -449,6 +448,18 @@ module Listing =
|
||||||
return toOption listing
|
return toOption listing
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/// Find a listing by its ID for viewing (includes continent information)
|
||||||
|
let findByIdForView (listingId : ListingId) conn =
|
||||||
|
withReconn(conn).ExecuteAsync(fun () -> task {
|
||||||
|
let! listing =
|
||||||
|
r.Table(Table.Listing)
|
||||||
|
.Filter(r.HashMap("id", listingId))
|
||||||
|
.EqJoin("continentId", r.Table(Table.Continent))
|
||||||
|
.Map(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
||||||
|
.RunResultAsync<ListingForView list> conn
|
||||||
|
return List.tryHead listing
|
||||||
|
})
|
||||||
|
|
||||||
/// Add a listing
|
/// Add a listing
|
||||||
let add (listing : Listing) conn =
|
let add (listing : Listing) conn =
|
||||||
withReconn(conn).ExecuteAsync(fun () -> task {
|
withReconn(conn).ExecuteAsync(fun () -> task {
|
||||||
|
@ -482,24 +493,26 @@ module Listing =
|
||||||
match srch.region with
|
match srch.region with
|
||||||
| Some rgn ->
|
| Some rgn ->
|
||||||
yield (fun q ->
|
yield (fun q ->
|
||||||
q.Filter(ReqlFunction1(fun s -> upcast s.G("region").Match(regexContains rgn))) :> ReqlExpr)
|
q.Filter(ReqlFunction1(fun it ->
|
||||||
|
upcast it.G(nameof srch.region).Match(regexContains rgn))) :> ReqlExpr)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
match srch.remoteWork with
|
match srch.remoteWork with
|
||||||
| "" -> ()
|
| "" -> ()
|
||||||
| _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
| _ ->
|
||||||
|
yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||||
match srch.text with
|
match srch.text with
|
||||||
| Some text ->
|
| Some text ->
|
||||||
yield (fun q ->
|
yield (fun q ->
|
||||||
q.Filter(ReqlFunction1(fun it -> upcast it.G("text").Match(regexContains text))) :> ReqlExpr)
|
q.Filter(ReqlFunction1(fun it ->
|
||||||
|
upcast it.G(nameof srch.text).Match(regexContains text))) :> ReqlExpr)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
}
|
}
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|> List.fold
|
|> List.fold
|
||||||
(fun q f -> f q)
|
(fun q f -> f q)
|
||||||
(r.Table(Table.Listing)
|
(r.Table(Table.Listing) :> ReqlExpr))
|
||||||
.EqJoin("continentId", r.Table(Table.Continent)) :> ReqlExpr))
|
.EqJoin("continentId", r.Table(Table.Continent))
|
||||||
.Merge(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
.Map(ReqlFunction1(fun it -> upcast r.HashMap("listing", it.G("left")).With("continent", it.G("right"))))
|
||||||
.Pluck("listing", "continent")
|
|
||||||
.RunResultAsync<ListingForView list> conn)
|
.RunResultAsync<ListingForView list> conn)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,17 +21,22 @@ module Error =
|
||||||
|
|
||||||
open System.Threading.Tasks
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
/// URL prefixes for the Vue app
|
||||||
|
let vueUrls = [
|
||||||
|
"/"; "/how-it-works"; "/privacy-policy"; "/terms-of-service"; "/citizen"; "/help-wanted"; "/listing"; "/profile"
|
||||||
|
"/so-long"; "/success-story"
|
||||||
|
]
|
||||||
|
|
||||||
/// Handler that will return a status code 404 and the text "Not Found"
|
/// Handler that will return a status code 404 and the text "Not Found"
|
||||||
let notFound : HttpHandler =
|
let notFound : HttpHandler =
|
||||||
fun next ctx -> task {
|
fun next ctx -> task {
|
||||||
let fac = ctx.GetService<ILoggerFactory>()
|
let fac = ctx.GetService<ILoggerFactory>()
|
||||||
let log = fac.CreateLogger("Handler")
|
let log = fac.CreateLogger("Handler")
|
||||||
match [ "GET"; "HEAD" ] |> List.contains ctx.Request.Method with
|
match [ "GET"; "HEAD" ] |> List.contains ctx.Request.Method with
|
||||||
| true ->
|
| true when vueUrls |> List.exists (fun url -> ctx.Request.Path.ToString().StartsWith url) ->
|
||||||
log.LogInformation "Returning Vue app"
|
log.LogInformation "Returning Vue app"
|
||||||
// TODO: check for valid URL prefixes
|
|
||||||
return! Vue.app next ctx
|
return! Vue.app next ctx
|
||||||
| false ->
|
| _ ->
|
||||||
log.LogInformation "Returning 404"
|
log.LogInformation "Returning 404"
|
||||||
return! RequestErrors.NOT_FOUND $"The URL {string ctx.Request.Path} was not recognized as a valid URL" next
|
return! RequestErrors.NOT_FOUND $"The URL {string ctx.Request.Path} was not recognized as a valid URL" next
|
||||||
ctx
|
ctx
|
||||||
|
@ -41,6 +46,11 @@ module Error =
|
||||||
let notAuthorized : HttpHandler =
|
let notAuthorized : HttpHandler =
|
||||||
setStatusCode 403 >=> fun _ _ -> Task.FromResult<HttpContext option> None
|
setStatusCode 403 >=> fun _ _ -> Task.FromResult<HttpContext option> None
|
||||||
|
|
||||||
|
/// Handler to log 500s and return a message we can display in the application
|
||||||
|
let unexpectedError (ex: exn) (log : ILogger) =
|
||||||
|
log.LogError(ex, "An unexpected error occurred")
|
||||||
|
clearResponse >=> ServerErrors.INTERNAL_ERROR ex.Message
|
||||||
|
|
||||||
|
|
||||||
/// Helper functions
|
/// Helper functions
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
|
@ -193,6 +203,15 @@ module Listing =
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /api/listing/view/[id]
|
||||||
|
let view listingId : HttpHandler =
|
||||||
|
authorize
|
||||||
|
>=> fun next ctx -> task {
|
||||||
|
match! Data.Listing.findByIdForView (ListingId listingId) (conn ctx) with
|
||||||
|
| Some listing -> return! json listing next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// POST: /listings
|
// POST: /listings
|
||||||
let add : HttpHandler =
|
let add : HttpHandler =
|
||||||
authorize
|
authorize
|
||||||
|
@ -453,6 +472,7 @@ let allEndpoints = [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/%O" Listing.get
|
routef "/%O" Listing.get
|
||||||
route "/search" Listing.search
|
route "/search" Listing.search
|
||||||
|
routef "/view/%O" Listing.view
|
||||||
route "s/mine" Listing.mine
|
route "s/mine" Listing.mine
|
||||||
]
|
]
|
||||||
POST [
|
POST [
|
||||||
|
|
960
src/JobsJobsJobs/App/package-lock.json
generated
960
src/JobsJobsJobs/App/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -10,39 +10,39 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "5.9.55",
|
"@mdi/font": "5.9.55",
|
||||||
"@vuelidate/core": "^2.0.0-alpha.22",
|
"@vuelidate/core": "^2.0.0-alpha.24",
|
||||||
"@vuelidate/validators": "^2.0.0-alpha.19",
|
"@vuelidate/validators": "^2.0.0-alpha.21",
|
||||||
"bootstrap": "^5.1.0",
|
"bootstrap": "^5.1.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.16.1",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.23.0",
|
||||||
"date-fns-tz": "^1.1.4",
|
"date-fns-tz": "^1.1.6",
|
||||||
"marked": "^2.1.3",
|
"marked": "^2.1.3",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.2.2",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.11",
|
||||||
"vuex": "^4.0.0-0"
|
"vuex": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bootstrap": "^5.1.0",
|
"@types/bootstrap": "^5.1.1",
|
||||||
"@types/marked": "^2.0.4",
|
"@types/marked": "^2.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/parser": "^4.29.1",
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
"@vue/cli-plugin-router": "~4.5.0",
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.0.0",
|
"@vue/compiler-sfc": "^3.2.2",
|
||||||
"@vue/eslint-config-standard": "^5.1.2",
|
"@vue/eslint-config-standard": "^6.1.0",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.24.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^5.0.0",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.16.0",
|
||||||
"sass": "~1.32.0",
|
"sass": "~1.37.0",
|
||||||
"sass-loader": "^10.0.0",
|
"sass-loader": "^10.0.0",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
import { Citizen } from './api'
|
||||||
import AppFooter from './components/layout/AppFooter.vue'
|
import AppFooter from './components/layout/AppFooter.vue'
|
||||||
import AppNav from './components/layout/AppNav.vue'
|
import AppNav from './components/layout/AppNav.vue'
|
||||||
import AppToaster from './components/layout/AppToaster.vue'
|
import AppToaster from './components/layout/AppToaster.vue'
|
||||||
|
@ -45,6 +46,16 @@ export default defineComponent({
|
||||||
export function yesOrNo (cond : boolean) : string {
|
export function yesOrNo (cond : boolean) : string {
|
||||||
return cond ? 'Yes' : 'No'
|
return cond ? 'Yes' : 'No'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display name for a citizen (the first available among real, display, or NAS handle)
|
||||||
|
*
|
||||||
|
* @param cit The citizen
|
||||||
|
* @returns The citizen's display name
|
||||||
|
*/
|
||||||
|
export function citizenName (cit : Citizen) : string {
|
||||||
|
return cit.realName || cit.displayName || cit.naUser
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
|
|
|
@ -75,6 +75,8 @@ async function apiResult<T> (resp : Response, action : string) : Promise<T | und
|
||||||
*/
|
*/
|
||||||
async function apiSend (resp : Response, action : string) : Promise<boolean | string> {
|
async function apiSend (resp : Response, action : string) : Promise<boolean | string> {
|
||||||
if (resp.status === 200) return true
|
if (resp.status === 200) return true
|
||||||
|
// HTTP 422 (Unprocessable Entity) is what the API returns for an expired JWT
|
||||||
|
if (resp.status === 422) return `Error ${action} - Your login has expired; refresh this page to renew it`
|
||||||
return `Error ${action} - (${resp.status}) ${await resp.text()}`
|
return `Error ${action} - (${resp.status}) ${await resp.text()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +174,17 @@ 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'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a job listing for viewing (also contains continent information)
|
||||||
|
*
|
||||||
|
* @param id The ID of the job listing to retrieve
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns The job listing (if found), undefined (if not found), or an error string
|
||||||
|
*/
|
||||||
|
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ListingForView | undefined | string> =>
|
||||||
|
apiResult<ListingForView>(await fetch(apiUrl(`listing/view/${id}`), reqInit('GET', user)),
|
||||||
|
'retrieving job listing'),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for job listings using the given parameters
|
* Search for job listings using the given parameters
|
||||||
*
|
*
|
||||||
|
@ -182,7 +195,7 @@ export default {
|
||||||
search: async (query : ListingSearch, user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => {
|
search: async (query : ListingSearch, user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (query.continentId) params.append('continentId', query.continentId)
|
if (query.continentId) params.append('continentId', query.continentId)
|
||||||
if (query.region) params.append('skill', query.region)
|
if (query.region) params.append('region', query.region)
|
||||||
params.append('remoteWork', query.remoteWork)
|
params.append('remoteWork', query.remoteWork)
|
||||||
if (query.text) params.append('text', query.text)
|
if (query.text) params.append('text', query.text)
|
||||||
return apiResult<ListingForView[]>(await fetch(apiUrl(`listing/search?${params.toString()}`),
|
return apiResult<ListingForView[]>(await fetch(apiUrl(`listing/search?${params.toString()}`),
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<template v-if="errors.length > 0">
|
<template v-if="errors.length > 0">
|
||||||
<p>The following error<template v-if="errors.length !== 1">s</template> occurred:</p>
|
<p>The following error<template v-if="errors.length !== 1">s</template> occurred:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(error, idx) in errors" :key="idx"><pre>{{error}}</pre></li>
|
<li v-for="(error, idx) in errors" :key="idx">{{error}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<slot v-else></slot>
|
<slot v-else></slot>
|
||||||
|
@ -21,3 +21,8 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
ul li
|
||||||
|
font-family: monospace
|
||||||
|
</style>
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<td>{{it.listing.title}}</td>
|
<td>{{it.listing.title}}</td>
|
||||||
<td>{{it.continent.name}} / {{it.listing.region}}</td>
|
<td>{{it.continent.name}} / {{it.listing.region}}</td>
|
||||||
<td class="text-center">{{yesOrNo(it.listing.remoteWork)}}</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-if="it.listing.neededBy" class="text-center">{{formatNeededBy(it.listing.neededBy)}}</td>
|
||||||
<td v-else class="text-center">N/A</td>
|
<td v-else class="text-center">N/A</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -43,7 +43,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, Ref, watch } from 'vue'
|
import { defineComponent, ref, Ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { format } from 'date-fns'
|
|
||||||
|
import { formatNeededBy } from './ListingView.vue'
|
||||||
import { yesOrNo } from '@/App.vue'
|
import { yesOrNo } from '@/App.vue'
|
||||||
import api, { ListingForView, ListingSearch, LogOnSuccess } from '@/api'
|
import api, { ListingForView, ListingSearch, LogOnSuccess } from '@/api'
|
||||||
import { queryValue } from '@/router'
|
import { queryValue } from '@/router'
|
||||||
|
@ -138,7 +139,7 @@ export default defineComponent({
|
||||||
searched,
|
searched,
|
||||||
results,
|
results,
|
||||||
yesOrNo,
|
yesOrNo,
|
||||||
format
|
formatNeededBy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,88 @@
|
||||||
<template>
|
<template>
|
||||||
<p>TODO: write</p>
|
<article>
|
||||||
|
<page-title :title="pageTitle" />
|
||||||
|
<load-data :load="retrieveListing">
|
||||||
|
<h3>{{it.listing.title}}</h3>
|
||||||
|
<h4 class="pb-3 text-muted">{{it.continent.name}} / {{it.listing.region}}</h4>
|
||||||
|
<p>
|
||||||
|
<template v-if="it.listing.neededBy">
|
||||||
|
<strong><em>NEEDED BY {{formatNeededBy(it.listing.neededBy)}}</em></strong> •
|
||||||
</template>
|
</template>
|
||||||
|
Listed by {{citizenName(citizen)}} –
|
||||||
|
<a :href="`https://noagendasocial.com/@${citizen.naUser}`" target="_blank">View No Agenda Social profile</a>
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<div v-html="details"></div>
|
||||||
|
</load-data>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref, Ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import marked from 'marked'
|
||||||
|
|
||||||
|
import api, { Citizen, ListingForView, LogOnSuccess, markedOptions } from '@/api'
|
||||||
|
import { citizenName } from '@/App.vue'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import LoadData from '@/components/LoadData.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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), 'PPP')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ListingView',
|
||||||
|
components: { LoadData },
|
||||||
|
setup () {
|
||||||
|
const store = useStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
/** The currently logged-on user */
|
||||||
|
const user = store.state.user as LogOnSuccess
|
||||||
|
|
||||||
|
/** The requested job listing */
|
||||||
|
const it : Ref<ListingForView | undefined> = ref(undefined)
|
||||||
|
|
||||||
|
/** The citizen who posted this job listing */
|
||||||
|
const citizen : Ref<Citizen | undefined> = ref(undefined)
|
||||||
|
|
||||||
|
/** Retrieve the job listing and supporting data */
|
||||||
|
const retrieveListing = async (errors : string[]) => {
|
||||||
|
const listingResp = await api.listings.retreiveForView(route.params.id as string, user)
|
||||||
|
if (typeof listingResp === 'string') {
|
||||||
|
errors.push(listingResp)
|
||||||
|
} else if (typeof listingResp === 'undefined') {
|
||||||
|
errors.push('Job Listing not found')
|
||||||
|
} else {
|
||||||
|
it.value = listingResp
|
||||||
|
const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user)
|
||||||
|
if (typeof citizenResp === 'string') {
|
||||||
|
errors.push(citizenResp)
|
||||||
|
} else if (typeof citizenResp === 'undefined') {
|
||||||
|
errors.push('Listing Citizen not found (this should not happen)')
|
||||||
|
} else {
|
||||||
|
citizen.value = citizenResp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageTitle: computed(() => it.value ? `${it.value.listing.title} | Job Listing` : 'Loading Job Listing...'),
|
||||||
|
retrieveListing,
|
||||||
|
it,
|
||||||
|
details: computed(() => marked(it.value?.listing.text || '', markedOptions)),
|
||||||
|
citizen,
|
||||||
|
citizenName,
|
||||||
|
formatNeededBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<article>
|
<article>
|
||||||
<page-title :title="pageTitle" />
|
<page-title :title="pageTitle" />
|
||||||
<load-data :load="retrieveProfile">
|
<load-data :load="retrieveProfile">
|
||||||
<h2><a :href="it.citizen.profileUrl" target="_blank">{{citizenName}}</a></h2>
|
<h2><a :href="it.citizen.profileUrl" target="_blank">{{it.citizen.citizenName()}}</a></h2>
|
||||||
<h4 class="pb-3">{{it.continent.name}}, {{it.profile.region}}</h4>
|
<h4 class="pb-3">{{it.continent.name}}, {{it.profile.region}}</h4>
|
||||||
<p v-html="workTypes"></p>
|
<p v-html="workTypes"></p>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -38,12 +38,14 @@
|
||||||
import { computed, defineComponent, ref, Ref } from 'vue'
|
import { computed, defineComponent, ref, Ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import marked from 'marked'
|
import marked from 'marked'
|
||||||
|
|
||||||
import api, { LogOnSuccess, markedOptions, ProfileForView } from '@/api'
|
import api, { LogOnSuccess, markedOptions, ProfileForView } from '@/api'
|
||||||
|
import { citizenName } from '@/App.vue'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import LoadData from '@/components/LoadData.vue'
|
import LoadData from '@/components/LoadData.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ProfileEdit',
|
name: 'ProfileView',
|
||||||
components: { LoadData },
|
components: { LoadData },
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
@ -55,12 +57,6 @@ export default defineComponent({
|
||||||
/** The requested profile */
|
/** The requested profile */
|
||||||
const it : Ref<ProfileForView | undefined> = ref(undefined)
|
const it : Ref<ProfileForView | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** The citizen's name (real, display, or NAS, whichever is found first) */
|
|
||||||
const citizenName = computed(() => {
|
|
||||||
const c = it.value?.citizen
|
|
||||||
return c?.realName || c?.displayName || c?.naUser || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
/** The work types for the top of the page */
|
/** The work types for the top of the page */
|
||||||
const workTypes = computed(() => {
|
const workTypes = computed(() => {
|
||||||
const parts : string[] = []
|
const parts : string[] = []
|
||||||
|
@ -90,12 +86,12 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageTitle: computed(() => it.value ? `Employment profile for ${citizenName.value}` : 'Loading Profile...'),
|
pageTitle: computed(() =>
|
||||||
|
it.value ? `Employment profile for ${citizenName(it.value.citizen)}` : 'Loading Profile...'),
|
||||||
user,
|
user,
|
||||||
retrieveProfile,
|
retrieveProfile,
|
||||||
it,
|
it,
|
||||||
workTypes,
|
workTypes,
|
||||||
citizenName,
|
|
||||||
bioHtml: computed(() => marked(it.value?.profile.biography || '', markedOptions)),
|
bioHtml: computed(() => marked(it.value?.profile.biography || '', markedOptions)),
|
||||||
expHtml: computed(() => marked(it.value?.profile.experience || '', markedOptions))
|
expHtml: computed(() => marked(it.value?.profile.experience || '', markedOptions))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user