Mobile layout / .NET 6 (#28)

- Mobile menu now shows for small and extra-small windows, with the traditional menu showing above those breakpoints (landscape on larger mobile, desktop)
- The back-end is now running on .NET 6 RC 1.
- Use existing library code for page title vs. hand-rolled component
This commit is contained in:
Daniel J. Summers 2021-09-17 12:13:32 -04:00 committed by GitHub
parent a1d1b53ff4
commit 2ff8618272
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 18844 additions and 235 deletions

View File

@ -20,7 +20,6 @@ type MastodonAccount () =
member val Url = "" with get, set member val Url = "" with get, set
open FSharp.Control.Tasks
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open System open System
open System.Net.Http open System.Net.Http

View File

@ -1,7 +1,6 @@
/// Data access functions for Jobs, Jobs, Jobs /// Data access functions for Jobs, Jobs, Jobs
module JobsJobsJobs.Api.Data module JobsJobsJobs.Api.Data
open FSharp.Control.Tasks
open JobsJobsJobs.Domain.Types open JobsJobsJobs.Domain.Types
open Polly open Polly
open RethinkDb.Driver open RethinkDb.Driver

View File

@ -1,7 +1,6 @@
/// Route handlers for Giraffe endpoints /// Route handlers for Giraffe endpoints
module JobsJobsJobs.Api.Handlers module JobsJobsJobs.Api.Handlers
open FSharp.Control.Tasks
open Giraffe open Giraffe
open JobsJobsJobs.Domain open JobsJobsJobs.Domain
open JobsJobsJobs.Domain.SharedTypes open JobsJobsJobs.Domain.SharedTypes
@ -288,7 +287,7 @@ module Listing =
// PATCH: /api/listing/[id] // PATCH: /api/listing/[id]
let expire listingId : HttpHandler = let expire listingId : HttpHandler =
authorize authorize
>=> fun next ctx -> task { >=> fun next ctx -> FSharp.Control.Tasks.Affine.task {
let dbConn = conn ctx let dbConn = conn ctx
let now = clock(ctx).GetCurrentInstant () let now = clock(ctx).GetCurrentInstant ()
match! Data.Listing.findById (ListingId listingId) dbConn with match! Data.Listing.findById (ListingId listingId) dbConn with
@ -309,7 +308,6 @@ module Listing =
| None -> () | None -> ()
return! ok next ctx return! ok next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// GET: /api/listing/search // GET: /api/listing/search

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "jobs-jobs-jobs", "name": "jobs-jobs-jobs",
"version": "2.1.0", "version": "2.2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -13,6 +13,7 @@
"@mdi/js": "^5.9.55", "@mdi/js": "^5.9.55",
"@vuelidate/core": "^2.0.0-alpha.24", "@vuelidate/core": "^2.0.0-alpha.24",
"@vuelidate/validators": "^2.0.0-alpha.21", "@vuelidate/validators": "^2.0.0-alpha.21",
"@vueuse/core": "^6.3.3",
"bootstrap": "^5.1.0", "bootstrap": "^5.1.0",
"core-js": "^3.16.3", "core-js": "^3.16.3",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",

View File

@ -10,11 +10,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue" import { defineComponent, onMounted } from "vue"
import "bootstrap/dist/css/bootstrap.min.css" import "bootstrap/dist/css/bootstrap.min.css"
import { Citizen } from "./api" import { Citizen } from "./api"
import { Mutations, useStore } from "./store"
import AppFooter from "./components/layout/AppFooter.vue" import AppFooter from "./components/layout/AppFooter.vue"
import AppNav from "./components/layout/AppNav.vue" import AppNav from "./components/layout/AppNav.vue"
import AppToaster from "./components/layout/AppToaster.vue" import AppToaster from "./components/layout/AppToaster.vue"
@ -29,6 +30,10 @@ export default defineComponent({
} }
}) })
const store = useStore()
onMounted(() => store.commit(Mutations.SetTitle, "Jobs, Jobs, Jobs"))
/** /**
* Return "Yes" for true and "No" for false * Return "Yes" for true and "No" for false
* *

View File

@ -1,15 +1,15 @@
<template lang="pug"> <template lang="pug">
form.container form.container
.row .row
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .col-xs-12.col-sm-6.col-md-4.col-lg-3
continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent") continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent")
.col.col-xs-12.col-sm-6.col-lg-3 .col-xs-12.col-sm-6.col-lg-3
.form-floating .form-floating
input.form-control(type="text" id="regionSearch" placeholder="(free-form text)" :value="criteria.region" input.form-control(type="text" id="regionSearch" placeholder="(free-form text)" :value="criteria.region"
@input="updateValue('region', $event.target.value)") @input="updateValue('region', $event.target.value)")
label(for="regionSearch") Region label(for="regionSearch") Region
.form-text (free-form text) .form-text (free-form text)
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0 .col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
label.jjj-label Remote Work Opportunity? label.jjj-label Remote Work Opportunity?
br br
.form-check.form-check-inline .form-check.form-check-inline
@ -24,13 +24,13 @@ form.container
input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'" input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'"
@click="updateValue('remoteWork', 'no')") @click="updateValue('remoteWork', 'no')")
label.form-check-label(for="remoteNo") No label.form-check-label(for="remoteNo") No
.col.col-xs-12.col-sm-6.col-lg-3 .col-xs-12.col-sm-6.col-lg-3
.form-floating .form-floating
input.form-control(type="text" id="textSearch" placeholder="(free-form text)" :value="criteria.text" input.form-control(type="text" id="textSearch" placeholder="(free-form text)" :value="criteria.text"
@input="updateValue('text', $event.target.value)") @input="updateValue('text', $event.target.value)")
label(for="textSearch") Job Listing Text label(for="textSearch") Job Listing Text
.form-text (free-form text) .form-text (free-form text)
.row: .col.col-xs-12 .row: .col
br br
button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search
</template> </template>

View File

@ -1,28 +0,0 @@
<template lang="pug">
p(v-if="false")
</template>
<script setup lang="ts">
import { onMounted, watch } from "vue"
const props = defineProps<{
title: string
}>()
/** The name of the application */
const appName = "Jobs, Jobs, Jobs"
/** Set the page title based on the input title attribute */
const setTitle = () => {
if (props.title === "") {
document.title = appName
} else {
document.title = `${props.title} | ${appName}`
}
}
onMounted(setTitle)
/** Change the page title when the property changes */
watch(() => props.title, setTitle)
</script>

View File

@ -0,0 +1,81 @@
<template lang="pug">
nav
template(v-if="isLoggedOn")
router-link(to="/citizen/dashboard" @click="hide") #[icon(:icon="mdiViewDashboardVariant")]&nbsp; Dashboard
router-link(to="/help-wanted" @click="hide") #[icon(:icon="mdiNewspaperVariantMultipleOutline")]&nbsp; Help Wanted!
router-link(to="/profile/search" @click="hide") #[icon(:icon="mdiViewListOutline")]&nbsp; Employment Profiles
router-link(to="/success-story/list" @click="hide") #[icon(:icon="mdiThumbUp")]&nbsp; Success Stories
.separator
router-link(to="/listings/mine" @click="hide") #[icon(:icon="mdiSignText")]&nbsp; My Job Listings
router-link(to="/citizen/profile" @click="hide") #[icon(:icon="mdiPencil")]&nbsp; My Employment Profile
.separator
router-link(to="/citizen/log-off" @click="hide") #[icon(:icon="mdiLogoutVariant")]&nbsp; Log Off
template(v-else)
router-link(to="/" @click="hide") #[icon(:icon="mdiHome")]&nbsp; Home
router-link(to="/profile/seeking" @click="hide") #[icon(:icon="mdiViewListOutline")]&nbsp; Job Seekers
router-link(to="/citizen/log-on" @click="hide") #[icon(:icon="mdiLoginVariant")]&nbsp; Log On
router-link(to="/how-it-works" @click="hide") #[icon(:icon="mdiHelpCircleOutline")]&nbsp; How It Works
</template>
<script setup lang="ts">
import { computed } from "vue"
import { useRouter } from "vue-router"
import { Offcanvas } from "bootstrap"
import { useStore } from "@/store"
import {
mdiHelpCircleOutline,
mdiHome,
mdiLoginVariant,
mdiLogoutVariant,
mdiNewspaperVariantMultipleOutline,
mdiPencil,
mdiSignText,
mdiThumbUp,
mdiViewDashboardVariant,
mdiViewListOutline
} from "@mdi/js"
const store = useStore()
const router = useRouter()
/** Whether a user is logged in or not */
const isLoggedOn = computed(() => store.state.user !== undefined)
/** The current mobile menu */
const menu = computed(() => {
const elt = document.getElementById("mobileMenu")
return elt ? Offcanvas.getOrCreateInstance(elt) : undefined
})
/** Hide the offcanvas menu (if it exists) when a link is clicked */
const hide = () => { if (menu.value) menu.value.hide() }
</script>
<style lang="sass" scoped>
path
fill: white
path:hover
fill: black
a:link, a:visited
text-decoration: none
color: white
nav > a
display: block
width: 100%
border-radius: .25rem
padding: .5rem
margin: .5rem 0
font-size: 1rem
> i
vertical-align: top
margin-right: 1rem
&.router-link-exact-active
background-color: rgba(255, 255, 255, .2)
&:hover
background-color: rgba(255, 255, 255, .5)
color: black
text-decoration: none
nav > div.separator
border-bottom: solid 1px rgba(255, 255, 255, .75)
height: 1px
</style>

View File

@ -1,88 +1,45 @@
<template lang="pug"> <template lang="pug">
aside.collapse.show.p-3 #mobileMenu.offcanvas.offcanvas-end(v-if="showMobileMenu" tabindex="-1" aria-labelledby="mobileMenuLabel")
.offcanvas-header
h5#mobileMenuLabel Menu
button.btn-close.text-reset(type="button" data-bs-dismiss="offcanvas" aria-label="Close")
.offcanvas-body: app-links
aside.collapse.show.p-3(v-else)
p.home-link.pb-3: router-link(to="/") Jobs, Jobs, Jobs p.home-link.pb-3: router-link(to="/") Jobs, Jobs, Jobs
p &nbsp; p &nbsp;
nav app-links
template(v-if="isLoggedOn")
router-link(to="/citizen/dashboard") #[icon(:icon="mdiViewDashboardVariant")]&nbsp; Dashboard
router-link(to="/help-wanted") #[icon(:icon="mdiNewspaperVariantMultipleOutline")]&nbsp; Help Wanted!
router-link(to="/profile/search") #[icon(:icon="mdiViewListOutline")]&nbsp; Employment Profiles
router-link(to="/success-story/list") #[icon(:icon="mdiThumbUp")]&nbsp; Success Stories
.separator
router-link(to="/listings/mine") #[icon(:icon="mdiSignText")]&nbsp; My Job Listings
router-link(to="/citizen/profile") #[icon(:icon="mdiPencil")]&nbsp; My Employment Profile
.separator
router-link(to="/citizen/log-off") #[icon(:icon="mdiLogoutVariant")]&nbsp; Log Off
template(v-else)
router-link(to="/") #[icon(:icon="mdiHome")]&nbsp; Home
router-link(to="/profile/seeking") #[icon(:icon="mdiViewListOutline")]&nbsp; Job Seekers
router-link(to="/citizen/log-on") #[icon(:icon="mdiLoginVariant")]&nbsp; Log On
router-link(to="/how-it-works") #[icon(:icon="mdiHelpCircleOutline")]&nbsp; How It Works
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue" import { useBreakpoints, breakpointsBootstrapV5 } from "@vueuse/core"
import { useStore } from "@/store" import AppLinks from "./AppLinks.vue"
import {
mdiHelpCircleOutline,
mdiHome,
mdiLoginVariant,
mdiLogoutVariant,
mdiNewspaperVariantMultipleOutline,
mdiPencil,
mdiSignText,
mdiThumbUp,
mdiViewDashboardVariant,
mdiViewListOutline
} from "@mdi/js"
const store = useStore() const breakpoints = useBreakpoints(breakpointsBootstrapV5)
/** Whether a user is logged in or not */ /** Whether the mobile menu or the desktop menu should be shown */
const isLoggedOn = computed(() => store.state.user !== undefined) const showMobileMenu = breakpoints.smaller("md")
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
aside aside,
#mobileMenu
background-image: linear-gradient(180deg, darkgreen 0%, green 70%) background-image: linear-gradient(180deg, darkgreen 0%, green 70%)
color: white color: white
font-size: 1.2rem font-size: 1.2rem
aside
min-height: 100vh min-height: 100vh
width: 250px width: 250px
min-width: 250px min-width: 250px
position: sticky position: sticky
top: 0 top: 0
path
fill: white
path:hover
fill: black
a:link, a:visited
text-decoration: none
color: white
// font-weight: 500
.home-link .home-link
font-size: 1.2rem font-size: 1.2rem
text-align: center text-align: center
background-color: rgba(0, 0, 0, .4) background-color: rgba(0, 0, 0, .4)
margin: -1rem margin: -1rem
padding: 1rem padding: 1rem
nav > a a:link,
display: block a:visited
width: 100%
border-radius: .25rem
padding: .5rem
margin: .5rem 0
font-size: 1rem
> i
vertical-align: top
margin-right: 1rem
&.router-link-exact-active
background-color: rgba(255, 255, 255, .2)
&:hover
background-color: rgba(255, 255, 255, .5)
color: black
text-decoration: none text-decoration: none
nav > div.separator color: white
border-bottom: solid 1px rgba(255, 255, 255, .75)
height: 1px
</style> </style>

View File

@ -1,16 +1,38 @@
<template lang="pug"> <template lang="pug">
nav.navbar.navbar-light.bg-light nav.navbar.navbar-dark(v-if="showMobileHeader")
span.navbar-text: router-link(to="/") Jobs, Jobs, Jobs
button.btn(data-bs-toggle="offcanvas" data-bs-target="#mobileMenu" aria-controls="mobileMenu")
icon(:icon="mdiMenu")
nav.navbar.navbar-light.bg-light(v-else)
span &nbsp; span &nbsp;
span.navbar-text. span.navbar-text.
(&hellip;and Jobs &ndash; #[audio-clip(clip="pelosi-jobs") Let&rsquo;s Vote for Jobs!]) (&hellip;and Jobs &ndash; #[audio-clip(clip="pelosi-jobs") Let&rsquo;s Vote for Jobs!])
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { mdiMenu } from "@mdi/js"
import { useBreakpoints, breakpointsBootstrapV5 } from "@vueuse/core"
import AudioClip from "@/components/AudioClip.vue" import AudioClip from "@/components/AudioClip.vue"
const breakpoints = useBreakpoints(breakpointsBootstrapV5)
/** Whether to show the mobile or desktop header */
const showMobileHeader = breakpoints.smaller("md")
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>
.navbar-dark
background-image: linear-gradient(0deg, green 0%, darkgreen 70%)
padding-left: 1rem
padding-right: 1rem
button
padding: 0
.navbar-text
font-weight: bold
color: white
.navbar-light
.navbar-text .navbar-text
font-style: italic font-style: italic
padding-right: 1rem padding: 0 1rem 0 0
</style> </style>

View File

@ -1,15 +1,15 @@
<template lang="pug"> <template lang="pug">
form.container form.container
.row .row
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .col-xs-12.col-sm-6.col-md-4.col-lg-3
continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent") continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent")
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .col-xs-12.col-sm-6.col-md-4.col-lg-3
.form-floating .form-floating
input.form-control.form-control-sm(type="text" id="region" placeholder="(free-form text)" input.form-control.form-control-sm(type="text" id="region" placeholder="(free-form text)"
:value="criteria.region" @input="updateValue('region', $event.target.value)") :value="criteria.region" @input="updateValue('region', $event.target.value)")
label(for="region") Region label(for="region") Region
.form-text (free-form text) .form-text (free-form text)
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0 .col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
label.jjj-label Seeking Remote Work? label.jjj-label Seeking Remote Work?
br br
.form-check.form-check-inline .form-check.form-check-inline
@ -24,13 +24,13 @@ form.container
input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'" input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'"
@click="updateValue('remoteWork', 'no')") @click="updateValue('remoteWork', 'no')")
label.form-check-label(for="remoteNo") No label.form-check-label(for="remoteNo") No
.col.col-xs-12.col-sm-6.col-lg-3 .col-xs-12.col-sm-6.col-lg-3
.form-floating .form-floating
input.form-control.form-control-sm(type="text" id="skillSearch" placeholder="(free-form text)" input.form-control.form-control-sm(type="text" id="skillSearch" placeholder="(free-form text)"
:value="criteria.skill" @input="updateValue('skill', $event.target.value)") :value="criteria.skill" @input="updateValue('skill', $event.target.value)")
label(for="skillSearch") Skill label(for="skillSearch") Skill
.form-text (free-form text) .form-text (free-form text)
.row: .col.col-xs-12 .row: .col
br br
button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search
</template> </template>

View File

@ -1,9 +1,9 @@
<template lang="pug"> <template lang="pug">
form.container form.container
.row .row
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .col-xs-12.col-sm-6.col-md-4.col-lg-3
continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent") continent-list(v-model="criteria.continentId" topLabel="Any" @update:modelValue="updateContinent")
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0 .col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
label.jjj-label Seeking Remote Work? label.jjj-label Seeking Remote Work?
br br
.form-check.form-check-inline .form-check.form-check-inline
@ -18,19 +18,19 @@ form.container
input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'" input.form-check-input(type="radio" id="remoteNo" name="remoteWork" :checked="criteria.remoteWork === 'no'"
@click="updateValue('remoteWork', 'no')") @click="updateValue('remoteWork', 'no')")
label.form-check-label(for="remoteNo") No label.form-check-label(for="remoteNo") No
.col.col-xs-12.col-sm-6.col-lg-3 .col-xs-12.col-sm-6.col-lg-3
.form-floating .form-floating
input.form-control(type="text" id="skillSearch" placeholder="(free-form text)" :value="criteria.skill" input.form-control(type="text" id="skillSearch" placeholder="(free-form text)" :value="criteria.skill"
@input="updateValue('skill', $event.target.value)") @input="updateValue('skill', $event.target.value)")
label(for="skillSearch") Skill label(for="skillSearch") Skill
.form-text (free-form text) .form-text (free-form text)
.col.col-xs-12.col-sm-6.col-lg-3 .col-xs-12.col-sm-6.col-lg-3
.form-floating .form-floating
input.form-control(type="text" id="bioSearch" placeholder="(free-form text)" :value="criteria.bioExperience" input.form-control(type="text" id="bioSearch" placeholder="(free-form text)" :value="criteria.bioExperience"
@input="updateValue('bioExperience', $event.target.value)") @input="updateValue('bioExperience', $event.target.value)")
label(for="bioSearch") Bio / Experience label(for="bioSearch") Bio / Experience
.form-text (free-form text) .form-text (free-form text)
.row: .col.col-xs-12 .row: .col
br br
button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search button.btn.btn-outline-primary(type="submit" @click.prevent="$emit('search')") Search
</template> </template>

View File

@ -1,15 +1,15 @@
<template lang="pug"> <template lang="pug">
.row.pb-3 .row.pb-3
.col.col-xs-2.col-md-1.align-self-center .col-xs-2.col-md-1.align-self-center
button.btn.btn-sm.btn-outline-danger.rounded-pill(title="Delete" @click.prevent="$emit('remove')") &nbsp;&minus;&nbsp; button.btn.btn-sm.btn-outline-danger.rounded-pill(title="Delete" @click.prevent="$emit('remove')") &nbsp;&minus;&nbsp;
.col.col-xs-10.col-md-6 .col-xs-10.col-md-6
.form-floating .form-floating
input.form-control(type="text" :id="`skillDesc${skill.id}`" maxlength="100" input.form-control(type="text" :id="`skillDesc${skill.id}`" maxlength="100"
placeholder="A skill (language, design technique, process, etc.)" :value="skill.description" placeholder="A skill (language, design technique, process, etc.)" :value="skill.description"
@input="updateValue('description', $event.target.value)") @input="updateValue('description', $event.target.value)")
label.jjj-label(:for="`skillDesc${skill.id}`") Skill label.jjj-label(:for="`skillDesc${skill.id}`") Skill
.form-text A skill (language, design technique, process, etc.) .form-text A skill (language, design technique, process, etc.)
.col.col-xs-12.col-md-5 .col-xs-12.col-md-5
.form-floating .form-floating
input.form-control(type="text" :id="`skillNotes${skill.id}`" maxlength="100" input.form-control(type="text" :id="`skillNotes${skill.id}`" maxlength="100"
placeholder="A further description of the skill (100 characters max)" :value="skill.notes" placeholder="A further description of the skill (100 characters max)" :value="skill.notes"

View File

@ -3,13 +3,11 @@ import App from "./App.vue"
import router from "./router" import router from "./router"
import store, { key } from "./store" import store, { key } from "./store"
import Icon from "./components/Icon.vue" import Icon from "./components/Icon.vue"
import PageTitle from "./components/PageTitle.vue"
const app = createApp(App) const app = createApp(App)
.use(router) .use(router)
.use(store, key) .use(store, key)
app.component("Icon", Icon) app.component("Icon", Icon)
app.component("PageTitle", PageTitle)
app.mount("#app") app.mount("#app")

View File

@ -3,10 +3,9 @@ import {
createWebHistory, createWebHistory,
RouteLocationNormalized, RouteLocationNormalized,
RouteLocationNormalizedLoaded, RouteLocationNormalizedLoaded,
RouteRecordName,
RouteRecordRaw RouteRecordRaw
} from "vue-router" } from "vue-router"
import store from "@/store" import store, { Mutations } from "@/store"
import Home from "@/views/Home.vue" import Home from "@/views/Home.vue"
import LogOn from "@/views/citizen/LogOn.vue" import LogOn from "@/views/citizen/LogOn.vue"
@ -29,124 +28,141 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: "/", path: "/",
name: "Home", name: "Home",
component: Home component: Home,
meta: { title: "Welcome!" }
}, },
{ {
path: "/how-it-works", path: "/how-it-works",
name: "HowItWorks", name: "HowItWorks",
component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue") component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue"),
meta: { title: "How It Works" }
}, },
{ {
path: "/privacy-policy", path: "/privacy-policy",
name: "PrivacyPolicy", name: "PrivacyPolicy",
component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue") component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue"),
meta: { title: "Privacy Policy" }
}, },
{ {
path: "/terms-of-service", path: "/terms-of-service",
name: "TermsOfService", name: "TermsOfService",
component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue") component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue"),
meta: { title: "Terms of Service" }
}, },
// Citizen URLs // Citizen URLs
{ {
path: "/citizen/log-on", path: "/citizen/log-on",
name: "LogOn", name: "LogOn",
component: LogOn component: LogOn,
meta: { title: "Log On" }
}, },
{ {
path: "/citizen/:abbr/authorized", path: "/citizen/:abbr/authorized",
name: "CitizenAuthorized", name: "CitizenAuthorized",
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Authorized.vue") component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Authorized.vue"),
meta: { title: "Logging On" }
}, },
{ {
path: "/citizen/dashboard", path: "/citizen/dashboard",
name: "Dashboard", name: "Dashboard",
component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue") component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue"),
meta: { auth: true, title: "Dashboard" }
}, },
{ {
path: "/citizen/profile", path: "/citizen/profile",
name: "EditProfile", name: "EditProfile",
component: () => import(/* webpackChunkName: "profedit" */ "../views/citizen/EditProfile.vue") component: () => import(/* webpackChunkName: "profedit" */ "../views/citizen/EditProfile.vue"),
meta: { auth: true, title: "Edit Profile" }
}, },
{ {
path: "/citizen/log-off", path: "/citizen/log-off",
name: "LogOff", name: "LogOff",
component: () => import(/* webpackChunkName: "logoff" */ "../views/citizen/LogOff.vue") component: () => import(/* webpackChunkName: "logoff" */ "../views/citizen/LogOff.vue"),
meta: { auth: true, title: "Logging Off" }
}, },
// Job Listing URLs // Job Listing URLs
{ {
path: "/help-wanted", path: "/help-wanted",
name: "HelpWanted", name: "HelpWanted",
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/HelpWanted.vue") component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/HelpWanted.vue"),
meta: { auth: true, title: "Help Wanted" }
}, },
{ {
path: "/listing/:id/edit", path: "/listing/:id/edit",
name: "EditListing", name: "EditListing",
component: () => import(/* webpackChunkName: "jobedit" */ "../views/listing/ListingEdit.vue") component: () => import(/* webpackChunkName: "jobedit" */ "../views/listing/ListingEdit.vue"),
meta: { auth: true, title: "Edit Job Listing" }
}, },
{ {
path: "/listing/:id/expire", path: "/listing/:id/expire",
name: "ExpireListing", name: "ExpireListing",
component: () => import(/* webpackChunkName: "jobedit" */ "../views/listing/ListingExpire.vue") component: () => import(/* webpackChunkName: "jobedit" */ "../views/listing/ListingExpire.vue"),
meta: { auth: true, title: "Expire Job Listing" }
}, },
{ {
path: "/listing/:id/view", path: "/listing/:id/view",
name: "ViewListing", name: "ViewListing",
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue") component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue"),
meta: { auth: true, title: "Loading Job Listing..." }
}, },
{ {
path: "/listings/mine", path: "/listings/mine",
name: "MyListings", name: "MyListings",
component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/MyListings.vue") component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/MyListings.vue"),
meta: { auth: true, title: "My Job Listings" }
}, },
// Profile URLs // Profile URLs
{ {
path: "/profile/:id/view", path: "/profile/:id/view",
name: "ViewProfile", name: "ViewProfile",
component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileView.vue") component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileView.vue"),
meta: { auth: true, title: "Loading Profile..." }
}, },
{ {
path: "/profile/search", path: "/profile/search",
name: "SearchProfiles", name: "SearchProfiles",
component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileSearch.vue") component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileSearch.vue"),
meta: { auth: true, title: "Search Profiles" }
}, },
{ {
path: "/profile/seeking", path: "/profile/seeking",
name: "PublicSearchProfiles", name: "PublicSearchProfiles",
component: () => import(/* webpackChunkName: "seeking" */ "../views/profile/Seeking.vue") component: () => import(/* webpackChunkName: "seeking" */ "../views/profile/Seeking.vue"),
meta: { auth: false, title: "People Seeking Work" }
}, },
// "So Long" URLs // "So Long" URLs
{ {
path: "/so-long/options", path: "/so-long/options",
name: "DeletionOptions", name: "DeletionOptions",
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionOptions.vue") component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionOptions.vue"),
meta: { auth: true, title: "Account Deletion Options" }
}, },
{ {
path: "/so-long/success/:abbr", path: "/so-long/success/:abbr",
name: "DeletionSuccess", name: "DeletionSuccess",
component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue") component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue"),
meta: { auth: false, title: "Account Deletion Success" }
}, },
// Success Story URLs // Success Story URLs
{ {
path: "/success-story/list", path: "/success-story/list",
name: "ListStories", name: "ListStories",
component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryList.vue") component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryList.vue"),
meta: { auth: false, title: "Success Stories" }
}, },
{ {
path: "/success-story/:id/edit", path: "/success-story/:id/edit",
name: "EditStory", name: "EditStory",
component: () => import(/* webpackChunkName: "succedit" */ "../views/success-story/StoryEdit.vue") component: () => import(/* webpackChunkName: "succedit" */ "../views/success-story/StoryEdit.vue"),
meta: { auth: false, title: "Edit Success Story" }
}, },
{ {
path: "/success-story/:id/view", path: "/success-story/:id/view",
name: "ViewStory", name: "ViewStory",
component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryView.vue") component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryView.vue"),
meta: { auth: false, title: "Success Story" }
} }
] ]
/** The routes that do not require logins */
const publicRoutes : Array<RouteRecordName> = [
"Home", "HowItWorks", "PrivacyPolicy", "TermsOfService", "LogOn", "CitizenAuthorized", "PublicSearchProfiles",
"DeletionSuccess"
]
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(process.env.BASE_URL),
@ -159,10 +175,11 @@ const router = createRouter({
// eslint-disable-next-line // eslint-disable-next-line
router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized) => { router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized) => {
if (store.state.user === undefined && !publicRoutes.includes(to.name ?? "")) { if (store.state.user === undefined && (to.meta.auth || false)) {
window.localStorage.setItem(AFTER_LOG_ON_URL, to.fullPath) window.localStorage.setItem(AFTER_LOG_ON_URL, to.fullPath)
return "/citizen/log-on" return "/citizen/log-on"
} }
store.commit(Mutations.SetTitle, to.meta.title ?? "")
}) })
export default router export default router

View File

@ -1,3 +1,4 @@
import { useTitle } from "@vueuse/core"
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, { Continent, Instance, LogOnSuccess } from "../api" import api, { Continent, Instance, LogOnSuccess } from "../api"
@ -6,6 +7,8 @@ import * as Mutations from "./mutations"
/** The state tracked by the application */ /** The state tracked by the application */
export interface State { export interface State {
/** The document's current title */
pageTitle : string
/** The currently logged-on user */ /** The currently logged-on user */
user : LogOnSuccess | undefined user : LogOnSuccess | undefined
/** The state of the log on process */ /** The state of the log on process */
@ -24,9 +27,13 @@ export function useStore () : Store<State> {
return baseUseStore(key) return baseUseStore(key)
} }
/** The application name */
const appName = "Jobs, Jobs, Jobs"
export default createStore({ export default createStore({
state: () : State => { state: () : State => {
return { return {
pageTitle: "",
user: undefined, user: undefined,
logOnState: "<em>Welcome back!</em>", logOnState: "<em>Welcome back!</em>",
continents: [], continents: [],
@ -34,6 +41,10 @@ export default createStore({
} }
}, },
mutations: { mutations: {
[Mutations.SetTitle]: (state, title : string) => {
state.pageTitle = title === "" ? appName : `${title} | ${appName}`
useTitle(state.pageTitle)
},
[Mutations.SetUser]: (state, user : LogOnSuccess) => { state.user = user }, [Mutations.SetUser]: (state, user : LogOnSuccess) => { state.user = user },
[Mutations.ClearUser]: (state) => { state.user = undefined }, [Mutations.ClearUser]: (state) => { state.user = undefined },
[Mutations.SetLogOnState]: (state, message : string) => { state.logOnState = message }, [Mutations.SetLogOnState]: (state, message : string) => { state.logOnState = message },

View File

@ -1,3 +1,6 @@
/** Set the page title */
export const SetTitle = "setTitle"
/** Set the logged-on user */ /** Set the logged-on user */
export const SetUser = "setUser" export const SetUser = "setUser"

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Welcome!")
p &nbsp; p &nbsp;
p. p.
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="How It Works")
h3 How It Works h3 How It Works
h5.pb-3.text-muted: em Last Updated August 29#[sup th], 2021 h5.pb-3.text-muted: em Last Updated August 29#[sup th], 2021
p: em. p: em.

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Privacy Policy")
h3 Privacy Policy h3 Privacy Policy
p: em (as of September 6#[sup th], 2021) p: em (as of September 6#[sup th], 2021)

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Terms of Service")
h3 Terms of Service h3 Terms of Service
p: em (as of September 6#[sup th], 2021) p: em (as of September 6#[sup th], 2021)

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Logging on...")
p &nbsp; p &nbsp;
p(v-html="message") p(v-html="message")
</template> </template>

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article.container article.container
page-title(title="Dashboard")
h3.pb-4 Welcome, {{user.name}} h3.pb-4 Welcome, {{user.name}}
load-data(:load="retrieveData"): .row.row-cols-1.row-cols-md-2 load-data(:load="retrieveData"): .row.row-cols-1.row-cols-md-2
.col: .card.h-100 .col: .card.h-100

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Edit Profile")
h3.pb-3 My Employment Profile h3.pb-3 My Employment Profile
load-data(:load="retrieveData"): form.row.g-3 load-data(:load="retrieveData"): form.row.g-3
.col-12.col-sm-10.col-md-8.col-lg-6 .col-12.col-sm-10.col-md-8.col-lg-6

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Logging off...")
p &nbsp; p &nbsp;
p.fst-italic Logging off&hellip; p.fst-italic Logging off&hellip;
</template> </template>

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Help Wanted")
h3.pb-3 Help Wanted h3.pb-3 Help Wanted
p(v-if="!searched"). p(v-if="!searched").
Enter relevant criteria to find results, or just click &ldquo;Search&rdquo; to see all current job listings. Enter relevant criteria to find results, or just click &ldquo;Search&rdquo; to see all current job listings.

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title="isNew ? 'Add a Job Listing' : 'Edit Job Listing'")
h3.pb-3(v-if="isNew") Add a Job Listing h3.pb-3(v-if="isNew") Add a Job Listing
h3.pb-3(v-else) Edit Job Listing h3.pb-3(v-else) Edit Job Listing
load-data(:load="retrieveData"): form.row.g-3 load-data(:load="retrieveData"): form.row.g-3
@ -44,7 +43,7 @@ import { required } from "@vuelidate/validators"
import api, { Listing, ListingForm, LogOnSuccess } from "@/api" import api, { Listing, ListingForm, LogOnSuccess } from "@/api"
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue" import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from "@/store" import { Mutations, useStore } from "@/store"
import ContinentList from "@/components/ContinentList.vue" import ContinentList from "@/components/ContinentList.vue"
import LoadData from "@/components/LoadData.vue" import LoadData from "@/components/LoadData.vue"
@ -99,6 +98,7 @@ const v$ = useVuelidate(rules, listing, { $lazy: true })
/** Retrieve the listing being edited (or set up the form for a new listing) */ /** Retrieve the listing being edited (or set up the form for a new listing) */
const retrieveData = async (errors : string[]) => { const retrieveData = async (errors : string[]) => {
if (isNew.value) store.commit(Mutations.SetTitle, "Add a Job Listing")
const listResult = isNew.value ? newListing : await api.listings.retreive(id, user) const listResult = isNew.value ? newListing : await api.listings.retreive(id, user)
if (typeof listResult === "string") { if (typeof listResult === "string") {
errors.push(listResult) errors.push(listResult)

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Expire Job Listing")
load-data(:load="retrieveListing") load-data(:load="retrieveListing")
h3.pb-3 Expire Job Listing ({{listing.title}}) h3.pb-3 Expire Job Listing ({{listing.title}})
p: em. p: em.

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title="title")
load-data(:load="retrieveListing") load-data(:load="retrieveListing")
h3 h3
| {{it.listing.title}} | {{it.listing.title}}
@ -24,7 +23,7 @@ import { formatNeededBy } from "./"
import api, { Citizen, ListingForView, LogOnSuccess } from "@/api" import api, { Citizen, ListingForView, LogOnSuccess } from "@/api"
import { citizenName } from "@/App.vue" import { citizenName } from "@/App.vue"
import { toHtml } from "@/markdown" import { toHtml } from "@/markdown"
import { useStore } from "@/store" import { Mutations, useStore } from "@/store"
import LoadData from "@/components/LoadData.vue" import LoadData from "@/components/LoadData.vue"
const store = useStore() const store = useStore()
@ -48,6 +47,7 @@ const retrieveListing = async (errors : string[]) => {
errors.push("Job Listing not found") errors.push("Job Listing not found")
} else { } else {
it.value = listingResp it.value = listingResp
store.commit(Mutations.SetTitle, `${listingResp.listing.title} | Job Listing`)
const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user) const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user)
if (typeof citizenResp === "string") { if (typeof citizenResp === "string") {
errors.push(citizenResp) errors.push(citizenResp)
@ -59,9 +59,6 @@ const retrieveListing = async (errors : string[]) => {
} }
} }
/** The page title (changes once the listing is loaded) */
const title = computed(() => it.value ? `${it.value.listing.title} | Job Listing` : "Loading Job Listing...")
/** The HTML details of the job listing */ /** The HTML details of the job listing */
const details = computed(() => toHtml(it.value?.listing.text ?? "")) const details = computed(() => toHtml(it.value?.listing.text ?? ""))

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="My Job Listings")
h3.pb-3 My Job Listings h3.pb-3 My Job Listings
p: router-link.btn.btn-outline-primary(to="/listing/new/edit") Add a New Job Listing p: router-link.btn.btn-outline-primary(to="/listing/new/edit") Add a New Job Listing
load-data(:load="getListings") load-data(:load="getListings")

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Search Profiles")
h3.pb-3 Search Profiles h3.pb-3 Search Profiles
p(v-if="!searched"). p(v-if="!searched").
Enter one or more criteria to filter results, or just click &ldquo;Search&rdquo; to list all profiles. Enter one or more criteria to filter results, or just click &ldquo;Search&rdquo; to list all profiles.
@ -13,23 +12,25 @@ article
thead: tr thead: tr
th(scope="col") Profile th(scope="col") Profile
th(scope="col") Name th(scope="col") Name
th.text-center(scope="col") Seeking? th.text-center(scope="col" v-if="wideDisplay") Seeking?
th.text-center(scope="col") Remote? th.text-center(scope="col") Remote?
th.text-center(scope="col") Full-Time? th.text-center(scope="col" v-if="wideDisplay") Full-Time?
th(scope="col") Last Updated th(scope="col" v-if="wideDisplay") Last Updated
tbody: tr(v-for="profile in results" :key="profile.citzenId") tbody: tr(v-for="profile in results" :key="profile.citzenId")
td: router-link(:to="`/profile/${profile.citizenId}/view`") View td: router-link(:to="`/profile/${profile.citizenId}/view`") View
td(:class="{ 'fw-bold' : profile.seekingEmployment }") {{profile.displayName}} td(:class="{ 'fw-bold' : profile.seekingEmployment }") {{profile.displayName}}
td.text-center {{yesOrNo(profile.seekingEmployment)}} td.text-center(v-if="wideDisplay") {{yesOrNo(profile.seekingEmployment)}}
td.text-center {{yesOrNo(profile.remoteWork)}} td.text-center {{yesOrNo(profile.remoteWork)}}
td.text-center {{yesOrNo(profile.fullTime)}} td.text-center(v-if="wideDisplay") {{yesOrNo(profile.fullTime)}}
td: full-date(:date="profile.lastUpdatedOn") td(v-if="wideDisplay"): full-date(:date="profile.lastUpdatedOn")
p.pt-3(v-else-if="searched") No results found for the specified criteria p.pt-3(v-else-if="searched") No results found for the specified criteria
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref, watch } from "vue" import { Ref, ref, watch } from "vue"
import { useRoute, useRouter } from "vue-router" import { useRoute, useRouter } from "vue-router"
import { useBreakpoints, breakpointsBootstrapV5 } from "@vueuse/core"
import { yesOrNo } from "@/App.vue" import { yesOrNo } from "@/App.vue"
import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from "@/api" import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from "@/api"
import { queryValue } from "@/router" import { queryValue } from "@/router"
@ -43,6 +44,7 @@ import ProfileSearchForm from "@/components/profile/SearchForm.vue"
const store = useStore() const store = useStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const breakpoints = useBreakpoints(breakpointsBootstrapV5)
/** Any errors encountered while retrieving data */ /** Any errors encountered while retrieving data */
const errors : Ref<string[]> = ref([]) const errors : Ref<string[]> = ref([])
@ -70,6 +72,9 @@ const results : Ref<ProfileSearchResult[]> = ref([])
/** Whether the search criteria should be collapsed */ /** Whether the search criteria should be collapsed */
const isCollapsed = ref(searched.value && results.value.length > 0) const isCollapsed = ref(searched.value && results.value.length > 0)
/** Hide certain columns if the display is too narrow */
const wideDisplay = breakpoints.greater("sm")
/** Set up the page to match its requested state */ /** Set up the page to match its requested state */
const setUpPage = async () => { const setUpPage = async () => {
if (queryValue(route, "searched") === "true") { if (queryValue(route, "searched") === "true") {

View File

@ -34,7 +34,7 @@ import { mdiPencil } from "@mdi/js"
import api, { LogOnSuccess, ProfileForView } from "@/api" import api, { LogOnSuccess, ProfileForView } from "@/api"
import { citizenName } from "@/App.vue" import { citizenName } from "@/App.vue"
import { toHtml } from "@/markdown" import { toHtml } from "@/markdown"
import { useStore } from "@/store" import { Mutations, useStore } from "@/store"
import LoadData from "@/components/LoadData.vue" import LoadData from "@/components/LoadData.vue"
const store = useStore() const store = useStore()
@ -66,14 +66,10 @@ const retrieveProfile = async (errors : string[]) => {
errors.push("Profile not found") errors.push("Profile not found")
} else { } else {
it.value = profileResp it.value = profileResp
store.commit(Mutations.SetTitle, `Employment profile for ${citizenName(profileResp.citizen)}`)
} }
} }
/** The title of the page (changes once the profile is loaded) */
const title = computed(() => it.value
? `Employment profile for ${citizenName(it.value.citizen)}`
: "Loading Profile...")
/** The HTML version of the citizen's professional biography */ /** The HTML version of the citizen's professional biography */
const bioHtml = computed(() => toHtml(it.value?.profile.biography ?? "")) const bioHtml = computed(() => toHtml(it.value?.profile.biography ?? ""))

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="People Seeking Work")
h3.pb-3 People Seeking Work h3.pb-3 People Seeking Work
p(v-if="!searched"). p(v-if="!searched").
Enter one or more criteria to filter results, or just click &ldquo;Search&rdquo; to list all profiles. Enter one or more criteria to filter results, or just click &ldquo;Search&rdquo; to list all profiles.

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Account Deletion Options")
h3.pb-3 Account Deletion Options h3.pb-3 Account Deletion Options
h4.pb-3 Option 1 &ndash; Delete Your Profile h4.pb-3 Option 1 &ndash; Delete Your Profile
p. p.

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Account Deletion Success")
h3.pb-3 Account Deletion Success h3.pb-3 Account Deletion Success
p. p.
Your account has been successfully deleted. To revoke the permissions you have previously granted to this Your account has been successfully deleted. To revoke the permissions you have previously granted to this

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title="title")
h3.pb-3 {{title}} h3.pb-3 {{title}}
load-data(:load="retrieveStory") load-data(:load="retrieveStory")
p(v-if="isNew"). p(v-if="isNew").
@ -26,7 +25,7 @@ import useVuelidate from "@vuelidate/core"
import api, { LogOnSuccess, StoryForm } from "@/api" import api, { LogOnSuccess, StoryForm } from "@/api"
import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue" import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from "@/store" import { Mutations, useStore } from "@/store"
import LoadData from "@/components/LoadData.vue" import LoadData from "@/components/LoadData.vue"
import MarkdownEditor from "@/components/MarkdownEditor.vue" import MarkdownEditor from "@/components/MarkdownEditor.vue"
@ -45,9 +44,6 @@ const id = route.params.id as string
/** Whether this is a new story */ /** Whether this is a new story */
const isNew = computed(() => id === "new") const isNew = computed(() => id === "new")
/** The page title */
const title = computed(() => isNew.value ? "Tell Your Success Story" : "Edit Success Story")
/** The form for editing the story */ /** The form for editing the story */
const story = reactive(new StoryForm()) const story = reactive(new StoryForm())
@ -64,6 +60,7 @@ const v$ = useVuelidate(rules, story, { $lazy: true })
const retrieveStory = async (errors : string[]) => { const retrieveStory = async (errors : string[]) => {
if (isNew.value) { if (isNew.value) {
story.id = "new" story.id = "new"
store.commit(Mutations.SetTitle, "Tell Your Success Story")
} else { } else {
const storyResult = await api.success.retrieve(id, user) const storyResult = await api.success.retrieve(id, user)
if (typeof storyResult === "string") { if (typeof storyResult === "string") {

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Success Stories")
h3.pb-3 Success Stories h3.pb-3 Success Stories
load-data(:load="retrieveStories") load-data(:load="retrieveStories")
table.table.table-sm.table-hover(v-if="stories?.length > 0") table.table.table-sm.table-hover(v-if="stories?.length > 0")

View File

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title="Success Story")
load-data(:load="retrieveStory") load-data(:load="retrieveStory")
h3 h3
| {{citizenName}}&rsquo;s Success Story | {{citizenName}}&rsquo;s Success Story

View File

@ -2,5 +2,14 @@ module.exports = {
transpileDependencies: [ transpileDependencies: [
'vuetify' 'vuetify'
], ],
outputDir: '../Api/wwwroot' outputDir: '../Api/wwwroot',
configureWebpack: {
module: {
rules: [{
test: /\.mjs$/,
include: /node_modules/,
type: "javascript/auto"
}]
}
}
} }