WIP on edit profile
Also many infrastructure / placeholder additions/changes
This commit is contained in:
parent
d36b01cae5
commit
e8696e0e94
|
@ -22,6 +22,7 @@ let configureApp (app : IApplicationBuilder) =
|
||||||
e.MapFallbackToFile "index.html" |> ignore)
|
e.MapFallbackToFile "index.html" |> ignore)
|
||||||
|> ignore
|
|> ignore
|
||||||
|
|
||||||
|
open Newtonsoft.Json
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open Microsoft.AspNetCore.Authentication.JwtBearer
|
open Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
open Microsoft.Extensions.Configuration
|
open Microsoft.Extensions.Configuration
|
||||||
|
@ -36,6 +37,10 @@ let configureServices (svc : IServiceCollection) =
|
||||||
svc.AddLogging () |> ignore
|
svc.AddLogging () |> ignore
|
||||||
svc.AddCors () |> ignore
|
svc.AddCors () |> ignore
|
||||||
|
|
||||||
|
let jsonCfg = JsonSerializerSettings ()
|
||||||
|
Data.Converters.all () |> List.iter jsonCfg.Converters.Add
|
||||||
|
svc.AddSingleton<Json.ISerializer> (NewtonsoftJson.Serializer jsonCfg) |> ignore
|
||||||
|
|
||||||
let svcs = svc.BuildServiceProvider ()
|
let svcs = svc.BuildServiceProvider ()
|
||||||
let cfg = svcs.GetRequiredService<IConfiguration> ()
|
let cfg = svcs.GetRequiredService<IConfiguration> ()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Count, LogOnSuccess, Profile } from './types'
|
import { Citizen, Continent, Count, LogOnSuccess, Profile } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a URL that will access the API
|
* Create a URL that will access the API
|
||||||
|
@ -39,6 +39,46 @@ export default {
|
||||||
const resp = await fetch(apiUrl(`citizen/log-on/${code}`), { method: 'GET', mode: 'cors' })
|
const resp = await fetch(apiUrl(`citizen/log-on/${code}`), { method: 'GET', mode: 'cors' })
|
||||||
if (resp.status === 200) return await resp.json() as LogOnSuccess
|
if (resp.status === 200) return await resp.json() as LogOnSuccess
|
||||||
return `Error logging on - ${await resp.text()}`
|
return `Error logging on - ${await resp.text()}`
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a citizen by their ID
|
||||||
|
*
|
||||||
|
* @param id The citizen ID to be retrieved
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns The citizen, or an error
|
||||||
|
*/
|
||||||
|
retrieve: async (id : string, user : LogOnSuccess) : Promise<Citizen | string> => {
|
||||||
|
const resp = await fetch(apiUrl(`citizen/get/${id}`), reqInit('GET', user))
|
||||||
|
if (resp.status === 200) return await resp.json() as Citizen
|
||||||
|
return `Error retrieving citizen ${id} - ${await resp.text()}`
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the current citizen's entire Jobs, Jobs, Jobs record
|
||||||
|
*
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns Undefined if successful, an error if not
|
||||||
|
*/
|
||||||
|
delete: async (user : LogOnSuccess) : Promise<string | undefined> => {
|
||||||
|
const resp = await fetch(apiUrl('citizen'), reqInit('DELETE', user))
|
||||||
|
if (resp.status === 200) return undefined
|
||||||
|
return `Error deleting citizen - ${await resp.text()}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** API functions for continents */
|
||||||
|
continent: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all continents
|
||||||
|
*
|
||||||
|
* @returns All continents, or an error
|
||||||
|
*/
|
||||||
|
all: async () : Promise<Continent[] | string> => {
|
||||||
|
const resp = await fetch(apiUrl('continent/all'), { method: 'GET' })
|
||||||
|
if (resp.status === 200) return await resp.json() as Continent[]
|
||||||
|
return `Error retrieving continents - ${await resp.text()}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -71,7 +111,19 @@ export default {
|
||||||
const result = await resp.json() as Count
|
const result = await resp.json() as Count
|
||||||
return result.count
|
return result.count
|
||||||
}
|
}
|
||||||
return `Error counting profiles = ${await resp.text()}`
|
return `Error counting profiles - ${await resp.text()}`
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the current user's employment profile
|
||||||
|
*
|
||||||
|
* @param user The currently logged-on user
|
||||||
|
* @returns Undefined if successful, an error if not
|
||||||
|
*/
|
||||||
|
delete: async (user : LogOnSuccess) : Promise<string | undefined> => {
|
||||||
|
const resp = await fetch(apiUrl('profile'), reqInit('DELETE', user))
|
||||||
|
if (resp.status === 200) return undefined
|
||||||
|
return `Error deleting profile - ${await resp.text()}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,30 @@
|
||||||
|
|
||||||
|
/** A user of Jobs, Jobs, Jobs */
|
||||||
|
export interface Citizen {
|
||||||
|
/** The ID of the user */
|
||||||
|
id : string
|
||||||
|
/** The handle by which the user is known on Mastodon */
|
||||||
|
naUser : string
|
||||||
|
/** The user's display name from Mastodon (updated every login) */
|
||||||
|
displayName : string | undefined
|
||||||
|
/** The user's real name */
|
||||||
|
realName : string | undefined
|
||||||
|
/** The URL for the user's Mastodon profile */
|
||||||
|
profileUrl : string
|
||||||
|
/** When the user joined Jobs, Jobs, Jobs */
|
||||||
|
joinedOn : number
|
||||||
|
/** When the user last logged in */
|
||||||
|
lastSeenOn : number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A continent */
|
||||||
|
export interface Continent {
|
||||||
|
/** The ID of the continent */
|
||||||
|
id : string
|
||||||
|
/** The name of the continent */
|
||||||
|
name : string
|
||||||
|
}
|
||||||
|
|
||||||
/** A successful logon */
|
/** A successful logon */
|
||||||
export interface LogOnSuccess {
|
export interface LogOnSuccess {
|
||||||
/** The JSON Web Token (JWT) to use for API access */
|
/** The JSON Web Token (JWT) to use for API access */
|
||||||
|
|
52
src/JobsJobsJobs/App/src/components/MarkdownEditor.vue
Normal file
52
src/JobsJobsJobs/App/src/components/MarkdownEditor.vue
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<nav class="nav nav-pills">
|
||||||
|
<a href="#" class="nav-link @MarkdownClass" @click.prevent="showMarkdown">Markdown</a>
|
||||||
|
<a href="#" class="nav-link @PreviewClass" @click.prevent="showPreview">Preview</a>
|
||||||
|
</nav>
|
||||||
|
<section v-if="preview" class="preview" v-html="previewHtml">
|
||||||
|
</section>
|
||||||
|
<textarea v-else :id="id" class="form-control" rows="10" v-text="text"
|
||||||
|
@input="$emit('update:text', $event.target.value)"></textarea>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MarkdownEditor',
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:text'],
|
||||||
|
setup (props) {
|
||||||
|
/** Whether to show the Markdown preview */
|
||||||
|
const preview = ref(false)
|
||||||
|
|
||||||
|
/** The HTML rendered for preview purposes */
|
||||||
|
const previewHtml = ref('')
|
||||||
|
|
||||||
|
/** Show the Markdown source */
|
||||||
|
const showMarkdown = () => { preview.value = false }
|
||||||
|
|
||||||
|
/** Show the Markdown preview */
|
||||||
|
const showPreview = () => {
|
||||||
|
// TODO: render markdown as HTML
|
||||||
|
previewHtml.value = props.text
|
||||||
|
preview.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
preview,
|
||||||
|
previewHtml,
|
||||||
|
showMarkdown,
|
||||||
|
showPreview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
48
src/JobsJobsJobs/App/src/components/shared/LoadData.vue
Normal file
48
src/JobsJobsJobs/App/src/components/shared/LoadData.vue
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="loading">Loading…</div>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="errors.length > 0">
|
||||||
|
<p v-for="(error, idx) in errors" :key="idx">{{error}}</p>
|
||||||
|
</div>
|
||||||
|
<slot v-else></slot>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted, ref } from 'vue'
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LoadData',
|
||||||
|
props: {
|
||||||
|
load: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
|
/** Type the input function */
|
||||||
|
const func = props.load as (errors: string[]) => Promise<unknown>
|
||||||
|
|
||||||
|
/** Errors encountered during loading */
|
||||||
|
const errors : string[] = []
|
||||||
|
|
||||||
|
/** Whether we are currently loading data */
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
/** Call the data load function */
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
await func(errors)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
|
@ -17,7 +17,42 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/citizen/profile',
|
path: '/citizen/profile',
|
||||||
component: () => import(/* webpackChurchName: "profedit" */ '../views/citizen/EditProfile.vue')
|
component: () => import(/* webpackChunkName: "profedit" */ '../views/citizen/EditProfile.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/citizen/log-off',
|
||||||
|
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue')
|
||||||
|
},
|
||||||
|
// Profile URLs
|
||||||
|
{
|
||||||
|
path: '/profile/view/:id',
|
||||||
|
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile/search',
|
||||||
|
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileSearch.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile/seeking',
|
||||||
|
component: () => import(/* webpackChunkName: "seeking" */ '../views/profile/Seeking.vue')
|
||||||
|
},
|
||||||
|
// "So Long" URLs
|
||||||
|
{
|
||||||
|
path: '/so-long/options',
|
||||||
|
component: () => import(/* webpackChunkName: "so-long" */ '../views/so-long/DeletionOptions.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/so-long/success',
|
||||||
|
component: () => import(/* webpackChunkName: "so-long" */ '../views/so-long/DeletionSuccess.vue')
|
||||||
|
},
|
||||||
|
// Success Story URLs
|
||||||
|
{
|
||||||
|
path: '/success-story/list',
|
||||||
|
component: () => import(/* webpackChunkName: "succview" */ '../views/success-story/StoryList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/success-story/add',
|
||||||
|
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryAdd.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { InjectionKey } from 'vue'
|
import { InjectionKey } from 'vue'
|
||||||
import { createStore, Store, useStore as baseUseStore } from 'vuex'
|
import { createStore, Store, useStore as baseUseStore } from 'vuex'
|
||||||
import api, { LogOnSuccess } from '../api'
|
import api, { Continent, LogOnSuccess } from '../api'
|
||||||
|
|
||||||
/** The state tracked by the application */
|
/** The state tracked by the application */
|
||||||
export interface State {
|
export interface State {
|
||||||
user: LogOnSuccess | undefined
|
user: LogOnSuccess | undefined
|
||||||
logOnState: string
|
logOnState: string
|
||||||
|
continents: Continent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An injection key to identify this state with Vue */
|
/** An injection key to identify this state with Vue */
|
||||||
|
@ -20,15 +21,22 @@ export default createStore({
|
||||||
state: () : State => {
|
state: () : State => {
|
||||||
return {
|
return {
|
||||||
user: undefined,
|
user: undefined,
|
||||||
logOnState: 'Logging you on with No Agenda Social...'
|
logOnState: 'Logging you on with No Agenda Social...',
|
||||||
|
continents: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setUser (state, user: LogOnSuccess) {
|
setUser (state, user: LogOnSuccess) {
|
||||||
state.user = user
|
state.user = user
|
||||||
},
|
},
|
||||||
|
clearUser (state) {
|
||||||
|
state.user = undefined
|
||||||
|
},
|
||||||
setLogOnState (state, message) {
|
setLogOnState (state, message) {
|
||||||
state.logOnState = message
|
state.logOnState = message
|
||||||
|
},
|
||||||
|
setContinents (state, continents : Continent[]) {
|
||||||
|
state.continents = continents
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -39,6 +47,15 @@ export default createStore({
|
||||||
} else {
|
} else {
|
||||||
commit('setUser', logOnResult)
|
commit('setUser', logOnResult)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async ensureContinents ({ state, commit }) {
|
||||||
|
if (state.continents.length > 0) return
|
||||||
|
const theSeven = await api.continent.all()
|
||||||
|
if (typeof theSeven === 'string') {
|
||||||
|
console.error(theSeven)
|
||||||
|
} else {
|
||||||
|
commit('setContinents', theSeven)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<h3>Welcome, {{user.name}}</h3>
|
<h3>Welcome, {{user.name}}</h3>
|
||||||
|
<load-data :load="retrieveData">
|
||||||
<template v-if="profile">
|
<template v-if="profile">
|
||||||
|
<p>
|
||||||
|
Your employment profile was last updated {{profile.lastUpdatedOn}}. Your profile currently lists
|
||||||
|
{{profile.skills.length}} skill<span v-if="profile.skills.length !== 1">s</span>.
|
||||||
|
</p>
|
||||||
|
<p><router-link :to="'/profile/view/' + user.citizenId">View Your Employment Profile</router-link></p>
|
||||||
|
<p v-if="profile.seekingEmployment">
|
||||||
|
Your profile indicates that you are seeking employment. Once you find it,
|
||||||
|
<router-link to="/success-story/add">tell your fellow citizens about it!</router-link>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p>
|
||||||
|
You do not have an employment profile established; click “Edit Profile” in the menu to get
|
||||||
|
started!
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
Your employment profile was last updated {{profile.lastUpdatedOn}}. Your profile currently lists
|
There <span v-if="profileCount === 1">is</span><span v-else>are</span> <span v-if="profileCount === 0">no</span><span v-else>{{profileCount}}</span>
|
||||||
{{profile.skills.length}} skill<span v-if="profile.skills.length !== 1">s</span>.
|
employment profile<span v-if="profileCount !== 1">s</span> from citizens of Gitmo Nation.
|
||||||
|
<span v-if="profileCount > 0">Take a look around and see if you can help them find work!</span>
|
||||||
</p>
|
</p>
|
||||||
<p><router-link :to="'/profile/view/' + user.citizenId">View Your Employment Profile</router-link></p>
|
</load-data>
|
||||||
<p v-if="profile.seekingEmployment">
|
|
||||||
Your profile indicates that you are seeking employment. Once you find it,
|
|
||||||
<router-link to="/success-story/add">tell your fellow citizens about it!</router-link>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<p>
|
|
||||||
You do not have an employment profile established; click “Edit Profile” in the menu to get
|
|
||||||
started!
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<hr>
|
|
||||||
<p>
|
|
||||||
There <span v-if="profileCount === 1">is</span><span v-else>are</span> <span v-if="profileCount === 0">no</span><span v-else>{{profileCount}}</span>
|
|
||||||
employment profile<span v-if="profileCount !== 1">s</span> from citizens of Gitmo Nation.
|
|
||||||
<span v-if="profileCount > 0">Take a look around and see if you can help them find work!</span>
|
|
||||||
</p>
|
|
||||||
<hr>
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
To see how this application works, check out “How It Works” in the sidebar (last updated June
|
To see how this application works, check out “How It Works” in the sidebar (last updated June
|
||||||
|
@ -32,47 +33,44 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, Ref, ref } from 'vue'
|
import { defineComponent, Ref, ref } from 'vue'
|
||||||
import api, { LogOnSuccess, Profile } from '../../api'
|
import api, { LogOnSuccess, Profile } from '../../api'
|
||||||
import { useStore } from '../../store'
|
import { useStore } from '../../store'
|
||||||
|
import LoadData from '../../components/shared/LoadData.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
|
components: { LoadData },
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
/** The currently logged-in user */
|
/** The currently logged-in user */
|
||||||
const user = store.state.user as LogOnSuccess
|
const user = store.state.user as LogOnSuccess
|
||||||
|
|
||||||
/** Error messages from data retrieval */
|
|
||||||
const errorMessages : string[] = []
|
|
||||||
|
|
||||||
/** The user's profile */
|
/** The user's profile */
|
||||||
const profile : Ref<Profile | undefined> = ref(undefined)
|
const profile : Ref<Profile | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** A count of profiles in the system */
|
/** A count of profiles in the system */
|
||||||
const profileCount = ref(0)
|
const profileCount = ref(0)
|
||||||
|
|
||||||
const retrieveData = async () => {
|
const retrieveData = async (errors : string[]) => {
|
||||||
const profileResult = await api.profile.retreive(undefined, user)
|
const profileResult = await api.profile.retreive(undefined, user)
|
||||||
if (typeof profileResult === 'string') {
|
if (typeof profileResult === 'string') {
|
||||||
errorMessages.push(profileResult)
|
errors.push(profileResult)
|
||||||
} else if (typeof profileResult !== 'undefined') {
|
} else if (typeof profileResult !== 'undefined') {
|
||||||
profile.value = profileResult
|
profile.value = profileResult
|
||||||
}
|
}
|
||||||
const count = await api.profile.count(user)
|
const count = await api.profile.count(user)
|
||||||
if (typeof count === 'string') {
|
if (typeof count === 'string') {
|
||||||
errorMessages.push(count)
|
errors.push(count)
|
||||||
} else {
|
} else {
|
||||||
profileCount.value = count
|
profileCount.value = count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(retrieveData)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
retrieveData,
|
||||||
user,
|
user,
|
||||||
errorMessages,
|
|
||||||
profile,
|
profile,
|
||||||
profileCount
|
profileCount
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,123 +1,103 @@
|
||||||
<template>
|
<template>
|
||||||
<h3>Employment Profile</h3>
|
<h3>Employment Profile</h3>
|
||||||
|
|
||||||
<v-form>
|
<load-data :load="retrieveData">
|
||||||
<v-container>
|
<form>
|
||||||
<v-row>
|
<v-container>
|
||||||
<v-col cols="12" sm="10" md="8" lg="6">
|
<v-row>
|
||||||
<v-text-field label="Real Name"
|
<v-col cols="12" sm="10" md="8" lg="6">
|
||||||
placeholder="Leave blank to use your NAS display name"
|
<label for="realName">Real Name</label>
|
||||||
counter="255"
|
<input type="text" id="realName" v-model="realName" maxlength="255"
|
||||||
maxlength="255"
|
placeholder="Leave blank to use your NAS display name">
|
||||||
:value="realName"></v-text-field>
|
</v-col>
|
||||||
<div class="form-group">
|
</v-row>
|
||||||
<label for="realName" class="jjj-label">Real Name</label>
|
<v-row>
|
||||||
[InputText id="realName" @bind-Value=@ProfileForm.RealName class="form-control"
|
<v-col>
|
||||||
placeholder="Leave blank to use your NAS display name" /]
|
<label>
|
||||||
[ValidationMessage For=@(() => ProfileForm.RealName) /]
|
<input type="checkbox" v-model="profile.seekingEmployment">
|
||||||
</div>
|
I am currently seeking employment
|
||||||
</v-col>
|
</label>
|
||||||
</v-row>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-check">
|
|
||||||
[InputCheckbox id="seeking" class="form-check-input" @bind-Value=@ProfileForm.IsSeekingEmployment /]
|
|
||||||
<label for="seeking" class="form-check-label">I am currently seeking employment</label>
|
|
||||||
<em v-if="profile?.seekingEmployment"> If you have found employment, consider
|
<em v-if="profile?.seekingEmployment"> If you have found employment, consider
|
||||||
<router-link to="/success-story/add">telling your fellow citizens about it</router-link>
|
<router-link to="/success-story/add">telling your fellow citizens about it</router-link>
|
||||||
</em>
|
</em>
|
||||||
</div>
|
</v-col>
|
||||||
</div>
|
</v-row>
|
||||||
</div>
|
<v-row>
|
||||||
<div class="form-row">
|
<v-col cols="12" sm="6" md="4">
|
||||||
<div class="col col-xs-12 col-sm-6 col-md-4">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="continentId" class="jjj-required">Continent</label>
|
<label for="continentId" class="jjj-required">Continent</label>
|
||||||
[InputSelect id="continentId" @bind-Value=@ProfileForm.ContinentId class="form-control"]
|
<select id="continentId">
|
||||||
<option>– Select –</option>
|
<option v-for="c in continents" :key="c.id" :value="c.id"
|
||||||
@foreach (var (id, name) in Continents)
|
:selected="c.id === profile?.continentId ? 'selected' : null">{{c.name}}</option>
|
||||||
{
|
</select>
|
||||||
<option value="@id">@name</option>
|
</v-col>
|
||||||
}
|
<v-col cols="12" sm="6" md="8">
|
||||||
[/InputSelect]
|
|
||||||
[ValidationMessage For=@(() => ProfileForm.ContinentId) /]
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-xs-12 col-sm-6 col-md-8">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="region" class="jjj-required">Region</label>
|
<label for="region" class="jjj-required">Region</label>
|
||||||
[InputText id="region" @bind-Value=@ProfileForm.Region class="form-control"
|
<input type="text" id="region" v-model="profile.region" maxlength="255"
|
||||||
placeholder="Country, state, geographic area, etc." /]
|
placeholder="Country, state, geographic area, etc.">
|
||||||
[ValidationMessage For=@(() => ProfileForm.Region) /]
|
</v-col>
|
||||||
</div>
|
</v-row>
|
||||||
</div>
|
<v-row>
|
||||||
</div>
|
<v-col>
|
||||||
<div class="form-row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="bio" class="jjj-required">Professional Biography</label>
|
<label for="bio" class="jjj-required">Professional Biography</label>
|
||||||
[MarkdownEditor Id="bio" @bind-Text=@ProfileForm.Biography /]
|
<markdown-editor id="bio" v-model:text="profile.biography" />
|
||||||
[ValidationMessage For=@(() => ProfileForm.Biography) /]
|
</v-col>
|
||||||
</div>
|
</v-row>
|
||||||
</div>
|
<v-row>
|
||||||
</div>
|
<v-col cols="12" sm="12" offset-md="2" md="4">
|
||||||
<div class="form-row">
|
<label>
|
||||||
<div class="col col-xs-12 col-sm-12 offset-md-2 col-md-4">
|
<input type="checkbox" v-model="profile.remoteWork">
|
||||||
<div class="form-check">
|
I am looking for remote work
|
||||||
[InputCheckbox id="isRemote" class="form-check-input" @bind-Value=@ProfileForm.RemoteWork /]
|
</label>
|
||||||
<label for="isRemote" class="form-check-label">I am looking for remote work</label>
|
</v-col>
|
||||||
</div>
|
<v-col cols="12" sm="12" md="4">
|
||||||
</div>
|
<label>
|
||||||
<div class="col col-xs-12 col-sm-12 col-md-4">
|
<input type="checkbox" v-model="profile.fullTime">
|
||||||
<div class="form-check">
|
I am looking for full-time work
|
||||||
[InputCheckbox id="isFull" class="form-check-input" @bind-Value=@ProfileForm.FullTime /]
|
</label>
|
||||||
<label for="isFull" class="form-check-label">I am looking for full-time work</label>
|
</v-col>
|
||||||
</div>
|
</v-row>
|
||||||
</div>
|
<hr>
|
||||||
</div>
|
<h4>
|
||||||
<hr>
|
Skills
|
||||||
<h4>
|
<button type="button" class="btn btn-outline-primary" @onclick="AddNewSkill">Add a Skill</button>
|
||||||
Skills
|
</h4>
|
||||||
<button type="button" class="btn btn-outline-primary" @onclick="AddNewSkill">Add a Skill</button>
|
@foreach (var skill in ProfileForm.Skills)
|
||||||
</h4>
|
{
|
||||||
@foreach (var skill in ProfileForm.Skills)
|
[SkillEdit Skill=@skill OnRemove=@RemoveSkill /]
|
||||||
{
|
}
|
||||||
[SkillEdit Skill=@skill OnRemove=@RemoveSkill /]
|
<hr>
|
||||||
}
|
<h4>Experience</h4>
|
||||||
<hr>
|
<p>
|
||||||
<h4>Experience</h4>
|
This application does not have a place to individually list your chronological job history; however, you can
|
||||||
<p>
|
use this area to list prior jobs, their dates, and anything else you want to include that’s not already a
|
||||||
This application does not have a place to individually list your chronological job history; however, you can
|
part of your Professional Biography above.
|
||||||
use this area to list prior jobs, their dates, and anything else you want to include that’s not already a
|
</p>
|
||||||
part of your Professional Biography above.
|
<v-row>
|
||||||
</p>
|
<v-col>
|
||||||
<div class="form-row">
|
<markdown-editor id="experience" v-model:text="profile.experience" />
|
||||||
<div class="col">
|
</v-col>
|
||||||
[MarkdownEditor Id="experience" @bind-Text=@ProfileForm.Experience /]
|
</v-row>
|
||||||
</div>
|
<v-row>
|
||||||
</div>
|
<v-col>
|
||||||
<div class="form-row">
|
<label>
|
||||||
<div class="col">
|
<input type="checkbox" v-model="profile.isPublic">
|
||||||
<div class="form-check">
|
|
||||||
[InputCheckbox id="isPublic" class="form-check-input" @bind-Value=@ProfileForm.IsPublic /]
|
|
||||||
<label for="isPublic" class="form-check-label">
|
|
||||||
Allow my profile to be searched publicly (outside NA Social)
|
Allow my profile to be searched publicly (outside NA Social)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</v-col>
|
||||||
</div>
|
</v-row>
|
||||||
</div>
|
<v-row>
|
||||||
<div class="form-row">
|
<v-col>
|
||||||
<div class="col">
|
<br>
|
||||||
<br>
|
<button type="submit" class="btn btn-outline-primary">Save</button>
|
||||||
<button type="submit" class="btn btn-outline-primary">Save</button>
|
</v-col>
|
||||||
</div>
|
</v-row>
|
||||||
</div>
|
</v-container>
|
||||||
</v-container>
|
</form>
|
||||||
</v-form>
|
<p v-if="!isNew">
|
||||||
<p v-if="!isNew">
|
<br><router-link :to="`/profile/view/${user.citizenId}`"><v-icon icon="file-account-outline" /> View Your User
|
||||||
<br><router-link :to="'/profile/view/' + user.citizenId"><v-icon icon="file-account-outline" /> View Your User
|
Profile</router-link>
|
||||||
Profile</router-link>
|
</p>
|
||||||
</p>
|
</load-data>
|
||||||
<p>
|
<p>
|
||||||
<br>If you want to delete your profile, or your entire account, <router-link to="/so-long/options">see your deletion
|
<br>If you want to delete your profile, or your entire account, <router-link to="/so-long/options">see your deletion
|
||||||
options here</router-link>.
|
options here</router-link>.
|
||||||
|
@ -126,12 +106,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref } from 'vue'
|
import { computed, defineComponent, Ref, ref } from 'vue'
|
||||||
import api, { LogOnSuccess, Profile } from '../../api'
|
import api, { LogOnSuccess, Profile } from '../../api'
|
||||||
|
import MarkdownEditor from '../../components/MarkdownEditor.vue'
|
||||||
|
import LoadData from '../../components/shared/LoadData.vue'
|
||||||
import { useStore } from '../../store'
|
import { useStore } from '../../store'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EditProfile',
|
name: 'EditProfile',
|
||||||
|
components: {
|
||||||
|
LoadData,
|
||||||
|
MarkdownEditor
|
||||||
|
},
|
||||||
setup () {
|
setup () {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
|
@ -141,35 +127,39 @@ export default defineComponent({
|
||||||
/** Whether this is a new profile */
|
/** Whether this is a new profile */
|
||||||
const isNew = ref(false)
|
const isNew = ref(false)
|
||||||
|
|
||||||
/** Errors that may be encountered */
|
|
||||||
const errorMessages : string[] = []
|
|
||||||
|
|
||||||
/** The user's current profile */
|
/** The user's current profile */
|
||||||
let profile : Profile | undefined
|
const profile : Ref<Profile | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** The user's real name */
|
/** The user's real name */
|
||||||
let realName : string | undefined
|
const realName : Ref<string | undefined> = ref(undefined)
|
||||||
|
|
||||||
/** Retrieve the user's profile */
|
/** Retrieve the user's profile and their real name */
|
||||||
const loadProfile = async () => {
|
const retrieveData = async (errors : string[]) => {
|
||||||
|
await store.dispatch('ensureContinents')
|
||||||
const profileResult = await api.profile.retreive(undefined, user)
|
const profileResult = await api.profile.retreive(undefined, user)
|
||||||
if (typeof profileResult === 'string') {
|
if (typeof profileResult === 'string') {
|
||||||
errorMessages.push(profileResult)
|
errors.push(profileResult)
|
||||||
} else if (typeof profileResult === 'undefined') {
|
} else if (typeof profileResult === 'undefined') {
|
||||||
isNew.value = true
|
isNew.value = true
|
||||||
} else {
|
} else {
|
||||||
profile = profileResult
|
profile.value = profileResult
|
||||||
|
// console.info(JSON.stringify(profile))
|
||||||
|
}
|
||||||
|
const nameResult = await api.citizen.retrieve(user.citizenId, user)
|
||||||
|
if (typeof nameResult === 'string') {
|
||||||
|
errors.push(nameResult)
|
||||||
|
} else {
|
||||||
|
realName.value = nameResult.realName || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadProfile)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
retrieveData,
|
||||||
user,
|
user,
|
||||||
isNew,
|
isNew,
|
||||||
errorMessages,
|
|
||||||
profile,
|
profile,
|
||||||
realName
|
realName,
|
||||||
|
continents: computed(() => store.state.continents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
25
src/JobsJobsJobs/App/src/views/citizen/LogOff.vue
Normal file
25
src/JobsJobsJobs/App/src/views/citizen/LogOff.vue
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<p>Logging off…</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useStore } from '../../store'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'LogOff',
|
||||||
|
setup () {
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.commit('clearUser')
|
||||||
|
router.push('/')
|
||||||
|
// TODO: toast
|
||||||
|
})
|
||||||
|
|
||||||
|
return { }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
3
src/JobsJobsJobs/App/src/views/profile/ProfileSearch.vue
Normal file
3
src/JobsJobsJobs/App/src/views/profile/ProfileSearch.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: convert this view</p>
|
||||||
|
</template>
|
20
src/JobsJobsJobs/App/src/views/profile/ProfileView.vue
Normal file
20
src/JobsJobsJobs/App/src/views/profile/ProfileView.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: convert this template</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ProfileEdit',
|
||||||
|
setup () {
|
||||||
|
const route = useRoute()
|
||||||
|
const profileId = route.params.id
|
||||||
|
|
||||||
|
return {
|
||||||
|
profileId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
3
src/JobsJobsJobs/App/src/views/profile/Seeking.vue
Normal file
3
src/JobsJobsJobs/App/src/views/profile/Seeking.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: convert this view</p>
|
||||||
|
</template>
|
82
src/JobsJobsJobs/App/src/views/so-long/DeletionOptions.vue
Normal file
82
src/JobsJobsJobs/App/src/views/so-long/DeletionOptions.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<h3>Account Deletion Options</h3>
|
||||||
|
|
||||||
|
<p v-if="error !== ''">{{error}}</p>
|
||||||
|
|
||||||
|
<h4>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 use if you
|
||||||
|
want to clear out your profile and start again (and remove the current one from others’ view).
|
||||||
|
</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<button class="btn btn-danger" @click="deleteProfile">Delete Your Profile</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4>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 below
|
||||||
|
<strong>will not</strong> affect your No Agenda Social account in any way; its effects are limited to Jobs, Jobs,
|
||||||
|
Jobs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
(This will not revoke this application’s permissions on No Agenda Social; you will have to remove this
|
||||||
|
yourself. The confirmation message has a link where you can do this; once the page loads, find the
|
||||||
|
<strong>Jobs, Jobs, Jobs</strong> entry, and click the <strong>× Revoke</strong> link for that entry.)
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<p class="text-center">
|
||||||
|
<button class="btn btn-danger" @click="deleteAccount">Delete Your Entire Account</button>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import api, { LogOnSuccess } from '../../api'
|
||||||
|
import { useStore } from '../../store'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'DeletionOptions',
|
||||||
|
setup () {
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** Error message encountered during actions */
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
/** Delete the profile only; redirect to home page on success */
|
||||||
|
const deleteProfile = async () => {
|
||||||
|
const resp = await api.profile.delete(store.state.user as LogOnSuccess)
|
||||||
|
if (typeof resp === 'string') {
|
||||||
|
error.value = resp
|
||||||
|
} else {
|
||||||
|
// TODO: notify
|
||||||
|
router.push('/citizen/dashboard')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete everything pertaining to the user's account */
|
||||||
|
const deleteAccount = async () => {
|
||||||
|
const resp = await api.citizen.delete(store.state.user as LogOnSuccess)
|
||||||
|
if (typeof resp === 'string') {
|
||||||
|
error.value = resp
|
||||||
|
} else {
|
||||||
|
store.commit('clearUser')
|
||||||
|
// TODO: notify
|
||||||
|
router.push('/so-long/success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
deleteProfile,
|
||||||
|
deleteAccount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
12
src/JobsJobsJobs/App/src/views/so-long/DeletionSuccess.vue
Normal file
12
src/JobsJobsJobs/App/src/views/so-long/DeletionSuccess.vue
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<h3>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
|
||||||
|
<strong>× Revoke</strong>. Otherwise, clicking “Log On” in the left-hand menu will create a new,
|
||||||
|
empty account without prompting you further.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Thank you for participating, and thank you for your courage. #GitmoNation
|
||||||
|
</p>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: convert this view</p>
|
||||||
|
</template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p>TODO: convert this view</p>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user