Skip to content

Commit 350dab4

Browse files
Add contribute feature with validation. (#242)
* feat: add community contribute feature with validation * feat: store & read shared form fields on local storage * Fix indentation and formatting in GlobalContributeModal * Default to the middle option * Improve form UX with clearer labels and contextual hints. * Refactoring and fixing a bug --------- Co-authored-by: Matthias Mohr <webmaster@mamo-net.de>
1 parent 8cfe77f commit 350dab4

7 files changed

Lines changed: 609 additions & 113 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<script setup lang="ts">
2+
import type { ContributeForm, ContributionType } from '../composables/useGlobalContribute'
3+
import PersonalDetailsFields from './PersonalDetailsFields.vue'
4+
5+
interface Props {
6+
modelValue: boolean
7+
contributeForm: ContributeForm
8+
contributionOptions: Array<{ value: ContributionType; label: string; description: string }>
9+
canSubmit: boolean
10+
isSubmitting: boolean
11+
}
12+
13+
interface Emits {
14+
'update:modelValue': [value: boolean]
15+
'update:form': [field: keyof ContributeForm, value: any]
16+
submit: []
17+
}
18+
19+
defineProps<Props>()
20+
const emit = defineEmits<Emits>()
21+
22+
const toggleContributionType = (type: ContributionType, form: ContributeForm) => {
23+
const index = form.contributionTypes.indexOf(type)
24+
const newTypes = [...form.contributionTypes]
25+
if (index >= 0) {
26+
newTypes.splice(index, 1)
27+
} else {
28+
newTypes.push(type)
29+
}
30+
emit('update:form', 'contributionTypes', newTypes)
31+
}
32+
</script>
33+
34+
<template>
35+
<v-dialog
36+
:model-value="modelValue"
37+
@update:model-value="emit('update:modelValue', $event)"
38+
max-width="550"
39+
>
40+
<v-card>
41+
<v-card-title class="text-h5">Become a Contributor</v-card-title>
42+
<v-card-subtitle class="text-body2 px-6 pb-2 text-wrap">
43+
To carry FTW forward will require the collective talent and resources of our community. We
44+
welcome your contributions and partnership!
45+
</v-card-subtitle>
46+
47+
<v-card-text>
48+
<div class="mb-3">
49+
<p class="text-xs font-weight-bold mb-2">How would you like to contribute? *</p>
50+
<div class="d-flex flex-column">
51+
<v-checkbox
52+
v-for="option in contributionOptions"
53+
:key="option.value"
54+
:model-value="contributeForm.contributionTypes.includes(option.value)"
55+
:label="option.label"
56+
@update:model-value="toggleContributionType(option.value, contributeForm)"
57+
class="pa-0 ma-0"
58+
density="compact"
59+
hide-details
60+
></v-checkbox>
61+
</div>
62+
</div>
63+
64+
<v-textarea
65+
:model-value="contributeForm.resourceLink"
66+
@update:model-value="emit('update:form', 'resourceLink', $event)"
67+
label="Do you have Field Boundaries data or a model to share?"
68+
hint="Share a Link or describe your resource"
69+
variant="outlined"
70+
rows="2"
71+
auto-grow
72+
class="mb-2"
73+
density="compact"
74+
></v-textarea>
75+
76+
<v-textarea
77+
:model-value="contributeForm.additionalInfo"
78+
@update:model-value="emit('update:form', 'additionalInfo', $event)"
79+
label="Anything else you would like us to know?"
80+
variant="outlined"
81+
rows="3"
82+
auto-grow
83+
class="mb-2"
84+
density="compact"
85+
></v-textarea>
86+
87+
<PersonalDetailsFields
88+
:name="contributeForm.name"
89+
:email="contributeForm.email"
90+
:organization="contributeForm.organization"
91+
required
92+
density="compact"
93+
field-spacing="mb-2"
94+
@update:name="emit('update:form', 'name', $event)"
95+
@update:email="emit('update:form', 'email', $event)"
96+
@update:organization="emit('update:form', 'organization', $event)"
97+
/>
98+
</v-card-text>
99+
100+
<v-card-actions>
101+
<v-spacer></v-spacer>
102+
<v-btn variant="text" @click="emit('update:modelValue', false)">Cancel</v-btn>
103+
<v-btn
104+
color="teal"
105+
variant="flat"
106+
:disabled="!canSubmit"
107+
:loading="isSubmitting"
108+
@click="emit('submit')"
109+
>
110+
Submit
111+
</v-btn>
112+
</v-card-actions>
113+
</v-card>
114+
</v-dialog>
115+
</template>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script setup lang="ts">
2+
import type { DetailedFeedbackForm } from '../composables/useGlobalFeedback'
3+
import PersonalDetailsFields from './PersonalDetailsFields.vue'
4+
5+
interface Props {
6+
modelValue: boolean
7+
detailsForm: DetailedFeedbackForm
8+
canSubmit: boolean
9+
isSubmitting: boolean
10+
}
11+
12+
interface Emits {
13+
'update:modelValue': [value: boolean]
14+
'update:form': [field: keyof DetailedFeedbackForm, value: string]
15+
submit: []
16+
}
17+
18+
defineProps<Props>()
19+
const emit = defineEmits<Emits>()
20+
</script>
21+
22+
<template>
23+
<v-dialog
24+
:model-value="modelValue"
25+
@update:model-value="emit('update:modelValue', $event)"
26+
max-width="550"
27+
>
28+
<v-card>
29+
<v-card-title class="text-h5">Tell Us More</v-card-title>
30+
<v-card-subtitle class="text-body2 px-6 pb-2 text-wrap">
31+
We are just getting started! We know there is room to improve and we need your feedback!
32+
</v-card-subtitle>
33+
34+
<v-card-text>
35+
<v-textarea
36+
:model-value="detailsForm.qualityFeedback"
37+
@update:model-value="emit('update:form', 'qualityFeedback', $event)"
38+
label="How can we improve field boundaries?"
39+
hint="Share what's good/bad and what would make them more useful to you"
40+
variant="outlined"
41+
rows="4"
42+
auto-grow
43+
required
44+
class="mb-3"
45+
></v-textarea>
46+
47+
<v-textarea
48+
:model-value="detailsForm.useCase"
49+
@update:model-value="emit('update:form', 'useCase', $event)"
50+
label="How would you like to use field boundaries?"
51+
hint="Describe your specific use case and how field boundaries could be more useful to you"
52+
variant="outlined"
53+
rows="3"
54+
auto-grow
55+
required
56+
class="mb-3"
57+
></v-textarea>
58+
59+
<PersonalDetailsFields
60+
:name="detailsForm.name"
61+
:email="detailsForm.email"
62+
:organization="detailsForm.organization"
63+
@update:name="emit('update:form', 'name', $event)"
64+
@update:email="emit('update:form', 'email', $event)"
65+
@update:organization="emit('update:form', 'organization', $event)"
66+
/>
67+
</v-card-text>
68+
69+
<v-card-actions>
70+
<v-spacer></v-spacer>
71+
<v-btn variant="text" @click="emit('update:modelValue', false)">Cancel</v-btn>
72+
<v-btn
73+
color="teal"
74+
variant="flat"
75+
:disabled="!canSubmit"
76+
:loading="isSubmitting"
77+
@click="emit('submit')"
78+
>
79+
Submit
80+
</v-btn>
81+
</v-card-actions>
82+
</v-card>
83+
</v-dialog>
84+
</template>

src/components/GlobalFeedbackWidget.vue

Lines changed: 35 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { computed } from 'vue'
33
import useMap from '../composables/useMap'
44
import { geoJsonResults } from '../composables/useMap'
55
import useSettings from '../composables/useSettings'
6+
import GlobalContributeModal from './GlobalContributeModal.vue'
7+
import GlobalFeedbackDetailsModal from './GlobalFeedbackDetailsModal.vue'
68
import useGlobalFeedback from '../composables/useGlobalFeedback'
9+
import useGlobalContribute from '../composables/useGlobalContribute'
710
811
const { settings } = useSettings()
912
const { map } = useMap()
@@ -21,11 +24,21 @@ const {
2124
isSubmittingQuick,
2225
isSubmittingDetails,
2326
openDetailsDialog,
24-
closeDetailsDialog,
2527
submitQuickFeedback,
2628
submitDetailedFeedback,
2729
} = useGlobalFeedback(map)
2830
31+
const {
32+
CONTRIBUTION_OPTIONS,
33+
contributeDialogOpen,
34+
contributeForm,
35+
canSubmit: canSubmitContribute,
36+
isSubmitting: isSubmittingContribute,
37+
openContributeDialog,
38+
submitContribute,
39+
updateFormField,
40+
} = useGlobalContribute()
41+
2942
const sliderLabels = computed(() => {
3043
return Object.fromEntries(options.map((opt, i) => [i, opt.title]))
3144
})
@@ -61,6 +74,9 @@ const sliderLabels = computed(() => {
6174
</div>
6275

6376
<div class="feedback-actions mt-3">
77+
<v-btn variant="outlined" color="teal" size="small" @click="openContributeDialog">
78+
Contribute
79+
</v-btn>
6480
<v-btn
6581
variant="outlined"
6682
color="teal"
@@ -89,71 +105,24 @@ const sliderLabels = computed(() => {
89105
</v-card-text>
90106
</v-card>
91107

92-
<v-dialog v-model="detailsDialogOpen" max-width="530">
93-
<v-card>
94-
<v-card-title class="text-h5">Tell Us More</v-card-title>
95-
<v-card-subtitle class="text-body2 px-6 pb-2 text-wrap">
96-
We are just getting started! We know there is room to improve and we need your feedback!
97-
</v-card-subtitle>
98-
99-
<v-card-text>
100-
<v-textarea
101-
v-model="detailsForm.qualityFeedback"
102-
label="What was good/bad? How must the field boundaries improve to be useful to you?"
103-
variant="outlined"
104-
rows="4"
105-
auto-grow
106-
required
107-
class="mb-3"
108-
></v-textarea>
109-
110-
<v-textarea
111-
v-model="detailsForm.useCase"
112-
label="How would you like to use field boundaries? Tell us about your use case."
113-
variant="outlined"
114-
rows="3"
115-
auto-grow
116-
required
117-
class="mb-3"
118-
></v-textarea>
119-
120-
<v-text-field
121-
v-model="detailsForm.name"
122-
label="Name"
123-
variant="outlined"
124-
class="mb-3"
125-
></v-text-field>
126-
127-
<v-text-field
128-
v-model="detailsForm.email"
129-
type="email"
130-
label="Email"
131-
variant="outlined"
132-
class="mb-3"
133-
></v-text-field>
134-
135-
<v-text-field
136-
v-model="detailsForm.organization"
137-
label="Organization"
138-
variant="outlined"
139-
></v-text-field>
140-
</v-card-text>
141-
142-
<v-card-actions>
143-
<v-spacer></v-spacer>
144-
<v-btn variant="text" @click="closeDetailsDialog">Cancel</v-btn>
145-
<v-btn
146-
color="teal"
147-
variant="flat"
148-
:disabled="!canSubmitDetailed"
149-
:loading="isSubmittingDetails"
150-
@click="submitDetailedFeedback"
151-
>
152-
Submit
153-
</v-btn>
154-
</v-card-actions>
155-
</v-card>
156-
</v-dialog>
108+
<GlobalFeedbackDetailsModal
109+
v-model="detailsDialogOpen"
110+
:details-form="detailsForm"
111+
:can-submit="canSubmitDetailed"
112+
:is-submitting="isSubmittingDetails"
113+
@update:form="(field, value) => (detailsForm[field] = value)"
114+
@submit="submitDetailedFeedback"
115+
/>
116+
117+
<GlobalContributeModal
118+
v-model="contributeDialogOpen"
119+
:contribute-form="contributeForm"
120+
:contribution-options="CONTRIBUTION_OPTIONS"
121+
:can-submit="canSubmitContribute"
122+
:is-submitting="isSubmittingContribute"
123+
@update:form="updateFormField"
124+
@submit="submitContribute"
125+
/>
157126
</div>
158127
</template>
159128

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { isValidEmail } from '../functions/feedback-utils'
4+
5+
interface Props {
6+
name: string
7+
email: string
8+
organization: string
9+
required?: boolean
10+
density?: 'default' | 'comfortable' | 'compact'
11+
fieldSpacing?: string
12+
}
13+
14+
const props = withDefaults(defineProps<Props>(), {
15+
required: false,
16+
density: 'default',
17+
fieldSpacing: 'mb-3',
18+
})
19+
20+
interface Emits {
21+
'update:name': [value: string]
22+
'update:email': [value: string]
23+
'update:organization': [value: string]
24+
}
25+
26+
const emit = defineEmits<Emits>()
27+
28+
const emailError = computed(() =>
29+
props.email && !isValidEmail(props.email) ? 'Please enter a valid email address.' : undefined,
30+
)
31+
</script>
32+
33+
<template>
34+
<v-text-field
35+
:model-value="name"
36+
@update:model-value="emit('update:name', ($event ?? '').trim())"
37+
:label="required ? 'Name *' : 'Name'"
38+
variant="outlined"
39+
:density="density"
40+
:class="fieldSpacing"
41+
></v-text-field>
42+
43+
<v-text-field
44+
:model-value="email"
45+
@update:model-value="emit('update:email', ($event ?? '').trim())"
46+
type="email"
47+
:label="required ? 'Email *' : 'Email'"
48+
variant="outlined"
49+
:density="density"
50+
:class="fieldSpacing"
51+
:error-messages="emailError"
52+
></v-text-field>
53+
54+
<v-text-field
55+
:model-value="organization"
56+
@update:model-value="emit('update:organization', ($event ?? '').trim())"
57+
label="Organization"
58+
variant="outlined"
59+
:density="density"
60+
></v-text-field>
61+
</template>

0 commit comments

Comments
 (0)