Version 3 #40
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<template lang="pug">
|
||||
.form-floating
|
||||
select.form-select(id="continentId" :class="{ 'is-invalid': isInvalid}" :value="continentId"
|
||||
@change="continentChanged")
|
||||
option(value="") – {{emptyLabel}} –
|
||||
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="">– {{emptyLabel}} –</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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template lang="pug">
|
||||
template(v-if="true") {{formatted}}
|
||||
<template>
|
||||
<template v-if="true">{{formatted}}</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template lang="pug">
|
||||
template(v-if="true") {{formatted}}
|
||||
<template>
|
||||
<template v-if="true">{{formatted}}</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template lang="pug">
|
||||
div(v-if="loading") Loading…
|
||||
error-list(v-else :errors="errors")
|
||||
slot
|
||||
<template>
|
||||
<div v-if="loading">Loading…</div>
|
||||
<error-list v-else :errors="errors">
|
||||
<slot />
|
||||
</error-list>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
<template lang="pug">
|
||||
.col-12
|
||||
nav.nav.nav-pills.pb-1
|
||||
button(:class="sourceClass" @click.prevent="showMarkdown") Markdown
|
||||
|
|
||||
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>
|
||||
|
||||
<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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template lang="pug">
|
||||
footer: p.text-muted.
|
||||
Jobs, Jobs, Jobs v{{appVersion}} • #[router-link(to="/privacy-policy") Privacy Policy]
|
||||
• #[router-link(to="/terms-of-service") Terms of Service]
|
||||
<template>
|
||||
<footer>
|
||||
<p class="text-muted">
|
||||
Jobs, Jobs, Jobs v{{appVersion}} • <router-link to="/privacy-policy">Privacy Policy</router-link>
|
||||
• <router-link to="/terms-of-service">Terms of Service</router-link>
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,20 +1,45 @@
|
|||
<template lang="pug">
|
||||
nav
|
||||
template(v-if="isLoggedOn")
|
||||
router-link(to="/citizen/dashboard" @click="hide") #[icon(:icon="mdiViewDashboardVariant")] Dashboard
|
||||
router-link(to="/help-wanted" @click="hide") #[icon(:icon="mdiNewspaperVariantMultipleOutline")] Help Wanted!
|
||||
router-link(to="/profile/search" @click="hide") #[icon(:icon="mdiViewListOutline")] Employment Profiles
|
||||
router-link(to="/success-story/list" @click="hide") #[icon(:icon="mdiThumbUp")] Success Stories
|
||||
.separator
|
||||
router-link(to="/listings/mine" @click="hide") #[icon(:icon="mdiSignText")] My Job Listings
|
||||
router-link(to="/citizen/profile" @click="hide") #[icon(:icon="mdiPencil")] My Employment Profile
|
||||
.separator
|
||||
router-link(to="/citizen/log-off" @click="hide") #[icon(:icon="mdiLogoutVariant")] Log Off
|
||||
template(v-else)
|
||||
router-link(to="/" @click="hide") #[icon(:icon="mdiHome")] Home
|
||||
router-link(to="/profile/seeking" @click="hide") #[icon(:icon="mdiViewListOutline")] Job Seekers
|
||||
router-link(to="/citizen/log-on" @click="hide") #[icon(:icon="mdiLoginVariant")] Log On
|
||||
router-link(to="/how-it-works" @click="hide") #[icon(:icon="mdiHelpCircleOutline")] How It Works
|
||||
<template>
|
||||
<nav>
|
||||
<template v-if="isLoggedOn">
|
||||
<router-link to="/citizen/dashboard" @click="hide">
|
||||
<icon :icon="mdiViewDashboardVariant" /> Dashboard
|
||||
</router-link>
|
||||
<router-link to="/help-wanted" @click="hide">
|
||||
<icon :icon="mdiNewspaperVariantMultipleOutline" /> Help Wanted!
|
||||
</router-link>
|
||||
<router-link to="/profile/search" @click="hide">
|
||||
<icon :icon="mdiViewListOutline" /> Employment Profiles
|
||||
</router-link>
|
||||
<router-link to="/success-story/list" @click="hide">
|
||||
<icon :icon="mdiThumbUp" /> Success Stories
|
||||
</router-link>
|
||||
<div class="separator"></div>
|
||||
<router-link to="/listings/mine" @click="hide">
|
||||
<icon :icon="mdiSignText" /> My Job Listings
|
||||
</router-link>
|
||||
<router-link to="/citizen/profile" @click="hide">
|
||||
<icon :icon="mdiPencil" /> My Employment Profile
|
||||
</router-link>
|
||||
<div class="separator"></div>
|
||||
<router-link to="/citizen/log-off" @click="hide">
|
||||
<icon :icon="mdiLogoutVariant" /> Log Off
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link to="/" @click="hide">
|
||||
<icon :icon="mdiHome" /> Home
|
||||
</router-link>
|
||||
<router-link to="/profile/seeking" @click="hide">
|
||||
<icon :icon="mdiViewListOutline" /> Job Seekers
|
||||
</router-link>
|
||||
<router-link to="/citizen/log-on" @click="hide">
|
||||
<icon :icon="mdiLoginVariant" /> Log On
|
||||
</router-link>
|
||||
</template>
|
||||
<router-link to="/how-it-works" @click="hide">
|
||||
<icon :icon="mdiHelpCircleOutline" /> How It Works
|
||||
</router-link>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -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
|
||||
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> </p>
|
||||
<app-links />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
span.navbar-text.
|
||||
(…and Jobs – #[audio-clip(clip="pelosi-jobs") Let’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> </span>
|
||||
<span class="navbar-text">
|
||||
(…and Jobs – <audio-clip clip="pelosi-jobs">Let’s Vote for Jobs!</audio-clip>)
|
||||
</span>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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')") −
|
||||
.col-xs-10.col-md-6
|
||||
.form-floating
|
||||
input.form-control(type="text" :id="`skillDesc${skill.id}`" maxlength="100"
|
||||
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"
|
||||
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)
|
||||
<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')"> − </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 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 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">
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<template lang="pug">
|
||||
article
|
||||
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’s true!)]] and find out what you’re missing.
|
||||
<template>
|
||||
<article>
|
||||
<p> </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>
|
||||
<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’s true!)</audio-clip></em> and find out what you’re missing.
|
||||
</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user