Env swap #21
|
@ -22,6 +22,7 @@ let configureApp (app : IApplicationBuilder) =
|
|||
e.MapFallbackToFile "index.html" |> ignore)
|
||||
|> ignore
|
||||
|
||||
open Newtonsoft.Json
|
||||
open NodaTime
|
||||
open Microsoft.AspNetCore.Authentication.JwtBearer
|
||||
open Microsoft.Extensions.Configuration
|
||||
|
@ -36,6 +37,10 @@ let configureServices (svc : IServiceCollection) =
|
|||
svc.AddLogging () |> 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 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
|
||||
|
@ -39,6 +39,46 @@ export default {
|
|||
const resp = await fetch(apiUrl(`citizen/log-on/${code}`), { method: 'GET', mode: 'cors' })
|
||||
if (resp.status === 200) return await resp.json() as LogOnSuccess
|
||||
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
|
||||
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 */
|
||||
export interface LogOnSuccess {
|
||||
/** 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',
|
||||
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 { createStore, Store, useStore as baseUseStore } from 'vuex'
|
||||
import api, { LogOnSuccess } from '../api'
|
||||
import api, { Continent, LogOnSuccess } from '../api'
|
||||
|
||||
/** The state tracked by the application */
|
||||
export interface State {
|
||||
user: LogOnSuccess | undefined
|
||||
logOnState: string
|
||||
continents: Continent[]
|
||||
}
|
||||
|
||||
/** An injection key to identify this state with Vue */
|
||||
|
@ -20,15 +21,22 @@ export default createStore({
|
|||
state: () : State => {
|
||||
return {
|
||||
user: undefined,
|
||||
logOnState: 'Logging you on with No Agenda Social...'
|
||||
logOnState: 'Logging you on with No Agenda Social...',
|
||||
continents: []
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setUser (state, user: LogOnSuccess) {
|
||||
state.user = user
|
||||
},
|
||||
clearUser (state) {
|
||||
state.user = undefined
|
||||
},
|
||||
setLogOnState (state, message) {
|
||||
state.logOnState = message
|
||||
},
|
||||
setContinents (state, continents : Continent[]) {
|
||||
state.continents = continents
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -39,6 +47,15 @@ export default createStore({
|
|||
} else {
|
||||
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: {
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
<template>
|
||||
<h3>Welcome, {{user.name}}</h3>
|
||||
|
||||
<template v-if="profile">
|
||||
<load-data :load="retrieveData">
|
||||
<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>
|
||||
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>.
|
||||
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>
|
||||
<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>
|
||||
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>
|
||||
</load-data>
|
||||
<hr>
|
||||
<p>
|
||||
To see how this application works, check out “How It Works” in the sidebar (last updated June
|
||||
|
@ -32,47 +33,44 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, Ref, ref } from 'vue'
|
||||
import { defineComponent, Ref, ref } from 'vue'
|
||||
import api, { LogOnSuccess, Profile } from '../../api'
|
||||
import { useStore } from '../../store'
|
||||
import LoadData from '../../components/shared/LoadData.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Dashboard',
|
||||
components: { LoadData },
|
||||
setup () {
|
||||
const store = useStore()
|
||||
|
||||
/** The currently logged-in user */
|
||||
const user = store.state.user as LogOnSuccess
|
||||
|
||||
/** Error messages from data retrieval */
|
||||
const errorMessages : string[] = []
|
||||
|
||||
/** The user's profile */
|
||||
const profile : Ref<Profile | undefined> = ref(undefined)
|
||||
|
||||
/** A count of profiles in the system */
|
||||
const profileCount = ref(0)
|
||||
|
||||
const retrieveData = async () => {
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
const profileResult = await api.profile.retreive(undefined, user)
|
||||
if (typeof profileResult === 'string') {
|
||||
errorMessages.push(profileResult)
|
||||
errors.push(profileResult)
|
||||
} else if (typeof profileResult !== 'undefined') {
|
||||
profile.value = profileResult
|
||||
}
|
||||
const count = await api.profile.count(user)
|
||||
if (typeof count === 'string') {
|
||||
errorMessages.push(count)
|
||||
errors.push(count)
|
||||
} else {
|
||||
profileCount.value = count
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(retrieveData)
|
||||
|
||||
return {
|
||||
retrieveData,
|
||||
user,
|
||||
errorMessages,
|
||||
profile,
|
||||
profileCount
|
||||
}
|
||||
|
|
|
@ -1,123 +1,103 @@
|
|||
<template>
|
||||
<h3>Employment Profile</h3>
|
||||
|
||||
<v-form>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="10" md="8" lg="6">
|
||||
<v-text-field label="Real Name"
|
||||
placeholder="Leave blank to use your NAS display name"
|
||||
counter="255"
|
||||
maxlength="255"
|
||||
:value="realName"></v-text-field>
|
||||
<div class="form-group">
|
||||
<label for="realName" class="jjj-label">Real Name</label>
|
||||
[InputText id="realName" @bind-Value=@ProfileForm.RealName class="form-control"
|
||||
placeholder="Leave blank to use your NAS display name" /]
|
||||
[ValidationMessage For=@(() => ProfileForm.RealName) /]
|
||||
</div>
|
||||
</v-col>
|
||||
</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>
|
||||
<load-data :load="retrieveData">
|
||||
<form>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="10" md="8" lg="6">
|
||||
<label for="realName">Real Name</label>
|
||||
<input type="text" id="realName" v-model="realName" maxlength="255"
|
||||
placeholder="Leave blank to use your NAS display name">
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<label>
|
||||
<input type="checkbox" v-model="profile.seekingEmployment">
|
||||
I am currently seeking employment
|
||||
</label>
|
||||
<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>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="form-group">
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<label for="continentId" class="jjj-required">Continent</label>
|
||||
[InputSelect id="continentId" @bind-Value=@ProfileForm.ContinentId class="form-control"]
|
||||
<option>– Select –</option>
|
||||
@foreach (var (id, name) in Continents)
|
||||
{
|
||||
<option value="@id">@name</option>
|
||||
}
|
||||
[/InputSelect]
|
||||
[ValidationMessage For=@(() => ProfileForm.ContinentId) /]
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-6 col-md-8">
|
||||
<div class="form-group">
|
||||
<select id="continentId">
|
||||
<option v-for="c in continents" :key="c.id" :value="c.id"
|
||||
:selected="c.id === profile?.continentId ? 'selected' : null">{{c.name}}</option>
|
||||
</select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="8">
|
||||
<label for="region" class="jjj-required">Region</label>
|
||||
[InputText id="region" @bind-Value=@ProfileForm.Region class="form-control"
|
||||
placeholder="Country, state, geographic area, etc." /]
|
||||
[ValidationMessage For=@(() => ProfileForm.Region) /]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<input type="text" id="region" v-model="profile.region" maxlength="255"
|
||||
placeholder="Country, state, geographic area, etc.">
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<label for="bio" class="jjj-required">Professional Biography</label>
|
||||
[MarkdownEditor Id="bio" @bind-Text=@ProfileForm.Biography /]
|
||||
[ValidationMessage For=@(() => ProfileForm.Biography) /]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col col-xs-12 col-sm-12 offset-md-2 col-md-4">
|
||||
<div class="form-check">
|
||||
[InputCheckbox id="isRemote" class="form-check-input" @bind-Value=@ProfileForm.RemoteWork /]
|
||||
<label for="isRemote" class="form-check-label">I am looking for remote work</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-xs-12 col-sm-12 col-md-4">
|
||||
<div class="form-check">
|
||||
[InputCheckbox id="isFull" class="form-check-input" @bind-Value=@ProfileForm.FullTime /]
|
||||
<label for="isFull" class="form-check-label">I am looking for full-time work</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h4>
|
||||
Skills
|
||||
<button type="button" class="btn btn-outline-primary" @onclick="AddNewSkill">Add a Skill</button>
|
||||
</h4>
|
||||
@foreach (var skill in ProfileForm.Skills)
|
||||
{
|
||||
[SkillEdit Skill=@skill OnRemove=@RemoveSkill /]
|
||||
}
|
||||
<hr>
|
||||
<h4>Experience</h4>
|
||||
<p>
|
||||
This application does not have a place to individually list your chronological job history; however, you can
|
||||
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 class="form-row">
|
||||
<div class="col">
|
||||
[MarkdownEditor Id="experience" @bind-Text=@ProfileForm.Experience /]
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
[InputCheckbox id="isPublic" class="form-check-input" @bind-Value=@ProfileForm.IsPublic /]
|
||||
<label for="isPublic" class="form-check-label">
|
||||
<markdown-editor id="bio" v-model:text="profile.biography" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" offset-md="2" md="4">
|
||||
<label>
|
||||
<input type="checkbox" v-model="profile.remoteWork">
|
||||
I am looking for remote work
|
||||
</label>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<label>
|
||||
<input type="checkbox" v-model="profile.fullTime">
|
||||
I am looking for full-time work
|
||||
</label>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<hr>
|
||||
<h4>
|
||||
Skills
|
||||
<button type="button" class="btn btn-outline-primary" @onclick="AddNewSkill">Add a Skill</button>
|
||||
</h4>
|
||||
@foreach (var skill in ProfileForm.Skills)
|
||||
{
|
||||
[SkillEdit Skill=@skill OnRemove=@RemoveSkill /]
|
||||
}
|
||||
<hr>
|
||||
<h4>Experience</h4>
|
||||
<p>
|
||||
This application does not have a place to individually list your chronological job history; however, you can
|
||||
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>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<markdown-editor id="experience" v-model:text="profile.experience" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<label>
|
||||
<input type="checkbox" v-model="profile.isPublic">
|
||||
Allow my profile to be searched publicly (outside NA Social)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<br>
|
||||
<button type="submit" class="btn btn-outline-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-form>
|
||||
<p v-if="!isNew">
|
||||
<br><router-link :to="'/profile/view/' + user.citizenId"><v-icon icon="file-account-outline" /> View Your User
|
||||
Profile</router-link>
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<br>
|
||||
<button type="submit" class="btn btn-outline-primary">Save</button>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</form>
|
||||
<p v-if="!isNew">
|
||||
<br><router-link :to="`/profile/view/${user.citizenId}`"><v-icon icon="file-account-outline" /> View Your User
|
||||
Profile</router-link>
|
||||
</p>
|
||||
</load-data>
|
||||
<p>
|
||||
<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>.
|
||||
|
@ -126,12 +106,18 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import { computed, defineComponent, Ref, ref } from 'vue'
|
||||
import api, { LogOnSuccess, Profile } from '../../api'
|
||||
import MarkdownEditor from '../../components/MarkdownEditor.vue'
|
||||
import LoadData from '../../components/shared/LoadData.vue'
|
||||
import { useStore } from '../../store'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EditProfile',
|
||||
components: {
|
||||
LoadData,
|
||||
MarkdownEditor
|
||||
},
|
||||
setup () {
|
||||
const store = useStore()
|
||||
|
||||
|
@ -141,35 +127,39 @@ export default defineComponent({
|
|||
/** Whether this is a new profile */
|
||||
const isNew = ref(false)
|
||||
|
||||
/** Errors that may be encountered */
|
||||
const errorMessages : string[] = []
|
||||
|
||||
/** The user's current profile */
|
||||
let profile : Profile | undefined
|
||||
const profile : Ref<Profile | undefined> = ref(undefined)
|
||||
|
||||
/** The user's real name */
|
||||
let realName : string | undefined
|
||||
const realName : Ref<string | undefined> = ref(undefined)
|
||||
|
||||
/** Retrieve the user's profile */
|
||||
const loadProfile = async () => {
|
||||
/** Retrieve the user's profile and their real name */
|
||||
const retrieveData = async (errors : string[]) => {
|
||||
await store.dispatch('ensureContinents')
|
||||
const profileResult = await api.profile.retreive(undefined, user)
|
||||
if (typeof profileResult === 'string') {
|
||||
errorMessages.push(profileResult)
|
||||
errors.push(profileResult)
|
||||
} else if (typeof profileResult === 'undefined') {
|
||||
isNew.value = true
|
||||
} 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 {
|
||||
retrieveData,
|
||||
user,
|
||||
isNew,
|
||||
errorMessages,
|
||||
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