Success stories migrated; closing in on done

This commit is contained in:
Daniel J. Summers 2021-08-08 22:56:05 -04:00
parent 4a10f5413d
commit 2922ced971
13 changed files with 198 additions and 23 deletions

View File

@ -468,9 +468,10 @@ module Success =
.Zip()
.Merge(ReqlFunction1(fun it ->
upcast r
.HashMap("displayName",
.HashMap("citizenName",
r.Branch(it.G("realName" ).Default_("").Ne(""), it.G("realName"),
it.G("displayName").Default_("").Ne(""), it.G("displayName"),
it.G("naUser")))))
it.G("naUser")))
.With("hasStory", it.G("story").Default_("").Gt(""))))
.Pluck("id", "citizenId", "citizenName", "recordedOn", "fromHere", "hasStory")
.RunResultAsync<StoryEntry list> conn)

View File

@ -341,7 +341,7 @@ module Success =
let! success = task {
match form.id with
| "new" ->
return Some { id = (Guid.NewGuid >> SuccessId) ()
return Some { id = SuccessId.create ()
citizenId = citizenId
recordedOn = now
fromHere = form.fromHere
@ -397,7 +397,7 @@ let allEndpoints = [
]
subRoute "/success" [
GET_HEAD [
routef "/get/%O" Success.get
routef "/%O" Success.get
route "/list" Success.all
]
POST [ route "/save" Success.save ]

View File

@ -12,6 +12,7 @@ import {
PublicSearch,
PublicSearchResult,
StoryEntry,
StoryForm,
Success
} from './types'
@ -137,6 +138,18 @@ export default {
/** API functions for profiles */
profile: {
/**
* Clear the "seeking employment" flag on the current citizen's profile
*
* @param user The currently logged-on user
* @returns True if the action was successful, or an error string if not
*/
markEmploymentFound: async (user : LogOnSuccess) : Promise<boolean | string> => {
const result = await fetch(apiUrl('profile/employment-found'), reqInit('PATCH', user))
if (result.ok) return true
return `${result.status} - ${result.statusText} (${await result.text()})`
},
/**
* Search for public profile data using the given parameters
*
@ -183,6 +196,7 @@ export default {
*
* @param data The profile data to be saved
* @param user The currently logged-on user
* @returns True if the save was successful, an error string if not
*/
save: async (data : ProfileForm, user : LogOnSuccess) : Promise<boolean | string> =>
apiSend(await fetch(apiUrl('profile/save'), reqInit('POST', user, data)), 'saving profile'),
@ -249,7 +263,17 @@ export default {
* @returns The success story, or an error
*/
retrieve: async (id : string, user : LogOnSuccess) : Promise<Success | string | undefined> =>
apiResult<Success>(await fetch(apiUrl(`success/${id}`), reqInit('GET', user)), `retrieving success story ${id}`)
apiResult<Success>(await fetch(apiUrl(`success/${id}`), reqInit('GET', user)), `retrieving success story ${id}`),
/**
* Save a success story
*
* @param data The data to be saved
* @param user The currently logged-on user
* @returns True if successful, an error string if not
*/
save: async (data : StoryForm, user : LogOnSuccess) : Promise<boolean | string> =>
apiSend(await fetch(apiUrl('success/save'), reqInit('POST', user, data)), 'saving success story')
}
}

View File

@ -179,6 +179,16 @@ export interface StoryEntry {
hasStory : boolean
}
/** The data required to provide a success story */
export class StoryForm {
/** The ID of this story */
id = ''
/** Whether the employment was obtained from Jobs, Jobs, Jobs */
fromHere = false
/** The success story */
story = ''
}
/** A record of success finding employment */
export interface Success {
/** The ID of the success report */

View File

@ -106,9 +106,9 @@ const routes: Array<RouteRecordRaw> = [
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryList.vue')
},
{
path: '/success-story/add',
name: 'AddStory',
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryAdd.vue')
path: '/success-story/edit/:id',
name: 'EditStory',
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryEdit.vue')
},
{
path: '/success-story/view/:id',

View File

@ -17,8 +17,8 @@
<input type="checkbox" id="isSeeking" class="form-check-input" v-model="v$.isSeekingEmployment.$model">
<label for="isSeeking" class="form-check-label">I am currently seeking employment</label>
</div>
<p v-if="profile?.seekingEmployment">
<em>If you have found employment, consider <router-link to="/success-story/add">telling your fellow
<p v-if="profile.isSeekingEmployment">
<em>If you have found employment, consider <router-link to="/success-story/edit/new">telling your fellow
citizens about it!</router-link></em>
</p>
</div>

View File

@ -3,14 +3,14 @@
<page-title :title="pageTitle" />
<load-data :load="retrieveProfile">
<h2><a :href="it.citizen.profileUrl" target="_blank">{{citizenName}}</a></h2>
<h4>{{it.continent.name}}, {{it.profile.region}}</h4>
<h4 class="pb-3">{{it.continent.name}}, {{it.profile.region}}</h4>
<p v-html="workTypes"></p>
<hr>
<div v-html="bioHtml"></div>
<template v-if="it.profile.skills.length > 0">
<hr>
<h4>Skills</h4>
<h4 class="pb-3">Skills</h4>
<ul>
<li v-for="(skill, idx) in it.profile.skills" :key="idx">
{{skill.description}}<template v-if="skill.notes"> ({{skill.notes}})</template>
@ -20,7 +20,7 @@
<template v-if="it.profile.experience">
<hr>
<h4>Experience / Employment History</h4>
<h4 class="pb-3">Experience / Employment History</h4>
<div v-html="expHtml"></div>
</template>

View File

@ -1,9 +1,9 @@
<template>
<article>
<page-title title="Account Deletion Options" />
<h3>Account Deletion Options</h3>
<h3 class="pb-3">Account Deletion Options</h3>
<h4>Option 1 &ndash; Delete Your Profile</h4>
<h4 class="pb-3">Option 1 &ndash; Delete Your Profile</h4>
<p>
Utilizing this option will remove your current employment profile and skills. This will preserve any success
stories you may have written, and preserves this application&rsquo;s knowledge of you. This is what you want to
@ -15,7 +15,7 @@
<hr>
<h4>Option 2 &ndash; Delete Your Account</h4>
<h4 class="pb-3">Option 2 &ndash; Delete Your Account</h4>
<p>
This option will make it like you never visited this site. It will delete your profile, skills, success stories,
and account. This is what you want to use if you want to disappear from this application. Clicking the button

View File

@ -1,7 +1,7 @@
<template>
<article>
<page-title title="Account Deletion Success" />
<h3>Account Deletion Success</h3>
<h3 class="pb-3">Account Deletion Success</h3>
<p>
Your account has been successfully deleted. To revoke the permissions you have previously granted to this
application, find it in <a href="https://noagendasocial.com/oauth/authorized_applications">this list</a> and click

View File

@ -1,3 +0,0 @@
<template>
<p>TODO: convert this view</p>
</template>

View File

@ -0,0 +1,142 @@
<template>
<article>
<page-title :title="title" />
<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 for="fromHere" class="form-check-label">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 type="submit" class="btn btn-primary" @click.prevent="saveStory(true)"><icon icon="" /> Save</button>
<p v-if="isNew">
<em>(Saving this will set &ldquo;Seeking Employment&rdquo; to &ldquo;No&rdquo; on your profile.)</em>
</p>
</div>
</form>
</load-data>
</article>
</template>
<script lang="ts">
import { computed, defineComponent, reactive } from 'vue'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import useVuelidate from '@vuelidate/core'
import api, { LogOnSuccess, StoryForm } from '@/api'
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
import { useStore } from '@/store'
import LoadData from '@/components/LoadData.vue'
import MarkdownEditor from '@/components/MarkdownEditor.vue'
export default defineComponent({
name: 'StoryEdit',
components: {
LoadData,
MarkdownEditor
},
setup () {
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 page title */
const title = computed(() => isNew.value ? 'Tell Your Success Story' : 'Edit Success Story')
/** 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'
} 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')
if (navigate) {
router.push('/success-story/list')
v$.value.$reset()
}
}
} else {
toastSuccess('Success Story saved successfully')
if (navigate) {
router.push('/success-story/list')
v$.value.$reset()
}
}
}
}
/** Prompt for save if the user navigates away with unsaved changes */
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
if (!v$.value.$anyDirty) return true
if (confirm('There are unsaved changes; save before leaving?')) {
await saveStory(false)
return true
}
return false
})
return {
title,
isNew,
retrieveStory,
v$,
saveStory
}
}
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<article>
<page-title title="Success Stories" />
<h3>Success Stories</h3>
<h3 class="pb-3">Success Stories</h3>
<load-data :load="retrieveStories">
<table v-if="stories?.length > 0" class="table table-sm table-hover">
<thead>

View File

@ -2,7 +2,7 @@
<article>
<page-title title="Success Story" />
<load-data :load="retrieveStory">
<h3>{{citizenName}}&rsquo;s Success Story</h3>
<h3 class="pb-3">{{citizenName}}&rsquo;s Success Story</h3>
<h4 class="text-muted"><full-date-time :date="story.recordedOn" /></h4>
<p v-if="story.fromHere" class="fst-italic"><strong>Found via Jobs, Jobs, Jobs</strong></p>
<hr>
@ -17,6 +17,7 @@ import { useRoute } from 'vue-router'
import marked from 'marked'
import api, { LogOnSuccess, markedOptions, Success } from '@/api'
import { useStore } from '@/store'
import FullDateTime from '@/components/FullDateTime.vue'
import LoadData from '@/components/LoadData.vue'