Success stories migrated; closing in on done
This commit is contained in:
parent
4a10f5413d
commit
2922ced971
|
@ -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)
|
||||
|
|
|
@ -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 ]
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 – Delete Your Profile</h4>
|
||||
<h4 class="pb-3">Option 1 – 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’s knowledge of you. This is what you want to
|
||||
|
@ -15,7 +15,7 @@
|
|||
|
||||
<hr>
|
||||
|
||||
<h4>Option 2 – Delete Your Account</h4>
|
||||
<h4 class="pb-3">Option 2 – 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
<p>TODO: convert this view</p>
|
||||
</template>
|
142
src/JobsJobsJobs/App/src/views/success-story/StoryEdit.vue
Normal file
142
src/JobsJobsJobs/App/src/views/success-story/StoryEdit.vue
Normal 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 “Seeking Employment” to “No” 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>
|
|
@ -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>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<article>
|
||||
<page-title title="Success Story" />
|
||||
<load-data :load="retrieveStory">
|
||||
<h3>{{citizenName}}’s Success Story</h3>
|
||||
<h3 class="pb-3">{{citizenName}}’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'
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user