11<script setup>
22import { computed , onBeforeUnmount , onMounted , reactive , ref , watch } from " vue" ;
3+ import packageJson from " ../../package.json" ;
34import joinedSpeciesCsv from " ../data/avilist_soi_avonet_joined.csv?raw" ;
45import { buildSpeciesFeedback } from " ../lib/species" ;
56import { parseSpeciesRecordsFromJoinedCsv } from " ../lib/speciesCatalog" ;
@@ -19,6 +20,7 @@ const STEPS = [
1920const WIZARD_STORAGE_KEY = " geocollab:wizard:v1" ;
2021const STORAGE_SAVE_DEBOUNCE_MS = 300 ;
2122const SUBMISSION_FOREGROUND_WAIT_MS = 1800 ;
23+ const APP_VERSION = packageJson .version ;
2224
2325const speciesRecords = parseSpeciesRecordsFromJoinedCsv (joinedSpeciesCsv);
2426const speciesById = new Map (speciesRecords .map ((item ) => [item .avibase_id , item]));
@@ -32,6 +34,7 @@ const lastSubmissionPayload = ref(null);
3234const contextReadConfirmed = ref (false );
3335const contextAlignConfirmed = ref (false );
3436const recaptureFeasibilityConfirmed = ref (false );
37+ const privacyStatementConfirmed = ref (false );
3538let saveTimerId = null ;
3639
3740const 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+
127153function isStepValid (stepIndex ) {
128154 if (stepIndex === 1 ) return stepOneValid ();
129155 if (stepIndex === 2 ) return stepTwoValid ();
@@ -134,6 +160,12 @@ function isStepValid(stepIndex) {
134160}
135161
136162const canContinueCurrentStep = computed (() => isStepValid (step .value ));
163+ const canSubmitFinalStep = computed (
164+ () =>
165+ canContinueCurrentStep .value &&
166+ privacyStatementConfirmed .value &&
167+ hasGoogleSheetsWebhook .value ,
168+ );
137169
138170function 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>
0 commit comments