diff --git a/src/JobsJobsJobs/Api/Data.fs b/src/JobsJobsJobs/Api/Data.fs index c72cea4..fa81781 100644 --- a/src/JobsJobsJobs/Api/Data.fs +++ b/src/JobsJobsJobs/Api/Data.fs @@ -381,6 +381,16 @@ module Continent = withReconn(conn).ExecuteAsync(fun () -> r.Table(Table.Continent) .RunResultAsync conn) + + /// Get a continent by its ID + let findById (contId : ContinentId) conn = task { + let! continent = + withReconn(conn).ExecuteAsync(fun () -> + r.Table(Table.Continent) + .Get(contId) + .RunResultAsync conn) + return toOption continent + } /// Job listing data access functions diff --git a/src/JobsJobsJobs/Api/Handlers.fs b/src/JobsJobsJobs/Api/Handlers.fs index 77c3a3b..9e2c8f3 100644 --- a/src/JobsJobsJobs/Api/Handlers.fs +++ b/src/JobsJobsJobs/Api/Handlers.fs @@ -203,6 +203,29 @@ module Profile = | Some profile -> return! json profile next ctx | None -> return! Error.notFound next ctx } + + // GET: /api/profile/view/[id] + let view citizenId : HttpHandler = + authorize + >=> fun next ctx -> task { + let citId = CitizenId citizenId + let dbConn = conn ctx + match! Data.Profile.findById citId dbConn with + | Some profile -> + match! Data.Citizen.findById citId dbConn with + | Some citizen -> + match! Data.Continent.findById profile.continentId dbConn with + | Some continent -> + return! + json { + profile = profile + citizen = citizen + continent = continent + } next ctx + | None -> return! Error.notFound next ctx + | None -> return! Error.notFound next ctx + | None -> return! Error.notFound next ctx + } // GET: /api/profile/count let count : HttpHandler = @@ -365,6 +388,7 @@ let allEndpoints = [ route "" Profile.current route "/count" Profile.count routef "/get/%O" Profile.get + routef "/view/%O" Profile.view route "/public-search" Profile.publicSearch route "/search" Profile.search ] diff --git a/src/JobsJobsJobs/App/package-lock.json b/src/JobsJobsJobs/App/package-lock.json index f1db9bd..f1fc470 100644 --- a/src/JobsJobsJobs/App/package-lock.json +++ b/src/JobsJobsJobs/App/package-lock.json @@ -1,6 +1,6 @@ { "name": "jobs-jobs-jobs", - "version": "0.1.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1353,6 +1353,12 @@ "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", "dev": true }, + "@types/marked": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.4.tgz", + "integrity": "sha512-L9VRSe0Id8xbPL99mUo/4aKgD7ZoRwFZqUQScNKHi2pFjF9ZYSMNShUHD6VlMT6J/prQq0T1mxuU25m3R7dFzg==", + "dev": true + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -8454,6 +8460,11 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", + "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", diff --git a/src/JobsJobsJobs/App/package.json b/src/JobsJobsJobs/App/package.json index 9021f43..e8a745d 100644 --- a/src/JobsJobsJobs/App/package.json +++ b/src/JobsJobsJobs/App/package.json @@ -11,6 +11,7 @@ "dependencies": { "@mdi/font": "5.9.55", "core-js": "^3.6.5", + "marked": "^2.1.3", "roboto-fontface": "*", "vue": "^3.0.0", "vue-router": "^4.0.0-0", @@ -18,6 +19,7 @@ "vuex": "^4.0.0-0" }, "devDependencies": { + "@types/marked": "^2.0.4", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", "@vue/cli-plugin-babel": "~4.5.0", diff --git a/src/JobsJobsJobs/App/src/App.vue b/src/JobsJobsJobs/App/src/App.vue index 699f5c5..5ea03b9 100644 --- a/src/JobsJobsJobs/App/src/App.vue +++ b/src/JobsJobsJobs/App/src/App.vue @@ -8,7 +8,11 @@ - + + + + + @@ -29,11 +33,6 @@ export default defineComponent({ AppFooter, AppNav, TitleBar - }, - setup () { - return { - // - } } }) @@ -66,4 +65,11 @@ ul justify-content: center .v-footer flex-direction: row-reverse +// Route transitions +.fade-enter-active, +.fade-leave-active + transition: opacity 0.125s ease +.fade-enter-from, +.fade-leave-to + opacity: 0 diff --git a/src/JobsJobsJobs/App/src/api/index.ts b/src/JobsJobsJobs/App/src/api/index.ts index 06346d6..3fca6ce 100644 --- a/src/JobsJobsJobs/App/src/api/index.ts +++ b/src/JobsJobsJobs/App/src/api/index.ts @@ -1,4 +1,5 @@ -import { Citizen, Continent, Count, LogOnSuccess, Profile } from './types' +import { MarkedOptions } from 'marked' +import { Citizen, Continent, Count, LogOnSuccess, Profile, ProfileForView } from './types' /** * Create a URL that will access the API @@ -24,6 +25,31 @@ const reqInit = (method : string, user : LogOnSuccess) : RequestInit => { } } +/** + * Retrieve a result for an API call + * + * @param resp The response received from the API + * @param action The action being performed (used in error messages) + * @returns The expected result (if found), undefined (if not found), or an error string + */ +async function apiResult (resp : Response, action : string) : Promise { + if (resp.status === 200) return await resp.json() as T + if (resp.status === 404) return undefined + return `Error ${action} - ${await resp.text()}` +} + +/** + * Run an API action that does not return a result + * + * @param resp The response received from the API call + * @param action The action being performed (used in error messages) + * @returns Undefined (if successful), or an error string + */ +const apiAction = async (resp : Response, action : string) : Promise => { + if (resp.status === 200) return undefined + return `Error ${action} - ${await resp.text()}` +} + export default { /** API functions for citizens */ @@ -48,11 +74,8 @@ export default { * @param user The currently logged-on user * @returns The citizen, or an error */ - retrieve: async (id : string, user : LogOnSuccess) : Promise => { - 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()}` - }, + retrieve: async (id : string, user : LogOnSuccess) : Promise => + apiResult(await fetch(apiUrl(`citizen/get/${id}`), reqInit('GET', user)), `retrieving citizen ${id}`), /** * Delete the current citizen's entire Jobs, Jobs, Jobs record @@ -60,11 +83,8 @@ export default { * @param user The currently logged-on user * @returns Undefined if successful, an error if not */ - delete: async (user : LogOnSuccess) : Promise => { - const resp = await fetch(apiUrl('citizen'), reqInit('DELETE', user)) - if (resp.status === 200) return undefined - return `Error deleting citizen - ${await resp.text()}` - } + delete: async (user : LogOnSuccess) : Promise => + apiAction(await fetch(apiUrl('citizen'), reqInit('DELETE', user)), 'deleting citizen') }, /** API functions for continents */ @@ -75,11 +95,8 @@ export default { * * @returns All continents, or an error */ - all: async () : Promise => { - 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()}` - } + all: async () : Promise => + apiResult(await fetch(apiUrl('continent/all'), { method: 'GET' }), 'retrieving continents') }, /** API functions for profiles */ @@ -99,6 +116,16 @@ export default { if (resp.status !== 204) return `Error retrieving profile - ${await resp.text()}` }, + /** + * Retrieve a profile for viewing + * + * @param id The ID of the profile to retrieve for viewing + * @param user The currently logged-on user + * @returns The profile (if found), undefined (if not found), or an error string + */ + retreiveForView: async (id : string, user : LogOnSuccess) : Promise => + apiResult(await fetch(apiUrl(`profile/view/${id}`), reqInit('GET', user)), 'retrieving profile'), + /** * Count profiles in the system * @@ -120,12 +147,15 @@ export default { * @param user The currently logged-on user * @returns Undefined if successful, an error if not */ - delete: async (user : LogOnSuccess) : Promise => { - const resp = await fetch(apiUrl('profile'), reqInit('DELETE', user)) - if (resp.status === 200) return undefined - return `Error deleting profile - ${await resp.text()}` - } + delete: async (user : LogOnSuccess) : Promise => + apiAction(await fetch(apiUrl('profile'), reqInit('DELETE', user)), 'deleting profile') } } +/** The standard Jobs, Jobs, Jobs options for `marked` (GitHub-Flavo(u)red Markdown (GFM) with smart quotes) */ +export const markedOptions : MarkedOptions = { + gfm: true, + smartypants: true +} + export * from './types' diff --git a/src/JobsJobsJobs/App/src/api/types.ts b/src/JobsJobsJobs/App/src/api/types.ts index 0648aea..8cbaa16 100644 --- a/src/JobsJobsJobs/App/src/api/types.ts +++ b/src/JobsJobsJobs/App/src/api/types.ts @@ -71,6 +71,16 @@ export interface Profile { skills : Skill[] } +/** The data required to show a viewable profile */ +export interface ProfileForView { + /** The profile itself */ + profile : Profile + /** The citizen to whom the profile belongs */ + citizen : Citizen + /** The continent for the profile */ + continent : Continent +} + /** A count */ export interface Count { /** The count being returned */ diff --git a/src/JobsJobsJobs/App/src/components/MarkdownEditor.vue b/src/JobsJobsJobs/App/src/components/MarkdownEditor.vue index 989cbba..2e6161f 100644 --- a/src/JobsJobsJobs/App/src/components/MarkdownEditor.vue +++ b/src/JobsJobsJobs/App/src/components/MarkdownEditor.vue @@ -1,7 +1,7 @@ + + diff --git a/src/JobsJobsJobs/App/src/components/PageTitle.vue b/src/JobsJobsJobs/App/src/components/PageTitle.vue new file mode 100644 index 0000000..3fd419a --- /dev/null +++ b/src/JobsJobsJobs/App/src/components/PageTitle.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/JobsJobsJobs/App/src/main.ts b/src/JobsJobsJobs/App/src/main.ts index 4e41ade..6b84ef8 100644 --- a/src/JobsJobsJobs/App/src/main.ts +++ b/src/JobsJobsJobs/App/src/main.ts @@ -3,9 +3,13 @@ import vuetify from './plugins/vuetify' import App from './App.vue' import router from './router' import store, { key } from './store' +import PageTitle from './components/PageTitle.vue' -createApp(App) +const app = createApp(App) .use(router) .use(store, key) .use(vuetify) - .mount('#app') + +app.component('PageTitle', PageTitle) + +app.mount('#app') diff --git a/src/JobsJobsJobs/App/src/store/index.ts b/src/JobsJobsJobs/App/src/store/index.ts index 79fa767..cd472d0 100644 --- a/src/JobsJobsJobs/App/src/store/index.ts +++ b/src/JobsJobsJobs/App/src/store/index.ts @@ -4,8 +4,11 @@ import api, { Continent, LogOnSuccess } from '../api' /** The state tracked by the application */ export interface State { + /** The currently logged-on user */ user: LogOnSuccess | undefined + /** The state of the log on process */ logOnState: string + /** All continents (use `ensureContinents` action) */ continents: Continent[] } @@ -26,13 +29,13 @@ export default createStore({ } }, mutations: { - setUser (state, user: LogOnSuccess) { + setUser (state, user : LogOnSuccess) { state.user = user }, clearUser (state) { state.user = undefined }, - setLogOnState (state, message) { + setLogOnState (state, message : string) { state.logOnState = message }, setContinents (state, continents : Continent[]) { diff --git a/src/JobsJobsJobs/App/src/views/Home.vue b/src/JobsJobsJobs/App/src/views/Home.vue index 27df12b..c1cac69 100644 --- a/src/JobsJobsJobs/App/src/views/Home.vue +++ b/src/JobsJobsJobs/App/src/views/Home.vue @@ -1,14 +1,17 @@