Migrate "my listings" page

This commit is contained in:
Daniel J. Summers 2023-01-15 16:26:06 -05:00
parent 3593d84fe6
commit 7001e75ac2
7 changed files with 95 additions and 135 deletions

View File

@ -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",

View File

@ -1,19 +0,0 @@
<template>
<article>
<p>&nbsp;</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&rsquo;s true!)</audio-clip></em> and find out what you&rsquo;re missing.
</p>
</article>
</template>
<script setup lang="ts">
import AudioClip from "@/components/AudioClip.vue"
</script>

View File

@ -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>&nbsp;~
<router-link :to="`/listing/${it.listing.id}/view`">View</router-link>&nbsp;~
<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>

View File

@ -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" [

View File

@ -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" />

View File

@ -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()}"

View 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 []
]
]