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)
 | 
			
		||||
  |> 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,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <h3>Welcome, {{user.name}}</h3>
 | 
			
		||||
 | 
			
		||||
  <load-data :load="retrieveData">
 | 
			
		||||
    <template v-if="profile">
 | 
			
		||||
      <p>
 | 
			
		||||
        Your employment profile was last updated {{profile.lastUpdatedOn}}. Your profile currently lists
 | 
			
		||||
@ -24,6 +24,7 @@
 | 
			
		||||
      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,80 +1,61 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <h3>Employment Profile</h3>
 | 
			
		||||
 | 
			
		||||
  <v-form>
 | 
			
		||||
  <load-data :load="retrieveData">
 | 
			
		||||
    <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>
 | 
			
		||||
            <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>
 | 
			
		||||
      <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>
 | 
			
		||||
        <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>
 | 
			
		||||
            <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  
 | 
			
		||||
@ -91,33 +72,32 @@
 | 
			
		||||
          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">
 | 
			
		||||
        <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">
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col>
 | 
			
		||||
            <br>
 | 
			
		||||
            <button type="submit" class="btn btn-outline-primary">Save</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
      </v-container>
 | 
			
		||||
  </v-form>
 | 
			
		||||
    </form>
 | 
			
		||||
    <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>
 | 
			
		||||
    </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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user