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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,45 @@
<template lang="pug"> <template>
nav <nav>
template(v-if="isLoggedOn") <template v-if="isLoggedOn">
router-link(to="/citizen/dashboard" @click="hide") #[icon(:icon="mdiViewDashboardVariant")]&nbsp; Dashboard <router-link to="/citizen/dashboard" @click="hide">
router-link(to="/help-wanted" @click="hide") #[icon(:icon="mdiNewspaperVariantMultipleOutline")]&nbsp; Help Wanted! <icon :icon="mdiViewDashboardVariant" />&nbsp; Dashboard
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 to="/help-wanted" @click="hide">
.separator <icon :icon="mdiNewspaperVariantMultipleOutline" />&nbsp; Help Wanted!
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 to="/profile/search" @click="hide">
.separator <icon :icon="mdiViewListOutline" />&nbsp; Employment Profiles
router-link(to="/citizen/log-off" @click="hide") #[icon(:icon="mdiLogoutVariant")]&nbsp; Log Off </router-link>
template(v-else) <router-link to="/success-story/list" @click="hide">
router-link(to="/" @click="hide") #[icon(:icon="mdiHome")]&nbsp; Home <icon :icon="mdiThumbUp" />&nbsp; Success Stories
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 <div class="separator"></div>
router-link(to="/how-it-works" @click="hide") #[icon(:icon="mdiHelpCircleOutline")]&nbsp; How It Works <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> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,17 @@
<template lang="pug"> <template>
article <article>
p &nbsp; <p>&nbsp;</p>
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
finding employment. This will enable them to continue providing value-for-value to Adam and John, as they continue 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. their work deconstructing the misinformation that passes for news on a day-to-day basis.
p </p>
| Do you not understand the terms in the paragraph above? No worries; just head over to <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 <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.
| #[em #[audio-clip(clip="thats-true") (that&rsquo;s true!)]] and find out what you&rsquo;re missing. </p>
</article>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

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

View File

@ -138,6 +138,8 @@ open JobsJobsJobs.Domain
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Citizens = module Citizens =
open Npgsql
/// Delete a citizen by their ID /// Delete a citizen by their ID
let deleteById citizenId = backgroundTask { let deleteById citizenId = backgroundTask {
let! _ = let! _ =
@ -160,8 +162,27 @@ module Citizens =
} }
/// Save a citizen /// Save a citizen
let save (citizen : Citizen) = let private saveCitizen (citizen : Citizen) connProps =
connection () |> saveDocument Table.Citizen (CitizenId.toString citizen.Id) <| mkDoc citizen 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 /// Attempt a user log on
let tryLogOn email (pwCheck : string -> bool) now = backgroundTask { let tryLogOn email (pwCheck : string -> bool) now = backgroundTask {
@ -184,18 +205,18 @@ module Citizens =
| Some it -> return it | Some it -> return it
| None -> | None ->
let it = { SecurityInfo.empty with Id = citizen.Id } let it = { SecurityInfo.empty with Id = citizen.Id }
do! saveDocument Table.SecurityInfo citizenId connProps (mkDoc it) do! saveSecurity it connProps
return it return it
} }
if info.AccountLocked then return Error "Log on unsuccessful (Account Locked)" if info.AccountLocked then return Error "Log on unsuccessful (Account Locked)"
elif pwCheck citizen.PasswordHash then elif pwCheck citizen.PasswordHash then
do! saveDocument Table.SecurityInfo citizenId connProps (mkDoc { info with FailedLogOnAttempts = 0 }) do! saveSecurity { info with FailedLogOnAttempts = 0 } connProps
do! saveDocument Table.Citizen citizenId connProps (mkDoc { citizen with LastSeenOn = now }) do! saveCitizen { citizen with LastSeenOn = now } connProps
return Ok { citizen with LastSeenOn = now } return Ok { citizen with LastSeenOn = now }
else else
let locked = info.FailedLogOnAttempts >= 4 let locked = info.FailedLogOnAttempts >= 4
do! mkDoc { info with FailedLogOnAttempts = info.FailedLogOnAttempts + 1; AccountLocked = locked } do! { info with FailedLogOnAttempts = info.FailedLogOnAttempts + 1; AccountLocked = locked }
|> saveDocument Table.SecurityInfo citizenId connProps |> saveSecurity <| connProps
return Error $"""Log on unsuccessful{if locked then " - Account is now locked" else ""}""" return Error $"""Log on unsuccessful{if locked then " - Account is now locked" else ""}"""
| None -> return Error "Log on unsuccessful" | 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})" return Error $"Could not get token ({codeResult.StatusCode:D}: {codeResult.ReasonPhrase})"
} }
open System.Text
open JobsJobsJobs.Domain 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 Microsoft.IdentityModel.Tokens
open System.IdentityModel.Tokens.Jwt open System.IdentityModel.Tokens.Jwt
open System.Security.Claims open System.Security.Claims
open System.Text
/// Create a JSON Web Token for this citizen to use for further requests to this API /// Create a JSON Web Token for this citizen to use for further requests to this API
let createJwt (citizen : Citizen) (cfg : AuthOptions) = let createJwt (citizen : Citizen) (cfg : AuthOptions) =

View File

@ -6,6 +6,7 @@ open JobsJobsJobs.Domain
open JobsJobsJobs.Domain.SharedTypes open JobsJobsJobs.Domain.SharedTypes
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open NodaTime
/// Handler to return the files required for the Vue client app /// Handler to return the files required for the Vue client app
module Vue = module Vue =
@ -122,7 +123,15 @@ module Citizen =
LastSeenOn = now LastSeenOn = now
} }
let citizen = { noPass with PasswordHash = PasswordHasher().HashPassword (noPass, form.Password) } 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 // TODO: generate auth code and e-mail confirmation
return! ok next ctx return! ok next ctx
} }