Version 3 #40
@ -6,7 +6,6 @@ import {
|
||||
RouteRecordRaw
|
||||
} from "vue-router"
|
||||
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 */
|
||||
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> = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home,
|
||||
meta: { title: "Welcome!" }
|
||||
},
|
||||
{
|
||||
path: "/how-it-works",
|
||||
name: "HowItWorks",
|
||||
@ -68,12 +61,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue"),
|
||||
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
|
||||
{
|
||||
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
|
||||
|
||||
|
||||
/// Handlers for /api/listing[s] routes
|
||||
/// Handlers for /listing[s] routes
|
||||
[<RequireQualifiedAccess>]
|
||||
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
|
||||
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]
|
||||
let get listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Listings.findById (ListingId listingId) with
|
||||
@ -826,6 +831,11 @@ let allEndpoints = [
|
||||
]
|
||||
]
|
||||
GET_HEAD [ route "/how-it-works" Home.howItWorks ]
|
||||
subRoute "/listing" [
|
||||
GET_HEAD [
|
||||
route "s/mine" Listing.mine
|
||||
]
|
||||
]
|
||||
GET_HEAD [ route "/privacy-policy" Home.privacyPolicy ]
|
||||
subRoute "/profile" [
|
||||
GET_HEAD [
|
||||
@ -851,14 +861,13 @@ let allEndpoints = [
|
||||
GET_HEAD [ route "/continents" Continent.all ]
|
||||
subRoute "/listing" [
|
||||
GET_HEAD [
|
||||
routef "/%O" Listing.get
|
||||
route "/search" Listing.search
|
||||
routef "/%O/view" Listing.view
|
||||
route "s/mine" Listing.mine
|
||||
routef "/%O" ListingApi.get
|
||||
route "/search" ListingApi.search
|
||||
routef "/%O/view" ListingApi.view
|
||||
]
|
||||
PATCH [ routef "/%O" Listing.expire ]
|
||||
POST [ route "s" Listing.add ]
|
||||
PUT [ routef "/%O" Listing.update ]
|
||||
PATCH [ routef "/%O" ListingApi.expire ]
|
||||
POST [ route "s" ListingApi.add ]
|
||||
PUT [ routef "/%O" ListingApi.update ]
|
||||
]
|
||||
POST [ route "/markdown-preview" Api.markdownPreview ]
|
||||
subRoute "/profile" [
|
||||
|
@ -15,6 +15,7 @@
|
||||
<Compile Include="Views\Layout.fs" />
|
||||
<Compile Include="Views\Citizen.fs" />
|
||||
<Compile Include="Views\Home.fs" />
|
||||
<Compile Include="Views\Listing.fs" />
|
||||
<Compile Include="Views\Profile.fs" />
|
||||
<Compile Include="Handlers.fs" />
|
||||
<Compile Include="App.fs" />
|
||||
|
@ -96,7 +96,14 @@ let yesOrNo value =
|
||||
open NodaTime
|
||||
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 =
|
||||
(ZonedDateTimePattern.CreateWithCurrentCulture ("MMMM d, yyyy", DateTimeZoneProviders.Tzdb))
|
||||
.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