Version 3 #40
|
@ -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
|
/// The parameters for a public job search
|
||||||
[<CLIMutable>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type PublicSearchForm =
|
type PublicSearchForm =
|
||||||
{ /// Retrieve citizens from this continent
|
{ /// Retrieve citizens from this continent
|
||||||
ContinentId : string
|
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
|
/// An entry in the list of success stories
|
||||||
|
[<NoComparison; NoEquality>]
|
||||||
type StoryEntry =
|
type StoryEntry =
|
||||||
{ /// The ID of this success story
|
{ /// The ID of this success story
|
||||||
Id : SuccessId
|
Id : SuccessId
|
||||||
|
|
|
@ -264,7 +264,7 @@ module Citizen =
|
||||||
let! citizen = Citizens.findById citizenId
|
let! citizen = Citizens.findById citizenId
|
||||||
let! profile = Profiles.findById citizenId
|
let! profile = Profiles.findById citizenId
|
||||||
let! prfCount = Profiles.count ()
|
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
|
// 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
|
/// Handlers for the home page, legal stuff, and help
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Home =
|
module Home =
|
||||||
|
@ -718,64 +707,74 @@ module Profile =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handlers for /api/profile routes
|
/// Handlers for /success-stor[y|ies] 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
|
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Success =
|
module Success =
|
||||||
|
|
||||||
// GET: /api/success/[id]
|
// GET: /success-story/[id]/edit
|
||||||
let get successId : HttpHandler = authorize >=> fun next ctx -> task {
|
let edit successId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
match! Successes.findById (SuccessId successId) with
|
let citizenId = currentCitizenId ctx
|
||||||
| Some story -> return! json story next 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
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: /api/success/list
|
// GET: /success-stories
|
||||||
let all : HttpHandler = authorize >=> fun next ctx -> task {
|
let list : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||||
let! stories = Successes.all ()
|
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
|
// GET: /success-story/[id]/view
|
||||||
let save : HttpHandler = authorize >=> fun next ctx -> task {
|
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 citizenId = currentCitizenId ctx
|
||||||
let! form = ctx.BindJsonAsync<StoryForm> ()
|
let! form = ctx.BindFormAsync<EditSuccessForm> ()
|
||||||
let! success = task {
|
let isNew = form.Id = ShortGuid.fromGuid Guid.Empty
|
||||||
match form.Id with
|
let! theSuccess = task {
|
||||||
| "new" ->
|
if isNew then
|
||||||
return Some { Id = SuccessId.create ()
|
return Some
|
||||||
|
{ Success.empty with
|
||||||
|
Id = SuccessId.create ()
|
||||||
CitizenId = citizenId
|
CitizenId = citizenId
|
||||||
RecordedOn = now ctx
|
RecordedOn = now ctx
|
||||||
IsFromHere = form.FromHere
|
|
||||||
Source = "profile"
|
Source = "profile"
|
||||||
Story = noneIfEmpty form.Story |> Option.map Text
|
|
||||||
}
|
}
|
||||||
| successId ->
|
else return! Successes.findById (SuccessId.ofString form.Id)
|
||||||
match! Successes.findById (SuccessId.ofString successId) with
|
}
|
||||||
|
match theSuccess with
|
||||||
| Some story when story.CitizenId = citizenId ->
|
| Some story when story.CitizenId = citizenId ->
|
||||||
return Some { story with
|
do! Successes.save
|
||||||
IsFromHere = form.FromHere
|
{ story with IsFromHere = form.FromHere; Story = noneIfEmpty form.Story |> Option.map Text }
|
||||||
Story = noneIfEmpty form.Story |> Option.map Text
|
if isNew then
|
||||||
}
|
match! Profiles.findById citizenId with
|
||||||
| Some _ | None -> return None
|
| Some profile -> do! Profiles.save { profile with IsSeekingEmployment = false }
|
||||||
}
|
| None -> ()
|
||||||
match success with
|
let extraMsg = if isNew then " and seeking employment flag cleared" else ""
|
||||||
| Some story ->
|
do! addSuccess $"Success story saved{extraMsg} successfully" ctx
|
||||||
do! Successes.save story
|
return! redirectToGet "/success-stories" next ctx
|
||||||
return! ok next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -831,6 +830,14 @@ let allEndpoints = [
|
||||||
route "/save" Profile.save
|
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 "/api" [
|
||||||
subRoute "/citizen" [
|
subRoute "/citizen" [
|
||||||
|
@ -838,17 +845,6 @@ let allEndpoints = [
|
||||||
route "/account" CitizenApi.account
|
route "/account" CitizenApi.account
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
GET_HEAD [ route "/continents" Continent.all ]
|
|
||||||
POST [ route "/markdown-preview" Api.markdownPreview ]
|
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\Home.fs" />
|
||||||
<Compile Include="Views\Listing.fs" />
|
<Compile Include="Views\Listing.fs" />
|
||||||
<Compile Include="Views\Profile.fs" />
|
<Compile Include="Views\Profile.fs" />
|
||||||
|
<Compile Include="Views\Success.fs" />
|
||||||
<Compile Include="Handlers.fs" />
|
<Compile Include="Handlers.fs" />
|
||||||
<Compile Include="App.fs" />
|
<Compile Include="App.fs" />
|
||||||
</ItemGroup>
|
</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
|
/// The form submitted to expire a listing
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type ExpireListingForm =
|
type ExpireListingForm =
|
||||||
|
|
|
@ -23,7 +23,7 @@ let confirmAccount isConfirmed =
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The citizen's dashboard page
|
/// 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" ] [
|
article [ _class "container" ] [
|
||||||
h3 [ _class "pb-4" ] [ rawText "ITM, "; str citizen.FirstName; rawText "!" ]
|
h3 [ _class "pb-4" ] [ rawText "ITM, "; str citizen.FirstName; rawText "!" ]
|
||||||
div [ _class "row row-cols-1 row-cols-md-2" ] [
|
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
|
match profile with
|
||||||
| Some prfl ->
|
| Some prfl ->
|
||||||
h6 [ _class "card-subtitle mb-3 text-muted fst-italic" ] [
|
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" ] [
|
p [ _class "card-text" ] [
|
||||||
rawText "Your profile currently lists "; str $"{List.length prfl.Skills}"
|
rawText "Your profile currently lists "; str $"{List.length prfl.Skills}"
|
||||||
|
|
|
@ -60,7 +60,7 @@ let private links ctx =
|
||||||
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
navLink "/citizen/dashboard" "view-dashboard-variant" "Dashboard"
|
||||||
navLink "/help-wanted" "newspaper-variant-multiple-outline" "Help Wanted!"
|
navLink "/help-wanted" "newspaper-variant-multiple-outline" "Help Wanted!"
|
||||||
navLink "/profile/search" "view-list-outline" "Employment Profiles"
|
navLink "/profile/search" "view-list-outline" "Employment Profiles"
|
||||||
navLink "/success-story/list" "thumb-up" "Success Stories"
|
navLink "/success-stories" "thumb-up" "Success Stories"
|
||||||
div [ _class "separator" ] []
|
div [ _class "separator" ] []
|
||||||
navLink "/citizen/account" "account-edit" "My Account"
|
navLink "/citizen/account" "account-edit" "My Account"
|
||||||
navLink "/listings/mine" "sign-text" "My Job Listings"
|
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