Migrate success story pages
This commit is contained in:
parent
a018b6b8f1
commit
ccd7311e74
|
@ -1,120 +0,0 @@
|
|||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">{{title}}</h3>
|
||||
<load-data :load="retrieveStory">
|
||||
<p v-if="isNew">
|
||||
Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came about; tell us
|
||||
about it below! <em>(These will be visible to other users, but not to the general public.)</em>
|
||||
</p>
|
||||
<form class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="fromHere" class="form-check-input" v-model="v$.fromHere.$model">
|
||||
<label class="form-check-label" for="fromHere">I found my employment here</label>
|
||||
</div>
|
||||
</div>
|
||||
<markdown-editor id="story" label="The Success Story" v-model:text="v$.story.$model" />
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary" type="submit" @click.prevent="saveStory(true)">
|
||||
<icon :icon="mdiContentSaveOutline" /> Save
|
||||
</button>
|
||||
<p v-if="isNew">
|
||||
<em>(Saving this will set “Seeking Employment” to “No” on your profile.)</em>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</load-data>
|
||||
<maybe-save :saveAction="doSave" :validator="v$" />
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { mdiContentSaveOutline } from "@mdi/js"
|
||||
import useVuelidate from "@vuelidate/core"
|
||||
|
||||
import api, { LogOnSuccess, StoryForm } from "@/api"
|
||||
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
|
||||
import { Mutations, useStore } from "@/store"
|
||||
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
import MarkdownEditor from "@/components/MarkdownEditor.vue"
|
||||
import MaybeSave from "@/components/MaybeSave.vue"
|
||||
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The ID of the story being edited */
|
||||
const id = route.params.id as string
|
||||
|
||||
/** Whether this is a new story */
|
||||
const isNew = computed(() => id === "new")
|
||||
|
||||
/** The form for editing the story */
|
||||
const story = reactive(new StoryForm())
|
||||
|
||||
/** Validator rules */
|
||||
const rules = computed(() => ({
|
||||
fromHere: { },
|
||||
story: { }
|
||||
}))
|
||||
|
||||
/** The validator */
|
||||
const v$ = useVuelidate(rules, story, { $lazy: true })
|
||||
|
||||
/** Retrieve the specified story */
|
||||
const retrieveStory = async (errors : string[]) => {
|
||||
if (isNew.value) {
|
||||
story.id = "new"
|
||||
store.commit(Mutations.SetTitle, "Tell Your Success Story")
|
||||
} else {
|
||||
const storyResult = await api.success.retrieve(id, user)
|
||||
if (typeof storyResult === "string") {
|
||||
errors.push(storyResult)
|
||||
} else if (typeof storyResult === "undefined") {
|
||||
errors.push("Story not found")
|
||||
} else if (storyResult.citizenId !== user.citizenId) {
|
||||
errors.push("Quit messing around")
|
||||
} else {
|
||||
story.id = storyResult.id
|
||||
story.fromHere = storyResult.fromHere
|
||||
story.story = storyResult.story ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Save the success story */
|
||||
const saveStory = async (navigate : boolean) => {
|
||||
const saveResult = await api.success.save(story, user)
|
||||
if (typeof saveResult === "string") {
|
||||
toastError(saveResult, "saving success story")
|
||||
} else {
|
||||
if (isNew.value) {
|
||||
const foundResult = await api.profile.markEmploymentFound(user)
|
||||
if (typeof foundResult === "string") {
|
||||
toastError(foundResult, "clearing employment flag")
|
||||
} else {
|
||||
toastSuccess("Success Story saved and Seeking Employment flag cleared successfully")
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
await router.push("/success-story/list")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastSuccess("Success Story saved successfully")
|
||||
v$.value.$reset()
|
||||
if (navigate) {
|
||||
await router.push("/success-story/list")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** No-parameter save function (used for save-on-navigate) */
|
||||
const doSave = async () => await saveStory(false)
|
||||
</script>
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<article>
|
||||
<h3 class="pb-3">Success Stories</h3>
|
||||
<load-data :load="retrieveStories">
|
||||
<table v-if="stories?.length > 0" class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Story</th>
|
||||
<th scope="col">From</th>
|
||||
<th scope="col">Found Here?</th>
|
||||
<th scope="col">Recorded On</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="story in stories" :key="story.id">
|
||||
<td>
|
||||
<router-link v-if="story.hasStory" :to="`/success-story/${story.id}/view`">View</router-link>
|
||||
<em v-else>None</em>
|
||||
<template v-if="story.citizenId === user.citizenId">
|
||||
~ <router-link :to="`/success-story/${story.id}/edit`">Edit</router-link>
|
||||
</template>
|
||||
</td>
|
||||
<td>{{story.citizenName}}</td>
|
||||
<td><strong v-if="story.fromHere">Yes</strong><template v-else>No</template></td>
|
||||
<td><full-date :date="story.recordedOn" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else>There are no success stories recorded <em>(yet)</em></p>
|
||||
</load-data>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref } from "vue"
|
||||
import api, { LogOnSuccess, StoryEntry } from "@/api"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import FullDate from "@/components/FullDate.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
const store = useStore()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The success stories to be displayed */
|
||||
const stories : Ref<StoryEntry[] | undefined> = ref(undefined)
|
||||
|
||||
/** Get all currently recorded stories */
|
||||
const retrieveStories = async (errors : string[]) => {
|
||||
const listResult = await api.success.list(user)
|
||||
if (typeof listResult === "string") {
|
||||
errors.push(listResult)
|
||||
} else if (typeof listResult === "undefined") {
|
||||
stories.value = []
|
||||
} else {
|
||||
stories.value = listResult
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,67 +0,0 @@
|
|||
<template>
|
||||
<article>
|
||||
<load-data :load="retrieveStory">
|
||||
<h3>
|
||||
{{citizenName}}’s Success Story
|
||||
<span v-if="story.fromHere" class="jjj-heading-label">
|
||||
<span class="badge bg-success">Via {{profileOrListing}} on Jobs, Jobs, Jobs</span>
|
||||
</span>
|
||||
</h3>
|
||||
<h4 class="pb-3 text-muted"><full-date-time :date="story.recordedOn" /></h4>
|
||||
<div v-if="story.story" v-html="successStory" />
|
||||
</load-data>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, Ref, ref } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import api, { LogOnSuccess, Success } from "@/api"
|
||||
import { citizenName as citName } from "@/App.vue"
|
||||
import { toHtml } from "@/markdown"
|
||||
import { useStore } from "@/store"
|
||||
|
||||
import FullDateTime from "@/components/FullDateTime.vue"
|
||||
import LoadData from "@/components/LoadData.vue"
|
||||
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
|
||||
/** The currently logged-on user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** The story to be displayed */
|
||||
const story : Ref<Success | undefined> = ref(undefined)
|
||||
|
||||
/** The citizen's name */
|
||||
const citizenName = ref("")
|
||||
|
||||
/** Retrieve the success story */
|
||||
const retrieveStory = async (errors : string []) => {
|
||||
const storyResponse = await api.success.retrieve(route.params.id as string, user)
|
||||
if (typeof storyResponse === "string") {
|
||||
errors.push(storyResponse)
|
||||
return
|
||||
}
|
||||
if (typeof storyResponse === "undefined") {
|
||||
errors.push("Success story not found")
|
||||
return
|
||||
}
|
||||
story.value = storyResponse
|
||||
const citResponse = await api.citizen.retrieve(story.value.citizenId, user)
|
||||
if (typeof citResponse === "string") {
|
||||
errors.push(citResponse)
|
||||
} else if (typeof citResponse === "undefined") {
|
||||
errors.push("Citizen not found")
|
||||
} else {
|
||||
citizenName.value = citName(citResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether this success is from an employment profile or a job listing */
|
||||
const profileOrListing = computed(() => story.value?.source === "profile" ? "employment profile" : "job listing")
|
||||
|
||||
/** The HTML success story */
|
||||
const successStory = computed(() => toHtml(story.value?.story ?? ""))
|
||||
</script>
|
|
@ -130,7 +130,7 @@ type ProfileForView =
|
|||
|
||||
|
||||
/// The parameters for a public job search
|
||||
[<CLIMutable>]
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type PublicSearchForm =
|
||||
{ /// Retrieve citizens from this continent
|
||||
ContinentId : string
|
||||
|
@ -163,20 +163,8 @@ type PublicSearchResult =
|
|||
}
|
||||
|
||||
|
||||
/// The data required to provide a success story
|
||||
type StoryForm =
|
||||
{ /// The ID of this story
|
||||
Id : string
|
||||
|
||||
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
||||
FromHere : bool
|
||||
|
||||
/// The success story
|
||||
Story : string
|
||||
}
|
||||
|
||||
|
||||
/// An entry in the list of success stories
|
||||
[<NoComparison; NoEquality>]
|
||||
type StoryEntry =
|
||||
{ /// The ID of this success story
|
||||
Id : SuccessId
|
||||
|
|
|
@ -264,7 +264,7 @@ module Citizen =
|
|||
let! citizen = Citizens.findById citizenId
|
||||
let! profile = Profiles.findById citizenId
|
||||
let! prfCount = Profiles.count ()
|
||||
return! Citizen.dashboard citizen.Value profile prfCount |> render "Dashboard" next ctx
|
||||
return! Citizen.dashboard citizen.Value profile prfCount (timeZone ctx) |> render "Dashboard" next ctx
|
||||
}
|
||||
|
||||
// POST: /citizen/delete
|
||||
|
@ -425,17 +425,6 @@ module CitizenApi =
|
|||
}
|
||||
|
||||
|
||||
/// Handlers for /api/continent routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module Continent =
|
||||
|
||||
// GET: /api/continent/all
|
||||
let all : HttpHandler = fun next ctx -> task {
|
||||
let! continents = Continents.all ()
|
||||
return! json continents next ctx
|
||||
}
|
||||
|
||||
|
||||
/// Handlers for the home page, legal stuff, and help
|
||||
[<RequireQualifiedAccess>]
|
||||
module Home =
|
||||
|
@ -718,64 +707,74 @@ module Profile =
|
|||
}
|
||||
|
||||
|
||||
/// Handlers for /api/profile routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module ProfileApi =
|
||||
|
||||
// PATCH: /api/profile/employment-found
|
||||
let employmentFound : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Profiles.findById (currentCitizenId ctx) with
|
||||
| Some profile ->
|
||||
do! Profiles.save { profile with IsSeekingEmployment = false }
|
||||
return! ok next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
||||
/// Handlers for /api/success routes
|
||||
/// Handlers for /success-stor[y|ies] routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module Success =
|
||||
|
||||
// GET: /api/success/[id]
|
||||
let get successId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Successes.findById (SuccessId successId) with
|
||||
| Some story -> return! json story next ctx
|
||||
// GET: /success-story/[id]/edit
|
||||
let edit successId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let isNew = successId = "new"
|
||||
let! theSuccess = task {
|
||||
if isNew then return Some { Success.empty with CitizenId = citizenId }
|
||||
else return! Successes.findById (SuccessId.ofString successId)
|
||||
}
|
||||
match theSuccess with
|
||||
| Some success when success.CitizenId = citizenId ->
|
||||
let pgTitle = $"""{if isNew then "Tell Your" else "Edit"} Success Story"""
|
||||
return!
|
||||
Success.edit (EditSuccessForm.fromSuccess success) (success.Id = SuccessId Guid.Empty) pgTitle
|
||||
(csrf ctx)
|
||||
|> render pgTitle next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /api/success/list
|
||||
let all : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
// GET: /success-stories
|
||||
let list : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
let! stories = Successes.all ()
|
||||
return! json stories next ctx
|
||||
return! Success.list stories (currentCitizenId ctx) (timeZone ctx) |> render "Success Stories" next ctx
|
||||
}
|
||||
|
||||
// POST: /api/success/save
|
||||
let save : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let! form = ctx.BindJsonAsync<StoryForm> ()
|
||||
let! success = task {
|
||||
match form.Id with
|
||||
| "new" ->
|
||||
return Some { Id = SuccessId.create ()
|
||||
CitizenId = citizenId
|
||||
RecordedOn = now ctx
|
||||
IsFromHere = form.FromHere
|
||||
Source = "profile"
|
||||
Story = noneIfEmpty form.Story |> Option.map Text
|
||||
}
|
||||
| successId ->
|
||||
match! Successes.findById (SuccessId.ofString successId) with
|
||||
| Some story when story.CitizenId = citizenId ->
|
||||
return Some { story with
|
||||
IsFromHere = form.FromHere
|
||||
Story = noneIfEmpty form.Story |> Option.map Text
|
||||
}
|
||||
| Some _ | None -> return None
|
||||
// GET: /success-story/[id]/view
|
||||
let view successId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
match! Successes.findById (SuccessId successId) with
|
||||
| Some success ->
|
||||
match! Citizens.findById success.CitizenId with
|
||||
| Some citizen ->
|
||||
return! Success.view success (Citizen.name citizen) (timeZone ctx) |> render "Success Story" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST: /success-story/save
|
||||
let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let! form = ctx.BindFormAsync<EditSuccessForm> ()
|
||||
let isNew = form.Id = ShortGuid.fromGuid Guid.Empty
|
||||
let! theSuccess = task {
|
||||
if isNew then
|
||||
return Some
|
||||
{ Success.empty with
|
||||
Id = SuccessId.create ()
|
||||
CitizenId = citizenId
|
||||
RecordedOn = now ctx
|
||||
Source = "profile"
|
||||
}
|
||||
else return! Successes.findById (SuccessId.ofString form.Id)
|
||||
}
|
||||
match success with
|
||||
| Some story ->
|
||||
do! Successes.save story
|
||||
return! ok next ctx
|
||||
match theSuccess with
|
||||
| Some story when story.CitizenId = citizenId ->
|
||||
do! Successes.save
|
||||
{ story with IsFromHere = form.FromHere; Story = noneIfEmpty form.Story |> Option.map Text }
|
||||
if isNew then
|
||||
match! Profiles.findById citizenId with
|
||||
| Some profile -> do! Profiles.save { profile with IsSeekingEmployment = false }
|
||||
| None -> ()
|
||||
let extraMsg = if isNew then " and seeking employment flag cleared" else ""
|
||||
do! addSuccess $"Success story saved{extraMsg} successfully" ctx
|
||||
return! redirectToGet "/success-stories" next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
@ -831,6 +830,14 @@ let allEndpoints = [
|
|||
route "/save" Profile.save
|
||||
]
|
||||
]
|
||||
subRoute "/success-stor" [
|
||||
GET_HEAD [
|
||||
route "ies" Success.list
|
||||
routef "y/%s/edit" Success.edit
|
||||
routef "y/%O/view" Success.view
|
||||
]
|
||||
POST [ route "y/save" Success.save ]
|
||||
]
|
||||
|
||||
subRoute "/api" [
|
||||
subRoute "/citizen" [
|
||||
|
@ -838,17 +845,6 @@ let allEndpoints = [
|
|||
route "/account" CitizenApi.account
|
||||
]
|
||||
]
|
||||
GET_HEAD [ route "/continents" Continent.all ]
|
||||
POST [ route "/markdown-preview" Api.markdownPreview ]
|
||||
subRoute "/profile" [
|
||||
PATCH [ route "/employment-found" ProfileApi.employmentFound ]
|
||||
]
|
||||
subRoute "/success" [
|
||||
GET_HEAD [
|
||||
routef "/%O" Success.get
|
||||
route "es" Success.all
|
||||
]
|
||||
POST [ route "" Success.save ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<Compile Include="Views\Home.fs" />
|
||||
<Compile Include="Views\Listing.fs" />
|
||||
<Compile Include="Views\Profile.fs" />
|
||||
<Compile Include="Views\Success.fs" />
|
||||
<Compile Include="Handlers.fs" />
|
||||
<Compile Include="App.fs" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -136,6 +136,30 @@ module EditProfileViewModel =
|
|||
}
|
||||
|
||||
|
||||
/// The data required to provide a success story
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditSuccessForm =
|
||||
{ /// The ID of this success story
|
||||
Id : string
|
||||
|
||||
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
||||
FromHere : bool
|
||||
|
||||
/// The success story
|
||||
Story : string
|
||||
}
|
||||
|
||||
/// Support functions for success edit forms
|
||||
module EditSuccessForm =
|
||||
|
||||
/// Create an edit form from a success story
|
||||
let fromSuccess (success : Success) =
|
||||
{ Id = SuccessId.toString success.Id
|
||||
FromHere = success.IsFromHere
|
||||
Story = success.Story |> Option.map MarkdownString.toString |> Option.defaultValue ""
|
||||
}
|
||||
|
||||
|
||||
/// The form submitted to expire a listing
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type ExpireListingForm =
|
||||
|
|
|
@ -23,7 +23,7 @@ let confirmAccount isConfirmed =
|
|||
]
|
||||
|
||||
/// The citizen's dashboard page
|
||||
let dashboard (citizen : Citizen) (profile : Profile option) profileCount =
|
||||
let dashboard (citizen : Citizen) (profile : Profile option) profileCount tz =
|
||||
article [ _class "container" ] [
|
||||
h3 [ _class "pb-4" ] [ rawText "ITM, "; str citizen.FirstName; rawText "!" ]
|
||||
div [ _class "row row-cols-1 row-cols-md-2" ] [
|
||||
|
@ -34,7 +34,7 @@ let dashboard (citizen : Citizen) (profile : Profile option) profileCount =
|
|||
match profile with
|
||||
| Some prfl ->
|
||||
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
||||
rawText "Last updated "; (* full-date-time :date="profile.lastUpdatedOn" *)
|
||||
rawText "Last updated "; str (fullDateTime prfl.LastUpdatedOn tz)
|
||||
]
|
||||
p [ _class "card-text" ] [
|
||||
rawText "Your profile currently lists "; str $"{List.length prfl.Skills}"
|
||||
|
|
|
@ -57,10 +57,10 @@ let private links ctx =
|
|||
] [ i [ _class $"mdi mdi-{icon}"; _ariaHidden "true" ] []; rawText text ]
|
||||
nav [ _class "jjj-nav" ] [
|
||||
if ctx.IsLoggedOn then
|
||||
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
||||
navLink "/help-wanted" "newspaper-variant-multiple-outline" "Help Wanted!"
|
||||
navLink "/profile/search" "view-list-outline" "Employment Profiles"
|
||||
navLink "/success-story/list" "thumb-up" "Success Stories"
|
||||
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
||||
navLink "/help-wanted" "newspaper-variant-multiple-outline" "Help Wanted!"
|
||||
navLink "/profile/search" "view-list-outline" "Employment Profiles"
|
||||
navLink "/success-stories" "thumb-up" "Success Stories"
|
||||
div [ _class "separator" ] []
|
||||
navLink "/citizen/account" "account-edit" "My Account"
|
||||
navLink "/listings/mine" "sign-text" "My Job Listings"
|
||||
|
|
89
src/JobsJobsJobs/Server/Views/Success.fs
Normal file
89
src/JobsJobsJobs/Server/Views/Success.fs
Normal file
|
@ -0,0 +1,89 @@
|
|||
/// Views for /success-stor[y|ies] URLs
|
||||
[<RequireQualifiedAccess>]
|
||||
module JobsJobsJobs.Views.Success
|
||||
|
||||
open Giraffe.ViewEngine
|
||||
open JobsJobsJobs.Domain
|
||||
open JobsJobsJobs.Domain.SharedTypes
|
||||
open JobsJobsJobs.ViewModels
|
||||
|
||||
/// The add/edit success story page
|
||||
let edit (m : EditSuccessForm) isNew pgTitle csrf =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText pgTitle ]
|
||||
if isNew then
|
||||
p [] [
|
||||
rawText "Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came "
|
||||
rawText "about; tell us about it below! "
|
||||
em [] [ rawText "(These will be visible to other users, but not to the general public.)" ]
|
||||
]
|
||||
form [ _class "row g-3"; _method "POST"; _action "/success-story/save" ] [
|
||||
antiForgery csrf
|
||||
input [ _type "hidden"; _name (nameof m.Id); _value m.Id ]
|
||||
div [ _class "col-12" ] [
|
||||
checkBox [] (nameof m.FromHere) m.FromHere "I found my employment here"
|
||||
]
|
||||
markdownEditor [] (nameof m.Story) m.Story "The Success Story"
|
||||
div [ _class "col-12" ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [
|
||||
i [ _class "mdi mdi-content-save-outline" ] []; rawText " Save"
|
||||
]
|
||||
if isNew then
|
||||
p [ _class "fst-italic" ] [
|
||||
rawText "(Saving this will set “Seeking Employment” to “No” on your "
|
||||
rawText "profile.)"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
/// The list of success stories
|
||||
let list (m : StoryEntry list) citizenId tz =
|
||||
article [] [
|
||||
h3 [ _class "pb-3" ] [ rawText "Success Stories" ]
|
||||
if List.isEmpty m then
|
||||
p [] [ rawText "There are no success stories recorded "; em [] [ rawText "(yet)" ] ]
|
||||
else
|
||||
table [ _class "table table-sm table-hover" ] [
|
||||
thead [] [
|
||||
[ "Story"; "From"; "Found Here?"; "Recorded On" ]
|
||||
|> List.map (fun it -> th [ _scope "col" ] [ rawText it ])
|
||||
|> tr []
|
||||
]
|
||||
m |> List.map (fun story ->
|
||||
tr [] [
|
||||
td [] [
|
||||
let theId = SuccessId.toString story.Id
|
||||
if story.HasStory then a [ _href $"/success-story/{theId}/view" ] [ rawText "View" ]
|
||||
else em [] [ rawText "None" ]
|
||||
if story.CitizenId = citizenId then
|
||||
rawText " ~ "; a [ _href $"/success-story/{theId}/edit" ] [ rawText "Edit" ]
|
||||
]
|
||||
td [] [ str story.CitizenName ]
|
||||
td [] [ if story.FromHere then strong [] [ rawText "Yes" ] else rawText "No" ]
|
||||
td [] [ str (fullDate story.RecordedOn tz) ]
|
||||
])
|
||||
|> tbody []
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
/// The page to view a success story
|
||||
let view (it : Success) citizenName tz =
|
||||
article [] [
|
||||
h3 [] [
|
||||
str citizenName; rawText "’s Success Story"
|
||||
if it.IsFromHere then
|
||||
span [ _class "jjj-heading-label" ] [
|
||||
rawText " "
|
||||
span [ _class "badge bg-success" ] [
|
||||
rawText "Via "
|
||||
rawText (if it.Source = "profile" then "employment profile" else "job listing")
|
||||
rawText " on Jobs, Jobs, Jobs"
|
||||
]
|
||||
]
|
||||
]
|
||||
h4 [ _class "pb-3 text-muted" ] [ str (fullDateTime it.RecordedOn tz) ]
|
||||
match it.Story with Some text -> div [] [ md2html text ] | None -> ()
|
||||
]
|
Loading…
Reference in New Issue
Block a user