web: EntryProposal form implementation.
This commit is contained in:
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
@ -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>
|
113
web/src/views/ProposeEvent.scss
Normal file
113
web/src/views/ProposeEvent.scss
Normal 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; }
|
||||
}
|
84
web/src/views/ProposeEvent.ts
Normal file
84
web/src/views/ProposeEvent.ts
Normal 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 };
|
||||
},
|
||||
});
|
89
web/src/views/ProposeEvent.vue
Normal file
89
web/src/views/ProposeEvent.vue
Normal 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>
|
Reference in New Issue
Block a user