web: EntryProposal form implementation.

This commit is contained in:
2021-10-26 01:46:59 -05:00
parent e055bee0f3
commit 3675c6054a
32 changed files with 705 additions and 203 deletions

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@ -1,18 +0,0 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js App" />
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue';
export default {
name: 'Home',
components: {
HelloWorld,
},
};
</script>

View File

@ -0,0 +1,113 @@
@import "~@/styles/forSize.mixin";
#event-proposal {
margin: 2em auto;
header {
background-color: #FFFCFC;
border-bottom: thin solid #aaa;
margin: 0;
padding: 0.5em;
h1, h2 {
margin-top: 0.5em;
margin-bottom: 0;
padding: 0;
}
}
&.success header { background-color: #38b00010; }
&.error header { background-color: #d9042910; }
form {
display: flex;
flex-direction: column;
align-items: flex-start;
fieldset {
border: none;
display: flex;
flex-direction: column;
align-items: stretch;
margin: 0.5em;
padding: 0;
width: calc(100% - 1em);
}
}
label {
margin: 0.5em;
display: flex;
span { width: 9em; }
input, textarea, select { flex-grow: 1; }
}
.invalid-message {
display: flex;
margin: 0.5em;
color: #d90429;
font-style: italic;
justify-content: center;
}
.actions {
display: flex;
justify-content: center;
margin: 0.5em;
button svg {
position: relative;
margin: 0 0.125em;
top: 0.125em;
}
}
&.success button {
color: #38b000;
background-color: #38b00010;
border-color: #38b000;
}
&.error button {
color: #d90429;
background-color: #d9042910;
border-color: #d90429;
}
}
.successes, .errors { margin: 0.5em auto; }
.successes { color: #38b000; }
.errors { color: #d90429; }
@include forSize(mobile) {
#header-splash {
width: 100%;
margin: 0;
}
#event-proposal {
margin: 1em;
width: 100%;
}
}
@include forSize(notMobile) {
#header-splash {
object-fit: cover;
object-position: center 56%;
width: 100%;
height: 20em;
}
#event-proposal {
border: solid thin #bbb;
border-radius: 0.25em;
box-shadow: 0.25em 0.25em 0.75em #aaa;
width: 30em;
}
.successes, .errors { width: 26em; }
}

View File

@ -0,0 +1,84 @@
import { defineComponent, Ref, ref } from 'vue';
import { logService } from '@jdbernard/logging';
import {
default as api,
EventProposalModel,
newEventProposal,
} from '@/api-client';
import {
CircleCheckIcon,
CircleCrossIcon,
HourGlassIcon,
SpinnerIcon,
} from '@/components/svg';
const logger = logService.getLogger('/propose-events');
type FormState =
| 'loading'
| 'ready'
| 'submitting'
| 'invalid'
| 'success'
| 'error';
export default defineComponent({
name: 'TheProposeEventView',
props: {},
components: { CircleCheckIcon, CircleCrossIcon, HourGlassIcon, SpinnerIcon },
setup: function TheProposeEventView() {
const departments: Ref<{ value: string; color: string }[]> = ref([]);
const formState: Ref<FormState> = ref('loading');
setTimeout(async () => {
departments.value = (await api.getEventProposalConfig()).departments;
formState.value = 'ready';
});
const formVal = { event: newEventProposal() };
const successes: string[] = [];
const errors: string[] = [];
function validateEvent(ev: EventProposalModel): boolean {
return (
!!ev.name &&
!!ev.description &&
!!ev.purpose &&
!!ev.department &&
!!ev.location &&
!!ev.owner
);
}
async function proposeEvent(): Promise<void> {
if (!validateEvent(formVal.event)) {
formState.value = 'invalid';
return;
}
formState.value = 'submitting';
logger.trace({ formState: formState.value });
if (await api.proposeEvent(formVal.event)) {
formState.value = 'success';
successes.push(
`We've recorded the proposed details for ${formVal.event.name}.`
);
} else {
formState.value = 'error';
errors.push(
'We were unable to record the proposed details for ' +
formVal.event.name +
". Poke Jonathan and tell him it's broken."
);
}
setTimeout(() => {
formVal.event = newEventProposal();
formState.value = 'ready';
}, 5000);
}
return { departments, errors, formState, formVal, successes, proposeEvent };
},
});

View File

@ -0,0 +1,89 @@
<template>
<img id="header-splash" src="../assets/welcome-wood.jpg" />
<div id="event-proposal" :class="[formState]">
<header>
<h1>Propose an Event</h1>
<h2>Hope Family Fellowship</h2>
</header>
<form @submit.prevent="proposeEvent">
<fieldset :disabled="formState !== 'ready' && formState !== 'invalid'">
<label>
<span>Event Name</span>
<input
type="text"
name="name"
placeholder="e.g. Men's Bible Study"
v-model="formVal.event.name"
/>
</label>
<label>
<span>Date and time</span>
<input type="date" name="date" v-model="formVal.event.date" />
</label>
<label>
<span>Department</span>
<select name="department" v-model="formVal.event.department">
<option value="">--- select a department ---</option>
<option
v-for="opt in departments"
:key="opt.value"
class="color-{{opt.color}}"
>
{{ opt.value }}
</option>
</select>
</label>
<label>
<span>Owner</span>
<input type="text" name="owner" v-model="formVal.event.owner" />
</label>
<label>
<span>Location</span>
<textarea v-model="formVal.event.location"></textarea>
</label>
<label>
<span>Purpose</span>
<textarea v-model="formVal.event.purpose"></textarea>
</label>
<label>
<span>Description</span>
<textarea v-model="formVal.event.description"></textarea>
</label>
<div class="invalid-message" v-if="formState === 'invalid'">
All fields are required.
</div>
<div class="actions">
<button type="submit">
<span v-if="formState === 'ready' || formState === 'invalid'"
>Propose Event</span
>
<span v-if="formState === 'submitting'">
<SpinnerIcon class="spin" />
submitting...
</span>
<span v-if="formState === 'loading'">
<HourGlassIcon class="tumble" />
Loading...
</span>
<span v-if="formState === 'success'">
<CircleCheckIcon />
Event Proposed!
</span>
<span v-if="formState === 'error'">
<CircleCheckIcon />
An error occurred.
</span>
</button>
</div>
</fieldset>
</form>
</div>
<div class="successes">
<div v-for="s in successes" :key="s">{{ s }}</div>
</div>
<div class="errors">
<div v-for="e in errors" :key="e">{{ s }}</div>
</div>
</template>
<script lang="ts" src="./ProposeEvent.ts"></script>
<style scoped lang="scss" src="./ProposeEvent.scss"></style>