WIP on validate/save profile
This commit is contained in:
parent
058c5e5b77
commit
bd97a3828f
21
src/JobsJobsJobs/App/package-lock.json
generated
21
src/JobsJobsJobs/App/package-lock.json
generated
|
@ -2589,6 +2589,22 @@
|
|||
"integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==",
|
||||
"dev": true
|
||||
},
|
||||
"@vuelidate/core": {
|
||||
"version": "2.0.0-alpha.22",
|
||||
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.0-alpha.22.tgz",
|
||||
"integrity": "sha512-HqRWY2c5pW6kxHNCupYyQn1+frjiFjEN6qgP0AwByvSu/JdKHDWlWqC2gJoxPiT3Oy23ulge43jVzHT3UO1r4A==",
|
||||
"requires": {
|
||||
"vue-demi": "^0.9.1"
|
||||
}
|
||||
},
|
||||
"@vuelidate/validators": {
|
||||
"version": "2.0.0-alpha.19",
|
||||
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.0-alpha.19.tgz",
|
||||
"integrity": "sha512-5TQxi1Wa6jFPYZ4UIUI8RKR2nqN/yujPxRjUMh5vCOhVXoqXPMfoYdfs5DBtfABhmjj7LTtXrZ78WVMQkL9tNw==",
|
||||
"requires": {
|
||||
"vue-demi": "^0.9.1"
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||
|
@ -12853,6 +12869,11 @@
|
|||
"@vue/shared": "3.1.4"
|
||||
}
|
||||
},
|
||||
"vue-demi": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.9.1.tgz",
|
||||
"integrity": "sha512-7s1lufRD2l369eFWPjgLvhqCRk0XzGWJsQc7K4q+0mZtixyGIvsK1Cg88P4NcaRIEiBuuN4q1NN4SZKFKwQswA=="
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.8.0.tgz",
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "5.9.55",
|
||||
"@vuelidate/core": "^2.0.0-alpha.22",
|
||||
"@vuelidate/validators": "^2.0.0-alpha.19",
|
||||
"bootstrap": "^5.1.0",
|
||||
"core-js": "^3.6.5",
|
||||
"date-fns": "^2.23.0",
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
Count,
|
||||
LogOnSuccess,
|
||||
Profile,
|
||||
ProfileForm,
|
||||
ProfileForView,
|
||||
ProfileSearch,
|
||||
ProfileSearchResult,
|
||||
|
@ -28,13 +29,22 @@ const apiUrl = (url : string) : string => `http://localhost:5000/api/${url}`
|
|||
* @param user The currently logged-on user
|
||||
* @returns RequestInit parameters
|
||||
*/
|
||||
const reqInit = (method : string, user : LogOnSuccess) : RequestInit => {
|
||||
// eslint-disable-next-line
|
||||
const reqInit = (method : string, user : LogOnSuccess, body : any | undefined = undefined) : RequestInit => {
|
||||
const headers = new Headers()
|
||||
headers.append('Authorization', `Bearer ${user.jwt}`)
|
||||
if (body) {
|
||||
headers.append('Content-Type', 'application/json')
|
||||
return {
|
||||
headers,
|
||||
method,
|
||||
cache: 'no-cache',
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
return {
|
||||
headers,
|
||||
method
|
||||
// mode: 'cors'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +61,18 @@ async function apiResult<T> (resp : Response, action : string) : Promise<T | und
|
|||
return `Error ${action} - ${await resp.text()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an update via the API
|
||||
*
|
||||
* @param resp The response received from the API
|
||||
* @param action The action being performed (used in error messages)
|
||||
* @returns True (if the response is a success) or an error string
|
||||
*/
|
||||
async function apiSend (resp : Response, action : string) : Promise<boolean | string> {
|
||||
if (resp.status === 200) return true
|
||||
return `Error ${action} - (${resp.status}) ${await resp.text()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an API action that does not return a result
|
||||
*
|
||||
|
@ -156,6 +178,15 @@ export default {
|
|||
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ProfileForView | string | undefined> =>
|
||||
apiResult<ProfileForView>(await fetch(apiUrl(`profile/view/${id}`), reqInit('GET', user)), 'retrieving profile'),
|
||||
|
||||
/**
|
||||
* Save a user's profile data
|
||||
*
|
||||
* @param data The profile data to be saved
|
||||
* @param user The currently logged-on user
|
||||
*/
|
||||
save: async (data : ProfileForm, user : LogOnSuccess) : Promise<boolean | string> =>
|
||||
apiSend(await fetch(apiUrl('profile/save'), reqInit('POST', user, data)), 'saving profile'),
|
||||
|
||||
/**
|
||||
* Search for profiles using the given parameters
|
||||
*
|
||||
|
|
|
@ -78,27 +78,27 @@ export interface Profile {
|
|||
}
|
||||
|
||||
/** The data required to update a profile */
|
||||
export interface ProfileForm {
|
||||
export class ProfileForm {
|
||||
/** Whether the citizen to whom this profile belongs is actively seeking employment */
|
||||
isSeekingEmployment : boolean
|
||||
isSeekingEmployment = false
|
||||
/** Whether this profile should appear in the public search */
|
||||
isPublic : boolean
|
||||
isPublic = false
|
||||
/** The user's real name */
|
||||
realName : string
|
||||
realName = ''
|
||||
/** The ID of the continent on which the citizen is located */
|
||||
continentId : string
|
||||
continentId = ''
|
||||
/** The area within that continent where the citizen is located */
|
||||
region : string
|
||||
region = ''
|
||||
/** If the citizen is available for remote work */
|
||||
remoteWork : boolean
|
||||
remoteWork = false
|
||||
/** If the citizen is seeking full-time employment */
|
||||
fullTime : boolean
|
||||
fullTime = false
|
||||
/** The user's professional biography */
|
||||
biography : string
|
||||
biography = ''
|
||||
/** The user's past experience */
|
||||
experience : string | undefined
|
||||
/** The skills for the user */
|
||||
skills : Skill[]
|
||||
skills : Skill[] = []
|
||||
}
|
||||
|
||||
/** The data required to show a viewable profile */
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div class="row pb-3">
|
||||
<div class="col col-xs-12">
|
||||
<div class="col-12">
|
||||
<nav class="nav nav-pills pb-1">
|
||||
<button :class="sourceClass" @click.prevent="showMarkdown">Markdown</button>
|
||||
<button :class="previewClass" @click.prevent="showPreview">Preview</button>
|
||||
|
@ -8,12 +7,12 @@
|
|||
<section v-if="preview" class="preview" v-html="previewHtml">
|
||||
</section>
|
||||
<div v-else class="form-floating">
|
||||
<textarea :id="id" class="form-control md-edit" rows="10" v-text="text"
|
||||
@input="$emit('update:text', $event.target.value)"></textarea>
|
||||
<textarea :id="id" :class="{ 'form-control': true, 'md-edit': true, 'is-invalid': isInvalid }" rows="10"
|
||||
v-text="text" @input="$emit('update:text', $event.target.value)"></textarea>
|
||||
<div class="invalid-feedback">Please enter some text for {{label}}</div>
|
||||
<label :for="id">{{label}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -35,7 +34,8 @@ export default defineComponent({
|
|||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
isInvalid: { type: Boolean }
|
||||
},
|
||||
emits: ['update:text'],
|
||||
setup (props) {
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
<page-title title="Edit Profile" />
|
||||
<h3>Employment Profile</h3>
|
||||
<load-data :load="retrieveData">
|
||||
<form>
|
||||
<div class="row pb-3">
|
||||
<div class="col col-xs-12 col-sm-10 col-md-8 col-lg-6">
|
||||
<form class="row g-3">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" id="realName" class="form-control" v-model="profile.realName" maxlength="255"
|
||||
placeholder="Leave blank to use your NAS display name">
|
||||
|
@ -13,9 +12,7 @@
|
|||
</div>
|
||||
<div class="form-text">Leave blank to use your NAS display name</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col col-xs-12">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" v-model="profile.seekingEmployment">
|
||||
<label class="form-check-label">I am currently seeking employment</label>
|
||||
|
@ -25,47 +22,49 @@
|
|||
citizens about it!</router-link></em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="col-12 col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select id="continentId" class="form-select" :value="profile.continentId">
|
||||
<select id="continentId" :class="{ 'form-select': true, 'is-invalid': v$.continentId.$error }"
|
||||
:value="v$.continentId.$model">
|
||||
<option v-for="c in continents" :key="c.id" :value="c.id">{{c.name}}</option>
|
||||
</select>
|
||||
<label for="continentId" class="jjj-required">Continent</label>
|
||||
</div>
|
||||
<div class="invalid-feedback">Please select a continent</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-6 col-md-8">
|
||||
<div class="col-12 col-sm-6 col-md-8">
|
||||
<div class="form-floating">
|
||||
<input type="text" id="region" class="form-control" v-model="profile.region" maxlength="255"
|
||||
placeholder="Country, state, geographic area, etc.">
|
||||
<input type="text" id="region" :class="{ 'form-control': true, 'is-invalid': v$.region.$error }"
|
||||
v-model="v$.region.$model" maxlength="255" placeholder="Country, state, geographic area, etc.">
|
||||
<div id="regionFeedback" class="invalid-feedback">Please enter a region</div>
|
||||
<label for="region" class="jjj-required">Region</label>
|
||||
</div>
|
||||
<div class="form-text">Country, state, geographic area, etc.</div>
|
||||
</div>
|
||||
</div>
|
||||
<markdown-editor id="bio" label="Professional Biography" v-model:text="profile.biography" />
|
||||
<div class="row pb-3">
|
||||
<div class="col col-xs-12 col-offset-md-2 col-md-4">
|
||||
<markdown-editor id="bio" label="Professional Biography" v-model:text="profile.biography"
|
||||
:isInvalid="v$.biography.$error" />
|
||||
<div class="col-12 col-offset-md-2 col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="isRemote" class="form-check-input" v-model="profile.remoteWork">
|
||||
<label class="form-check-label" for="isRemote">I am looking for remote work</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-md-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="isFullTime" class="form-check-input" v-model="profile.fullTime">
|
||||
<label class="form-check-label" for="isFullTime">I am looking for full-time work</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<hr>
|
||||
<h4 class="pb-2">
|
||||
Skills
|
||||
<button class="btn btn-sm btn-outline-primary rounded-pill" @click.prevent="addSkill">Add a Skill</button>
|
||||
</h4>
|
||||
</div>
|
||||
<profile-skill-edit v-for="(skill, idx) in profile.skills" :key="skill.id" v-model="profile.skills[idx]"
|
||||
@remove="removeSkill(skill.id)" />
|
||||
<div class="col-12">
|
||||
<hr>
|
||||
<h4>Experience</h4>
|
||||
<p>
|
||||
|
@ -73,9 +72,9 @@
|
|||
use this area to list prior jobs, their dates, and anything else you want to include that’s not
|
||||
already a part of your Professional Biography above.
|
||||
</p>
|
||||
</div>
|
||||
<markdown-editor id="experience" label="Experience" v-model:text="profile.experience" />
|
||||
<div class="row pb-3">
|
||||
<div class="col col-xs-12">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="isPublic" class="form-check-input" v-model="profile.isPublic">
|
||||
<label class="form-check-label" for="isPublic">
|
||||
|
@ -83,10 +82,9 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-3">
|
||||
<div class="col col-xs-12">
|
||||
<button class="btn btn-primary">Save</button>
|
||||
<div class="col-12">
|
||||
<p v-if="v$.$error" class="text-danger">Please correct the errors above</p>
|
||||
<button class="btn btn-primary" @click.prevent="saveProfile">Save</button>
|
||||
<template v-if="!isNew">
|
||||
|
||||
<button class="btn btn-outline-secondary" @click.prevent="viewProfile">
|
||||
|
@ -94,7 +92,6 @@
|
|||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</load-data>
|
||||
<hr>
|
||||
|
@ -106,13 +103,16 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, Ref, ref } from 'vue'
|
||||
import { computed, defineComponent, ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required } from '@vuelidate/validators'
|
||||
import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from '@/api'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
import LoadData from '@/components/LoadData.vue'
|
||||
import MarkdownEditor from '@/components/MarkdownEditor.vue'
|
||||
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
|
||||
import ProfileSkillEdit from '@/components/profile/SkillEdit.vue'
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -148,7 +148,17 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
/** The user's current profile (plus a few items, adapted for editing) */
|
||||
const profile : Ref<ProfileForm | undefined> = ref(undefined)
|
||||
const profile = reactive(new ProfileForm())
|
||||
|
||||
/** The validation rules for the form */
|
||||
const rules = {
|
||||
continentId: { required },
|
||||
region: { required },
|
||||
biography: { required }
|
||||
}
|
||||
|
||||
/** Initialize form validation */
|
||||
const v$ = useVuelidate(rules, profile /*, { $lazy: true } */)
|
||||
|
||||
/** Retrieve the user's profile and their real name */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
|
@ -164,45 +174,55 @@ export default defineComponent({
|
|||
errors.push(nameResult)
|
||||
}
|
||||
if (errors.length > 0) return
|
||||
// Update the empty form with appropriate values
|
||||
const p = isNew.value ? newProfile : profileResult as Profile
|
||||
profile.value = {
|
||||
isSeekingEmployment: p.seekingEmployment,
|
||||
isPublic: p.isPublic,
|
||||
continentId: p.continentId,
|
||||
region: p.region,
|
||||
remoteWork: p.remoteWork,
|
||||
fullTime: p.fullTime,
|
||||
biography: p.biography,
|
||||
experience: p.experience,
|
||||
skills: p.skills,
|
||||
realName: typeof nameResult !== 'undefined' ? (nameResult as Citizen).realName || '' : ''
|
||||
}
|
||||
profile.isSeekingEmployment = p.seekingEmployment
|
||||
profile.isPublic = p.isPublic
|
||||
profile.continentId = p.continentId
|
||||
profile.region = p.region
|
||||
profile.remoteWork = p.remoteWork
|
||||
profile.fullTime = p.fullTime
|
||||
profile.biography = p.biography
|
||||
profile.experience = p.experience
|
||||
profile.skills = p.skills
|
||||
profile.realName = typeof nameResult !== 'undefined' ? (nameResult as Citizen).realName || '' : ''
|
||||
}
|
||||
|
||||
/** The ID for new skills */
|
||||
let newSkillId = 0
|
||||
|
||||
/** Add a skill to the profile */
|
||||
const addSkill = () => {
|
||||
const form = profile.value as ProfileForm
|
||||
form.skills.push({ id: `new${newSkillId}`, description: '', notes: undefined })
|
||||
newSkillId++
|
||||
profile.value = form
|
||||
}
|
||||
const addSkill = () => { profile.skills.push({ id: `new${newSkillId++}`, description: '', notes: undefined }) }
|
||||
|
||||
/** Remove the given skill from the profile */
|
||||
const removeSkill = (skillId : string) => {
|
||||
const form = profile.value as ProfileForm
|
||||
form.skills = form.skills.filter(s => s.id !== skillId)
|
||||
profile.value = form
|
||||
}
|
||||
const removeSkill = (skillId : string) => { profile.skills = profile.skills.filter(s => s.id !== skillId) }
|
||||
|
||||
/** Save the current profile values */
|
||||
const saveProfile = async () => {
|
||||
// TODO
|
||||
v$.value.$touch()
|
||||
if (v$.value.$error) return
|
||||
// Remove any blank skills before submitting
|
||||
profile.skills = profile.skills.filter(s => !(s.description.trim() === '' && (s.notes || '').trim() === ''))
|
||||
const saveResult = await api.profile.save(profile, user)
|
||||
if (typeof saveResult === 'string') {
|
||||
toastError(saveResult, 'saving profile')
|
||||
} else {
|
||||
toastSuccess('Profile Saved Successfuly')
|
||||
v$.value.$reset()
|
||||
}
|
||||
}
|
||||
|
||||
/** View the profile, prompting for save if data has changed */
|
||||
const viewProfile = async () => {
|
||||
alert(v$.value.$dirty)
|
||||
if (v$.value.$dirty && confirm('There are unsaved changes; save before viewing?')) {
|
||||
await saveProfile()
|
||||
router.push(`/profile/view/${user.citizenId}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
v$,
|
||||
retrieveData,
|
||||
user,
|
||||
isNew,
|
||||
|
@ -211,7 +231,7 @@ export default defineComponent({
|
|||
addSkill,
|
||||
removeSkill,
|
||||
saveProfile,
|
||||
viewProfile: () => router.push(`/profile/view/${user.citizenId}`)
|
||||
viewProfile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue
Block a user