Version 3 #40

Merged
danieljsummers merged 67 commits from version-2-3 into main 2023-02-02 23:47:28 +00:00
25 changed files with 413 additions and 256 deletions
Showing only changes of commit 4ca84c1bc1 - Show all commits

View File

@ -3,7 +3,7 @@
<app-nav />
<div class="jjj-main">
<title-bar />
<main class="container-fluid">
<main class="jjj-content container-fluid">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
@ -84,6 +84,11 @@ label.jjj-required::after
flex-direction: row
.jjj-main
flex-grow: 1
display: flex
flex-flow: column
min-height: 100vh
.jjj-content
flex-grow: 2
// Route transitions
.fade-enter-active,
.fade-leave-active

View File

@ -1,5 +1,8 @@
<template lang="pug">
span(@click="playFile") #[slot] #[audio(:id="clip"): source(:src="clipSource")]
<template>
<span @click="playFile">
<slot />
<audio :id="clip"><source :src="clipSource" /></audio>
</span>
</template>
<script setup lang="ts">

View File

@ -1,8 +1,12 @@
<template lang="pug">
.card: .card-body
h6.card-title
a(href="#" :class="{ 'cp-c': collapsed, 'cp-o': !collapsed }" @click.prevent="toggle") {{headerText}}
slot(v-if="!collapsed")
<template>
<div class="card">
<div class="card-body">
<h6 class="card-title">
<a href="#" :class="{ 'cp-c': collapsed, 'cp-o': !collapsed }" @click.prevent="toggle">{{headerText}}</a>
</h6>
<slot v-if="!collapsed" />
</div>
</div>
</template>
<script setup lang="ts">

View File

@ -1,11 +1,13 @@
<template lang="pug">
.form-floating
select.form-select(id="continentId" :class="{ 'is-invalid': isInvalid}" :value="continentId"
@change="continentChanged")
option(value="") &ndash; {{emptyLabel}} &ndash;
option(v-for="c in continents" :key="c.id" :value="c.id") {{c.name}}
label.jjj-required(for="continentId") Continent
.invalid-feedback Please select a continent
<template>
<div class="form-floating">
<select id="continentId" :class="{ 'form-select': true, 'is-invalid': isInvalid}" :value="continentId"
@change="continentChanged">
<option value="">&ndash; {{emptyLabel}} &ndash;</option>
<option v-for="c in continents" :key="c.id" :value="c.id">{{c.name}}</option>
</select>
<label class="jjj-required" for="continentId">Continent</label>
</div>
<div class="invalid-feedback">Please select a continent</div>
</template>
<script setup lang="ts">

View File

@ -1,8 +1,11 @@
<template lang="pug">
template(v-if="errors.length > 0")
p The following error#[template(v-if="errors.length !== 1") s] occurred:
ul: li(v-for="(error, idx) in errors" :key="idx") {{error}}
slot(v-else)
<template>
<template v-if="errors.length > 0">
<p>The following error<template v-if="errors.length !== 1">s</template> occurred:</p>
<ul>
<li v-for="(error, idx) in errors" :key="idx">{{error}}</li>
</ul>
</template>
<slot v-else />
</template>
<script setup lang="ts">

View File

@ -1,5 +1,5 @@
<template lang="pug">
template(v-if="true") {{formatted}}
<template>
<template v-if="true">{{formatted}}</template>
</template>
<script setup lang="ts">

View File

@ -1,5 +1,5 @@
<template lang="pug">
template(v-if="true") {{formatted}}
<template>
<template v-if="true">{{formatted}}</template>
</template>
<script setup lang="ts">

View File

@ -1,5 +1,7 @@
<template lang="pug">
svg(viewbox="0 0 24 24"): path(:fill="color || 'white'" :d="icon")
<template>
<svg viewbox="0 0 24 24">
<path :fill="color || 'white'" :d="icon" />
</svg>
</template>
<script setup lang="ts">

View File

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

View File

@ -1,7 +1,8 @@
<template lang="pug">
div(v-if="loading") Loading&hellip;
error-list(v-else :errors="errors")
slot
<template>
<div v-if="loading">Loading&hellip;</div>
<error-list v-else :errors="errors">
<slot />
</error-list>
</template>
<script setup lang="ts">

View File

@ -1,15 +1,18 @@
<template lang="pug">
.col-12
nav.nav.nav-pills.pb-1
button(:class="sourceClass" @click.prevent="showMarkdown") Markdown
| &nbsp;
button(:class="previewClass" @click.prevent="showPreview") Preview
section.preview(v-if="preview" v-html="previewHtml")
.form-floating(v-else)
textarea.form-control.md-edit(:id="id" :class="{ 'is-invalid': isInvalid }" rows="10" v-text="text"
@input="$emit('update:text', $event.target.value)")
.invalid-feedback Please enter some text for {{label}}
label(:for="id") {{label}}
<template>
<div class="col-12">
<nav class="nav nav-pills pb-1">
<button :class="sourceClass" @click.prevent="showMarkdown">Markdown</button>
&nbsp;
<button :class="previewClass" @click.prevent="showPreview">Preview</button>
</nav>
<section class="preview" v-if="preview" v-html="previewHtml" aria-label="Rendered Markdown preview" />
<div class="form-floating" v-else>
<textarea :id="id" class="form-control md-edit" :class="{ 'is-invalid': isInvalid }" rows="10" v-text="text"
@input="$emit('update:text', $event.target.value)"></textarea>
<div class="invalid-feedback">Please enter some text for {{label}}</div>
<label :for="id">{{label}}</label>
</div>
</div>
</template>
<script setup lang="ts">

View File

@ -1,11 +1,19 @@
<template lang="pug">
.modal.fade(id="maybeSaveModal" tabindex="-1" aria-labelledby="maybeSaveLabel" aria-hidden="true"): .modal-dialog: .modal-content
.modal-header: h5.modal-title(id="maybeSaveLabel") Unsaved Changes
.modal-body You have modified the data on this page since it was last saved. What would you like to do?
.modal-footer
button.btn.btn-secondary(type="button" @click.prevent="close") Stay on This Page
button.btn.btn-primary(type="button" @click.prevent="save") Save Changes
button.btn.btn-danger(type="button" @click.prevent="discard") Discard Changes
<template>
<div id="maybeSaveModal" class="modal fade" tabindex="-1" aria-labelledby="maybeSaveLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header"><h5 id="maybeSaveLabel" class="modal-title">Unsaved Changes</h5></div>
<div class="modal-body">
You have modified the data on this page since it was last saved. What would you like to do?
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" @click.prevent="close">Stay on This Page</button>
<button class="btn btn-primary" type="button" @click.prevent="save">Save Changes</button>
<button class="btn btn-danger" type="button" @click.prevent="discard">Discard Changes</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">

View File

@ -1,7 +1,10 @@
<template lang="pug">
footer: p.text-muted.
Jobs, Jobs, Jobs v{{appVersion}} &bull; #[router-link(to="/privacy-policy") Privacy Policy]
&bull; #[router-link(to="/terms-of-service") Terms of Service]
<template>
<footer>
<p class="text-muted">
Jobs, Jobs, Jobs v{{appVersion}} &bull; <router-link to="/privacy-policy">Privacy Policy</router-link>
&bull; <router-link to="/terms-of-service">Terms of Service</router-link>
</p>
</footer>
</template>
<script setup lang="ts">

View File

@ -1,20 +1,45 @@
<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>
<nav>
<template v-if="isLoggedOn">
<router-link to="/citizen/dashboard" @click="hide">
<icon :icon="mdiViewDashboardVariant" />&nbsp; Dashboard
</router-link>
<router-link to="/help-wanted" @click="hide">
<icon :icon="mdiNewspaperVariantMultipleOutline" />&nbsp; Help Wanted!
</router-link>
<router-link to="/profile/search" @click="hide">
<icon :icon="mdiViewListOutline" />&nbsp; Employment Profiles
</router-link>
<router-link to="/success-story/list" @click="hide">
<icon :icon="mdiThumbUp" />&nbsp; Success Stories
</router-link>
<div class="separator"></div>
<router-link to="/listings/mine" @click="hide">
<icon :icon="mdiSignText" />&nbsp; My Job Listings
</router-link>
<router-link to="/citizen/profile" @click="hide">
<icon :icon="mdiPencil" />&nbsp; My Employment Profile
</router-link>
<div class="separator"></div>
<router-link to="/citizen/log-off" @click="hide">
<icon :icon="mdiLogoutVariant" />&nbsp; Log Off
</router-link>
</template>
<template v-else>
<router-link to="/" @click="hide">
<icon :icon="mdiHome" />&nbsp; Home
</router-link>
<router-link to="/profile/seeking" @click="hide">
<icon :icon="mdiViewListOutline" />&nbsp; Job Seekers
</router-link>
<router-link to="/citizen/log-on" @click="hide">
<icon :icon="mdiLoginVariant" />&nbsp; Log On
</router-link>
</template>
<router-link to="/how-it-works" @click="hide">
<icon :icon="mdiHelpCircleOutline" />&nbsp; How It Works
</router-link>
</nav>
</template>
<script setup lang="ts">

View File

@ -1,13 +1,17 @@
<template lang="pug">
#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 &nbsp;
app-links
<template>
<div id="mobileMenu" class="offcanvas offcanvas-end" v-if="showMobileMenu" tabindex="-1"
aria-labelledby="mobileMenuLabel">
<div class="offcanvas-header">
<h5 id="mobileMenuLabel">Menu</h5>
<button class="btn-close text-reset" type="button" data-bs-dismiss="offcanvas" aria-label="Close" />
</div>
<div class="offcanvas-body"><app-links /></div>
</div>
<aside class="collapse show p-3" v-else>
<p class="home-link pb-3"><router-link to="/">Jobs, Jobs, Jobs</router-link></p>
<p>&nbsp;</p>
<app-links />
</aside>
</template>
<script setup lang="ts">

View File

@ -1,6 +1,7 @@
<template lang="pug">
div(aria-live="polite" aria-atomic="true" id="toastHost")
.toast-container.position-absolute.p-3.bottom-0.start-50.translate-middle-x(id="toasts")
<template>
<div id="toastHost" aria-live="polite" aria-atomic="true">
<div id="toasts" class="toast-container position-absolute p-3 bottom-0 start-50 translate-middle-x"></div>
</div>
</template>
<script lang="ts">

View File

@ -1,12 +1,16 @@
<template lang="pug">
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.navbar-text.
(&hellip;and Jobs &ndash; #[audio-clip(clip="pelosi-jobs") Let&rsquo;s Vote for Jobs!])
<template>
<nav class="navbar navbar-dark" v-if="showMobileHeader">
<span class="navbar-text"><router-link to="/">Jobs, Jobs, Jobs</router-link></span>
<button class="btn" data-bs-toggle="offcanvas" data-bs-target="#mobileMenu" aria-controls="mobileMenu">
<icon :icon="mdiMenu" />
</button>
</nav>
<nav class="navbar navbar-light bg-light" v-else>
<span>&nbsp;</span>
<span class="navbar-text">
(&hellip;and Jobs &ndash; <audio-clip clip="pelosi-jobs">Let&rsquo;s Vote for Jobs!</audio-clip>)
</span>
</nav>
</template>
<script setup lang="ts">

View File

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

View File

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

View File

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

View File

@ -1,16 +1,17 @@
<template lang="pug">
article
p &nbsp;
p.
<template>
<article>
<p>&nbsp;</p>
<p>
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in
finding employment. This will enable them to continue providing value-for-value to Adam and John, as they continue
their work deconstructing the misinformation that passes for news on a day-to-day basis.
p
| Do you not understand the terms in the paragraph above? No worries; just head over to
|
a(href="https://noagendashow.net" target="_blank") The Best Podcast in the Universe
= " "
| #[em #[audio-clip(clip="thats-true") (that&rsquo;s true!)]] and find out what you&rsquo;re missing.
</p>
<p>
Do you not understand the terms in the paragraph above? No worries; just head over to
<a href="https://noagendashow.net" target="_blank" rel="noopener">The Best Podcast in the Universe</a> <em>
<audio-clip clip="thats-true">(that&rsquo;s true!)</audio-clip></em> and find out what you&rsquo;re missing.
</p>
</article>
</template>
<script setup lang="ts">

View File

@ -1,49 +1,53 @@
<template>
<article>
<h3 class="pb-3">Register</h3>
<form class="row g-3">
<form class="row g-3" novalidate>
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="text" id="firstName" v-model="v$.firstName.$model" placeholder="First Name">
<div v-if="v$.firstName.$error" class="text-danger">Please enter your first name</div>
<div class="form-floating has-validation">
<input type="text" id="firstName" :class="{'form-control': true, 'is-invalid': v$.firstName.$error}"
v-model="v$.firstName.$model" placeholder="First Name">
<div class="invalid-feedback">Please enter your first name</div>
<label class="jjj-required" for="firstName">First Name</label>
</div>
</div>
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="text" id="lastName" v-model="v$.lastName.$model" placeholder="Last Name">
<div v-if="v$.lastName.$error" class="text-danger">Please enter your last name</div>
<input type="text" id="lastName" :class="{'form-control': true, 'is-invalid': v$.lastName.$error}"
v-model="v$.lastName.$model" placeholder="Last Name">
<div class="invalid-feedback">Please enter your last name</div>
<label class="jjj-required" for="firstName">Last Name</label>
</div>
</div>
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="text" id="displayName" v-model="v$.displayName.$model"
<input type="text" id="displayName" class="form-control" v-model="v$.displayName.$model"
placeholder="Display Name">
<label for="displayName">Display Name</label>
<div class="form-text"><em>Optional; overrides "FirstName LastName"</em></div>
<div class="form-text"><em>Optional; overrides first/last for display</em></div>
</div>
</div>
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="email" id="email" v-model="v$.email.$model" placeholder="E-mail Address">
<div v-if="v$.email.$error" class="text-danger">Please enter a valid e-mail address</div>
<input type="email" id="email" :class="{'form-control': true, 'is-invalid': v$.email.$error}"
v-model="v$.email.$model" placeholder="E-mail Address">
<div class="invalid-feedback">Please enter a valid e-mail address</div>
<label class="jjj-required" for="email">E-mail Address</label>
</div>
</div>
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="password" id="password" v-model="v$.password.$model" placeholder="Password"
minlength="8">
<div v-if="v$.password.$error" class="text-danger">Please enter a password at least 8 characters long</div>
<input type="password" id="password" :class="{'form-control': true, 'is-invalid': v$.password.$error}"
v-model="v$.password.$model" placeholder="Password">
<div class="invalid-feedback">Please enter a password at least 8 characters long</div>
<label class="jjj-required" for="password">Password</label>
</div>
</div>
<div class="col-6 col-xl-4">
<div class="form-floating">
<input class="form-control" type="password" id="confirmPassword" v-model="v$.confirmPassword.$model"
placeholder="Confirm Password">
<div v-if="v$.confirmPassword.$error" class="text-danger">The passwords do not match</div>
<input type="password" id="confirmPassword"
:class="{'form-control': true, 'is-invalid': v$.confirmPassword.$error}"
v-model="v$.confirmPassword.$model" placeholder="Confirm Password">
<div class="invalid-feedback">The passwords do not match</div>
<label class="jjj-required" for="confirmPassword">Confirm Password</label>
</div>
</div>

View File

@ -138,6 +138,8 @@ open JobsJobsJobs.Domain
[<RequireQualifiedAccess>]
module Citizens =
open Npgsql
/// Delete a citizen by their ID
let deleteById citizenId = backgroundTask {
let! _ =
@ -160,8 +162,27 @@ module Citizens =
}
/// Save a citizen
let save (citizen : Citizen) =
connection () |> saveDocument Table.Citizen (CitizenId.toString citizen.Id) <| mkDoc citizen
let private saveCitizen (citizen : Citizen) connProps =
saveDocument Table.Citizen (CitizenId.toString citizen.Id) connProps (mkDoc citizen)
/// Save security information for a citizen
let private saveSecurity (security : SecurityInfo) connProps =
saveDocument Table.SecurityInfo (CitizenId.toString security.Id) connProps (mkDoc security)
/// Save a citizen
let save citizen =
saveCitizen citizen (connection ())
/// Register a citizen (saves citizen and security settings)
let register citizen (security : SecurityInfo) = backgroundTask {
let connProps = connection ()
use conn = Sql.createConnection connProps
do! conn.OpenAsync ()
use! txn = conn.BeginTransactionAsync ()
do! saveCitizen citizen connProps
do! saveSecurity security connProps
do! txn.CommitAsync ()
}
/// Attempt a user log on
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
@ -184,18 +205,18 @@ module Citizens =
| Some it -> return it
| None ->
let it = { SecurityInfo.empty with Id = citizen.Id }
do! saveDocument Table.SecurityInfo citizenId connProps (mkDoc it)
do! saveSecurity it connProps
return it
}
if info.AccountLocked then return Error "Log on unsuccessful (Account Locked)"
elif pwCheck citizen.PasswordHash then
do! saveDocument Table.SecurityInfo citizenId connProps (mkDoc { info with FailedLogOnAttempts = 0 })
do! saveDocument Table.Citizen citizenId connProps (mkDoc { citizen with LastSeenOn = now })
do! saveSecurity { info with FailedLogOnAttempts = 0 } connProps
do! saveCitizen { citizen with LastSeenOn = now } connProps
return Ok { citizen with LastSeenOn = now }
else
let locked = info.FailedLogOnAttempts >= 4
do! mkDoc { info with FailedLogOnAttempts = info.FailedLogOnAttempts + 1; AccountLocked = locked }
|> saveDocument Table.SecurityInfo citizenId connProps
do! { info with FailedLogOnAttempts = info.FailedLogOnAttempts + 1; AccountLocked = locked }
|> saveSecurity <| connProps
return Error $"""Log on unsuccessful{if locked then " - Account is now locked" else ""}"""
| None -> return Error "Log on unsuccessful"
}

View File

@ -76,12 +76,17 @@ let verifyWithMastodon (authCode : string) (inst : MastodonInstance) rtnHost (lo
return Error $"Could not get token ({codeResult.StatusCode:D}: {codeResult.ReasonPhrase})"
}
open System.Text
open JobsJobsJobs.Domain
/// Create a confirmation or password reset token for a user
let createToken (citizen : Citizen) =
Convert.ToBase64String (Guid.NewGuid().ToByteArray () |> Array.append (Encoding.UTF8.GetBytes citizen.Email))
open Microsoft.IdentityModel.Tokens
open System.IdentityModel.Tokens.Jwt
open System.Security.Claims
open System.Text
/// Create a JSON Web Token for this citizen to use for further requests to this API
let createJwt (citizen : Citizen) (cfg : AuthOptions) =

View File

@ -6,6 +6,7 @@ open JobsJobsJobs.Domain
open JobsJobsJobs.Domain.SharedTypes
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open NodaTime
/// Handler to return the files required for the Vue client app
module Vue =
@ -122,7 +123,15 @@ module Citizen =
LastSeenOn = now
}
let citizen = { noPass with PasswordHash = PasswordHasher().HashPassword (noPass, form.Password) }
do! Citizens.save citizen
let security =
{ SecurityInfo.empty with
Id = citizen.Id
AccountLocked = true
Token = Some (Auth.createToken citizen)
TokenUsage = Some "confirm"
TokenExpires = Some (now + (Duration.FromDays 3))
}
do! Citizens.register citizen security
// TODO: generate auth code and e-mail confirmation
return! ok next ctx
}