Version 3 #40
|
@ -6,7 +6,6 @@ import {
|
||||||
RouteRecordRaw
|
RouteRecordRaw
|
||||||
} from "vue-router"
|
} from "vue-router"
|
||||||
import store, { Mutations } from "@/store"
|
import store, { Mutations } from "@/store"
|
||||||
import Home from "@/views/Home.vue"
|
|
||||||
|
|
||||||
/** The URL to which the user should be pointed once they have authorized with Mastodon */
|
/** The URL to which the user should be pointed once they have authorized with Mastodon */
|
||||||
export const AFTER_LOG_ON_URL = "jjj-after-log-on-url"
|
export const AFTER_LOG_ON_URL = "jjj-after-log-on-url"
|
||||||
|
@ -24,12 +23,6 @@ export function queryValue (route: RouteLocationNormalizedLoaded, key : string)
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
name: "Home",
|
|
||||||
component: Home,
|
|
||||||
meta: { title: "Welcome!" }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/how-it-works",
|
path: "/how-it-works",
|
||||||
name: "HowItWorks",
|
name: "HowItWorks",
|
||||||
|
@ -68,12 +61,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue"),
|
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue"),
|
||||||
meta: { auth: true, title: "Loading Job Listing..." }
|
meta: { auth: true, title: "Loading Job Listing..." }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/listings/mine",
|
|
||||||
name: "MyListings",
|
|
||||||
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/MyListings.vue"),
|
|
||||||
meta: { auth: true, title: "My Job Listings" }
|
|
||||||
},
|
|
||||||
// Success Story URLs
|
// Success Story URLs
|
||||||
{
|
{
|
||||||
path: "/success-story/list",
|
path: "/success-story/list",
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
<template>
|
|
||||||
<article>
|
|
||||||
<p> </p>
|
|
||||||
<p>
|
|
||||||
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in
|
|
||||||
finding employment. This will enable them to continue providing value-for-value to Adam and John, as they continue
|
|
||||||
their work deconstructing the misinformation that passes for news on a day-to-day basis.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Do you not understand the terms in the paragraph above? No worries; just head over to
|
|
||||||
<a href="https://noagendashow.net" target="_blank" rel="noopener">The Best Podcast in the Universe</a> <em>
|
|
||||||
<audio-clip clip="thats-true">(that’s true!)</audio-clip></em> and find out what you’re missing.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import AudioClip from "@/components/AudioClip.vue"
|
|
||||||
</script>
|
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<article>
|
|
||||||
<h3 class="pb-3">My Job Listings</h3>
|
|
||||||
<p><router-link class="btn btn-outline-primary" to="/listing/new/edit">Add a New Job Listing</router-link></p>
|
|
||||||
<load-data :load="getListings">
|
|
||||||
<h4 v-if="expired.length > 0" class="pb-2">Active Job Listings</h4>
|
|
||||||
<table v-if="active.length > 0" class="pb-3 table table-sm table-hover pt-3">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Action</th>
|
|
||||||
<th scope="col">Title</th>
|
|
||||||
<th scope="col">Continent / Region</th>
|
|
||||||
<th scope="col">Created</th>
|
|
||||||
<th scope="col">Updated</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="it in active" :key="it.listing.id">
|
|
||||||
<td>
|
|
||||||
<router-link :to="`/listing/${it.listing.id}/edit`">Edit</router-link> ~
|
|
||||||
<router-link :to="`/listing/${it.listing.id}/view`">View</router-link> ~
|
|
||||||
<router-link :to="`/listing/${it.listing.id}/expire`">Expire</router-link>
|
|
||||||
</td>
|
|
||||||
<td>{{it.listing.title}}</td>
|
|
||||||
<td>{{it.continent.name}} / {{it.listing.region}}</td>
|
|
||||||
<td><full-date-time :date="it.listing.createdOn" /></td>
|
|
||||||
<td><full-date-time :date="it.listing.updatedOn" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p v-else class="pb-3 fst-italic">You have no active job listings</p>
|
|
||||||
<template v-if="expired.length > 0">
|
|
||||||
<h4 class="pb-2">Expired Job Listings</h4>
|
|
||||||
<table class="table table-sm table-hover pt-3">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Action</th>
|
|
||||||
<th scope="col">Title</th>
|
|
||||||
<th scope="col">Filled Here?</th>
|
|
||||||
<th scope="col">Expired</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="it in expired" :key="it.listing.id">
|
|
||||||
<td><router-link :to="`/listing/${it.listing.id}/view`">View</router-link></td>
|
|
||||||
<td>{{it.listing.title}}</td>
|
|
||||||
<td>{{yesOrNo(it.listing.wasFilledHere)}}</td>
|
|
||||||
<td><full-date-time :date="it.listing.updatedOn" /></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
</load-data>
|
|
||||||
</article>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, Ref, ref } from "vue"
|
|
||||||
import api, { ListingForView, LogOnSuccess } from "@/api"
|
|
||||||
import { yesOrNo } from "@/App.vue"
|
|
||||||
import { useStore } from "@/store"
|
|
||||||
|
|
||||||
import FullDateTime from "@/components/FullDateTime.vue"
|
|
||||||
import LoadData from "@/components/LoadData.vue"
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
|
|
||||||
/** The listings for the user */
|
|
||||||
const listings : Ref<ListingForView[]> = ref([])
|
|
||||||
|
|
||||||
/** The active (non-expired) listings entered by this user */
|
|
||||||
const active = computed(() => listings.value.filter(it => !it.listing.isExpired))
|
|
||||||
|
|
||||||
/** The expired listings entered by this user */
|
|
||||||
const expired = computed(() => listings.value.filter(it => it.listing.isExpired))
|
|
||||||
|
|
||||||
/** Retrieve the job listing posted by the current citizen */
|
|
||||||
const getListings = async (errors : string[]) => {
|
|
||||||
const listResult = await api.listings.mine(store.state.user as LogOnSuccess)
|
|
||||||
if (typeof listResult === "string") {
|
|
||||||
errors.push(listResult)
|
|
||||||
} else if (typeof listResult === "undefined") {
|
|
||||||
errors.push("API call returned 404 (this should not happen)")
|
|
||||||
} else {
|
|
||||||
listings.value = listResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -486,19 +486,24 @@ module Home =
|
||||||
renderHandler "Terms of Service" Home.termsOfService
|
renderHandler "Terms of Service" Home.termsOfService
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /api/listing[s] routes
|
/// Handlers for /listing[s] routes
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Listing =
|
module Listing =
|
||||||
|
|
||||||
|
// GET: /listings/mine
|
||||||
|
let mine : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
|
let! listings = Listings.findByCitizen (currentCitizenId ctx)
|
||||||
|
return! Listing.mine listings (timeZone ctx) |> render "My Job Listings" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Handlers for /api/listing[s] routes
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module ListingApi =
|
||||||
|
|
||||||
/// Parse the string we receive from JSON into a NodaTime local date
|
/// Parse the string we receive from JSON into a NodaTime local date
|
||||||
let private parseDate = DateTime.Parse >> LocalDate.FromDateTime
|
let private parseDate = DateTime.Parse >> LocalDate.FromDateTime
|
||||||
|
|
||||||
// GET: /api/listings/mine
|
|
||||||
let mine : HttpHandler = authorize >=> fun next ctx -> task {
|
|
||||||
let! listings = Listings.findByCitizen (currentCitizenId ctx)
|
|
||||||
return! json listings next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET: /api/listing/[id]
|
// GET: /api/listing/[id]
|
||||||
let get listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
let get listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||||
match! Listings.findById (ListingId listingId) with
|
match! Listings.findById (ListingId listingId) with
|
||||||
|
@ -826,6 +831,11 @@ let allEndpoints = [
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/how-it-works" Home.howItWorks ]
|
GET_HEAD [ route "/how-it-works" Home.howItWorks ]
|
||||||
|
subRoute "/listing" [
|
||||||
|
GET_HEAD [
|
||||||
|
route "s/mine" Listing.mine
|
||||||
|
]
|
||||||
|
]
|
||||||
GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ]
|
GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ]
|
||||||
subRoute "/profile" [
|
subRoute "/profile" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
|
@ -851,14 +861,13 @@ let allEndpoints = [
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
GET_HEAD [ route "/continents" Continent.all ]
|
||||||
subRoute "/listing" [
|
subRoute "/listing" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
routef "/%O" Listing.get
|
routef "/%O" ListingApi.get
|
||||||
route "/search" Listing.search
|
route "/search" ListingApi.search
|
||||||
routef "/%O/view" Listing.view
|
routef "/%O/view" ListingApi.view
|
||||||
route "s/mine" Listing.mine
|
|
||||||
]
|
]
|
||||||
PATCH [ routef "/%O" Listing.expire ]
|
PATCH [ routef "/%O" ListingApi.expire ]
|
||||||
POST [ route "s" Listing.add ]
|
POST [ route "s" ListingApi.add ]
|
||||||
PUT [ routef "/%O" Listing.update ]
|
PUT [ routef "/%O" ListingApi.update ]
|
||||||
]
|
]
|
||||||
POST [ route "/markdown-preview" Api.markdownPreview ]
|
POST [ route "/markdown-preview" Api.markdownPreview ]
|
||||||
subRoute "/profile" [
|
subRoute "/profile" [
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<Compile Include="Views\Layout.fs" />
|
<Compile Include="Views\Layout.fs" />
|
||||||
<Compile Include="Views\Citizen.fs" />
|
<Compile Include="Views\Citizen.fs" />
|
||||||
<Compile Include="Views\Home.fs" />
|
<Compile Include="Views\Home.fs" />
|
||||||
|
<Compile Include="Views\Listing.fs" />
|
||||||
<Compile Include="Views\Profile.fs" />
|
<Compile Include="Views\Profile.fs" />
|
||||||
<Compile Include="Handlers.fs" />
|
<Compile Include="Handlers.fs" />
|
||||||
<Compile Include="App.fs" />
|
<Compile Include="App.fs" />
|
||||||
|
|
|
@ -96,7 +96,14 @@ let yesOrNo value =
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open NodaTime.Text
|
open NodaTime.Text
|
||||||
|
|
||||||
/// Generate a full date from an instant in the citizen's local time zone
|
/// Generate a full date in the citizen's local time zone
|
||||||
let fullDate (value : Instant) tz =
|
let fullDate (value : Instant) tz =
|
||||||
(ZonedDateTimePattern.CreateWithCurrentCulture ("MMMM d, yyyy", DateTimeZoneProviders.Tzdb))
|
(ZonedDateTimePattern.CreateWithCurrentCulture ("MMMM d, yyyy", DateTimeZoneProviders.Tzdb))
|
||||||
.Format(value.InZone(DateTimeZoneProviders.Tzdb[tz]))
|
.Format(value.InZone(DateTimeZoneProviders.Tzdb[tz]))
|
||||||
|
|
||||||
|
/// Generate a full date/time in the citizen's local time
|
||||||
|
let fullDateTime (value : Instant) tz =
|
||||||
|
let dtPattern = ZonedDateTimePattern.CreateWithCurrentCulture ("MMMM d, yyyy h:mm", DateTimeZoneProviders.Tzdb)
|
||||||
|
let amPmPattern = ZonedDateTimePattern.CreateWithCurrentCulture ("tt", DateTimeZoneProviders.Tzdb)
|
||||||
|
let tzValue = value.InZone(DateTimeZoneProviders.Tzdb[tz])
|
||||||
|
$"{dtPattern.Format(tzValue)} {amPmPattern.Format(tzValue).ToLowerInvariant()}"
|
||||||
|
|
63
src/JobsJobsJobs/Server/Views/Listing.fs
Normal file
63
src/JobsJobsJobs/Server/Views/Listing.fs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/// Views for /profile URLs
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module JobsJobsJobs.Views.Listing
|
||||||
|
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
open Giraffe.ViewEngine.Htmx
|
||||||
|
open JobsJobsJobs.Domain
|
||||||
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
|
open JobsJobsJobs.ViewModels
|
||||||
|
|
||||||
|
|
||||||
|
/// "My Listings" page
|
||||||
|
let mine (listings : ListingForView list) tz =
|
||||||
|
let active = listings |> List.filter (fun it -> not it.Listing.IsExpired)
|
||||||
|
let expired = listings |> List.filter (fun it -> it.Listing.IsExpired)
|
||||||
|
article [] [
|
||||||
|
h3 [ _class "pb-3" ] [ rawText "My Job Listings" ]
|
||||||
|
p [] [ a [ _href "/listing/new/edit"; _class "btn btn-outline-primary" ] [ rawText "Add a New Job Listing" ] ]
|
||||||
|
if not (List.isEmpty expired) then h4 [ _class "pb-2" ] [ rawText "Active Job Listings" ]
|
||||||
|
if List.isEmpty active then
|
||||||
|
p [ _class "pb-3 fst-italic" ] [ rawText "You have no active job listings" ]
|
||||||
|
else
|
||||||
|
table [ _class "pb-3 table table-sm table-hover pt-3" ] [
|
||||||
|
thead [] [
|
||||||
|
[ "Action"; "Title"; "Continent / Region"; "Created"; "Updated" ]
|
||||||
|
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
||||||
|
|> tr []
|
||||||
|
]
|
||||||
|
active
|
||||||
|
|> List.map (fun it ->
|
||||||
|
let listId = ListingId.toString it.Listing.Id
|
||||||
|
tr [] [
|
||||||
|
td [] [
|
||||||
|
a [ _href $"/listing/{listId}/edit" ] [ rawText "Edit" ]; rawText " ~ "
|
||||||
|
a [ _href $"/listing/{listId}/view" ] [ rawText "View" ]; rawText " ~ "
|
||||||
|
a [ _href $"/listing/{listId}/expire" ] [ rawText "Expire" ]
|
||||||
|
]
|
||||||
|
td [] [ str it.Listing.Title ]
|
||||||
|
td [] [ str it.Continent.Name; rawText " / "; str it.Listing.Region ]
|
||||||
|
td [] [ str (fullDateTime it.Listing.CreatedOn tz) ]
|
||||||
|
td [] [ str (fullDateTime it.Listing.UpdatedOn tz) ]
|
||||||
|
])
|
||||||
|
|> tbody []
|
||||||
|
]
|
||||||
|
if not (List.isEmpty expired) then
|
||||||
|
h4 [ _class "pb-2" ] [ rawText "Expired Job Listings" ]
|
||||||
|
table [ _class "table table-sm table-hover pt-3" ] [
|
||||||
|
thead [] [
|
||||||
|
[ "Action"; "Title"; "Filled Here?"; "Expired" ]
|
||||||
|
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
||||||
|
|> tr []
|
||||||
|
]
|
||||||
|
expired
|
||||||
|
|> List.map (fun it ->
|
||||||
|
tr [] [
|
||||||
|
td [] [ a [ _href $"/listing/{ListingId.toString it.Listing.Id}/view" ] [rawText "View" ] ]
|
||||||
|
td [] [ str it.Listing.Title ]
|
||||||
|
td [] [ str (yesOrNo (defaultArg it.Listing.WasFilledHere false)) ]
|
||||||
|
td [] [ str (fullDateTime it.Listing.UpdatedOn tz) ]
|
||||||
|
])
|
||||||
|
|> tbody []
|
||||||
|
]
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user