Skip to content

Commit cf06b9c

Browse files
committed
Require privacy consent and show app version
Import app version and bump package.json version; add a privacy consent checkbox and submission validation that requires users to confirm the privacy statement and for a Google Sheets webhook to be configured before submitting. Enhance contact field validation/error messages, wire submit button enabled state to consent and webhook, and add a styled consent panel and legal footer showing the app version. Include corresponding CSS for the consent UI and footer.
1 parent 59b8e9a commit cf06b9c

3 files changed

Lines changed: 199 additions & 29 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "geocollab-form",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"private": true,
55
"type": "module",
66
"scripts": {

src/components/GeoCollabWizard.vue

Lines changed: 117 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup>
22
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
3+
import packageJson from "../../package.json";
34
import joinedSpeciesCsv from "../data/avilist_soi_avonet_joined.csv?raw";
45
import { buildSpeciesFeedback } from "../lib/species";
56
import { parseSpeciesRecordsFromJoinedCsv } from "../lib/speciesCatalog";
@@ -19,6 +20,7 @@ const STEPS = [
1920
const WIZARD_STORAGE_KEY = "geocollab:wizard:v1";
2021
const STORAGE_SAVE_DEBOUNCE_MS = 300;
2122
const SUBMISSION_FOREGROUND_WAIT_MS = 1800;
23+
const APP_VERSION = packageJson.version;
2224
2325
const speciesRecords = parseSpeciesRecordsFromJoinedCsv(joinedSpeciesCsv);
2426
const speciesById = new Map(speciesRecords.map((item) => [item.avibase_id, item]));
@@ -32,6 +34,7 @@ const lastSubmissionPayload = ref(null);
3234
const contextReadConfirmed = ref(false);
3335
const contextAlignConfirmed = ref(false);
3436
const recaptureFeasibilityConfirmed = ref(false);
37+
const privacyStatementConfirmed = ref(false);
3538
let saveTimerId = null;
3639
3740
const selectedSpecies = computed(() => speciesById.get(draft.species.avibase_id));
@@ -124,6 +127,29 @@ function stepFiveValid() {
124127
);
125128
}
126129
130+
const stepFiveValidationMessages = computed(() => {
131+
const messages = [];
132+
if (isBlank(draft.contact.full_name)) messages.push("Enter your full name.");
133+
if (isBlank(draft.contact.address)) messages.push("Enter your address.");
134+
if (isBlank(draft.contact.email)) {
135+
messages.push("Enter your email address.");
136+
} else if (!isValidEmail(draft.contact.email)) {
137+
messages.push("Enter a valid email address.");
138+
}
139+
return messages;
140+
});
141+
142+
const submitValidationMessages = computed(() => {
143+
const messages = [...stepFiveValidationMessages.value];
144+
if (!privacyStatementConfirmed.value) {
145+
messages.push("Read and confirm the privacy statement.");
146+
}
147+
if (!hasGoogleSheetsWebhook.value) {
148+
messages.push("Submission is not configured yet.");
149+
}
150+
return messages;
151+
});
152+
127153
function isStepValid(stepIndex) {
128154
if (stepIndex === 1) return stepOneValid();
129155
if (stepIndex === 2) return stepTwoValid();
@@ -134,6 +160,12 @@ function isStepValid(stepIndex) {
134160
}
135161
136162
const canContinueCurrentStep = computed(() => isStepValid(step.value));
163+
const canSubmitFinalStep = computed(
164+
() =>
165+
canContinueCurrentStep.value &&
166+
privacyStatementConfirmed.value &&
167+
hasGoogleSheetsWebhook.value,
168+
);
137169
138170
function goBack() {
139171
if (step.value > 1) {
@@ -896,6 +928,8 @@ if (typeof window !== "undefined" && import.meta.env.DEV) {
896928
v-model="draft.contact.full_name"
897929
label="Full name"
898930
density="comfortable"
931+
:error="isBlank(draft.contact.full_name)"
932+
:error-messages="isBlank(draft.contact.full_name) ? ['Full name is required.'] : []"
899933
/>
900934
</v-col>
901935
<v-col cols="12" md="6">
@@ -904,8 +938,14 @@ if (typeof window !== "undefined" && import.meta.env.DEV) {
904938
label="Email"
905939
type="email"
906940
density="comfortable"
907-
:error="draft.contact.email.length > 0 && !isValidEmail(draft.contact.email)"
908-
error-messages=""
941+
:error="isBlank(draft.contact.email) || !isValidEmail(draft.contact.email)"
942+
:error-messages="
943+
isBlank(draft.contact.email)
944+
? ['Email is required.']
945+
: !isValidEmail(draft.contact.email)
946+
? ['Enter a valid email address.']
947+
: []
948+
"
909949
/>
910950
</v-col>
911951
</v-row>
@@ -919,7 +959,13 @@ if (typeof window !== "undefined" && import.meta.env.DEV) {
919959
/>
920960
</v-col>
921961
<v-col cols="12" md="6">
922-
<v-text-field v-model="draft.contact.address" label="Address" density="comfortable" />
962+
<v-text-field
963+
v-model="draft.contact.address"
964+
label="Address"
965+
density="comfortable"
966+
:error="isBlank(draft.contact.address)"
967+
:error-messages="isBlank(draft.contact.address) ? ['Address is required.'] : []"
968+
/>
923969
</v-col>
924970
</v-row>
925971
@@ -931,6 +977,55 @@ if (typeof window !== "undefined" && import.meta.env.DEV) {
931977
rows="3"
932978
density="comfortable"
933979
/>
980+
<div class="submit-consent-panel">
981+
<v-checkbox
982+
v-model="privacyStatementConfirmed"
983+
density="comfortable"
984+
hide-details
985+
class="collab-confirm submit-consent-checkbox"
986+
>
987+
<template #label>
988+
<span
989+
>I have read and understood the
990+
<a
991+
href="https://www.vogelwarte.ch/en/privacy-statement/"
992+
target="_blank"
993+
rel="noopener noreferrer"
994+
@click.stop
995+
>Swiss Ornithological Institute privacy statement</a
996+
>.</span
997+
>
998+
</template>
999+
</v-checkbox>
1000+
<v-alert
1001+
v-if="submitValidationMessages.length > 0"
1002+
type="warning"
1003+
variant="tonal"
1004+
density="compact"
1005+
class="submit-consent-alert"
1006+
>
1007+
<ul class="submit-consent-list">
1008+
<li v-for="message in submitValidationMessages" :key="message">
1009+
{{ message }}
1010+
</li>
1011+
</ul>
1012+
</v-alert>
1013+
<div class="submit-consent-actions">
1014+
<v-btn
1015+
color="primary"
1016+
size="large"
1017+
:disabled="
1018+
!canSubmitFinalStep || isSubmitting || isSubmissionLocked || isSubmissionPending
1019+
"
1020+
:loading="isSubmitting"
1021+
class="submit-consent-button"
1022+
data-testid="submit-btn"
1023+
@click="submitForm"
1024+
>
1025+
{{ isSubmissionLocked ? "Submitted" : "Submit" }}
1026+
</v-btn>
1027+
</div>
1028+
</div>
9341029
<v-alert
9351030
v-if="submissionState === 'error'"
9361031
type="error"
@@ -979,18 +1074,6 @@ if (typeof window !== "undefined" && import.meta.env.DEV) {
9791074
9801075
<v-divider />
9811076
<footer class="wizard-nav">
982-
<p v-if="step < STEPS.length" class="privacy-note privacy-note--footer">
983-
This form stores your in-progress draft locally in your browser. If you use the map,
984-
location search requests are sent to Mapbox.
985-
</p>
986-
<p v-else class="privacy-note privacy-note--footer">
987-
By submitting, you agree that the information in this form will be sent to a private
988-
Google account managed by the form administrator, who will use it to assess your
989-
proposal, contact you about it, and share it with the Swiss Ornithological Institute if
990-
relevant for the collaboration process. Draft answers remain stored only in this browser
991-
until you submit or clear them.
992-
</p>
993-
9941077
<div class="wizard-nav__actions">
9951078
<v-btn variant="text" :disabled="step === 1" data-testid="back-btn" @click="goBack">
9961079
Back
@@ -1006,21 +1089,27 @@ if (typeof window !== "undefined" && import.meta.env.DEV) {
10061089
>
10071090
Continue
10081091
</v-btn>
1009-
1010-
<v-btn
1011-
v-else
1012-
color="primary"
1013-
:disabled="
1014-
!canContinueCurrentStep || isSubmitting || isSubmissionLocked || isSubmissionPending
1015-
"
1016-
:loading="isSubmitting"
1017-
data-testid="submit-btn"
1018-
@click="submitForm"
1019-
>
1020-
{{ isSubmissionLocked ? "Submitted" : "Submit" }}
1021-
</v-btn>
10221092
</div>
10231093
</footer>
10241094
</v-sheet>
1095+
<footer class="page-legal-footer">
1096+
<p class="legal-note"><a
1097+
href="https://www.vogelwarte.ch/en/projects/geocollab/"
1098+
target="_blank"
1099+
rel="noopener noreferrer"
1100+
>GeoCollab project</a
1101+
><span class="legal-note__separator">|</span><a
1102+
href="https://www.vogelwarte.ch/en/privacy-statement/"
1103+
target="_blank"
1104+
rel="noopener noreferrer"
1105+
>Privacy statement</a
1106+
><span class="legal-note__separator">|</span><a
1107+
href="https://www.vogelwarte.ch/modx/en/vogelwarte/impressum"
1108+
target="_blank"
1109+
rel="noopener noreferrer"
1110+
>Legal notice</a
1111+
><span class="legal-note__separator">|</span><span>Version {{ APP_VERSION }}</span
1112+
><span class="legal-note__separator">|</span><span>&copy; 2026 GeoCollab</span></p>
1113+
</footer>
10251114
</v-container>
10261115
</template>

src/styles/main.css

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,87 @@ a:hover {
237237
margin: 0;
238238
}
239239

240+
.submit-consent-panel {
241+
display: grid;
242+
gap: 12px;
243+
margin-top: 20px;
244+
padding: 18px 20px;
245+
border: 1px solid var(--gc-color-border, #d7dfda);
246+
border-radius: 16px;
247+
background:
248+
linear-gradient(180deg, rgb(255 255 255 / 98%) 0%, rgb(247 250 248 / 100%) 100%);
249+
box-shadow: inset 0 1px 0 rgb(255 255 255 / 65%);
250+
}
251+
252+
.submit-consent-checkbox {
253+
margin-top: 0;
254+
}
255+
256+
.submit-consent-checkbox .v-label {
257+
white-space: normal;
258+
line-height: 1.5;
259+
overflow: visible;
260+
opacity: 1;
261+
}
262+
263+
.submit-consent-checkbox .v-selection-control {
264+
align-items: center;
265+
}
266+
267+
.submit-consent-checkbox a {
268+
margin-left: 0.2rem;
269+
}
270+
271+
.submit-consent-actions {
272+
display: flex;
273+
justify-content: center;
274+
padding-top: 2px;
275+
}
276+
277+
.submit-consent-alert {
278+
margin-top: 2px;
279+
}
280+
281+
.submit-consent-list {
282+
margin: 0;
283+
padding-left: 1.1rem;
284+
}
285+
286+
.submit-consent-button {
287+
min-width: 196px;
288+
}
289+
290+
.legal-note {
291+
margin: 0;
292+
font-size: 0.82rem;
293+
line-height: 1.35;
294+
color: var(--gc-color-muted, #60716d);
295+
}
296+
297+
.page-legal-footer {
298+
display: flex;
299+
justify-content: center;
300+
padding: 12px 12px 0;
301+
text-align: center;
302+
}
303+
304+
.legal-note a {
305+
color: inherit;
306+
text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
307+
text-underline-offset: 0.12em;
308+
}
309+
310+
.legal-note__separator {
311+
margin: 0 0.55rem;
312+
opacity: 0.55;
313+
}
314+
315+
.legal-note a:hover,
316+
.legal-note a:focus-visible {
317+
color: var(--gc-color-text, #1f2a28);
318+
text-decoration-color: currentColor;
319+
}
320+
240321
.project-simple-stack {
241322
display: grid;
242323
gap: 18px;

0 commit comments

Comments
 (0)