GeoCollab Form is a Vue 3 + Vuetify intake app for potential geolocator collaborations with the Swiss Ornithological Institute. It is designed to do two things:
- help applicants understand whether a project is broadly suitable for GeoCollab
- collect structured project information for internal review
The app is built as a multi-step wizard and is deployed as a static site on GitHub Pages.
- Vue 3
- Vite
- Vuetify
- Mapbox GL JS
- Google Sheets via Apps Script webhook
- GitHub Pages for hosting
Requirements:
- Node.js 20+
- npm
- Python 3 only if you want to regenerate the species data
Install dependencies:
npm installCreate a local env file:
cp .env.example .envSet the frontend environment variables in .env:
VITE_MAPBOX_TOKEN=...
VITE_GOOGLE_SHEETS_WEBHOOK_URL=...Start the dev server:
npm run devBuild for production:
npm run buildPreview the production build locally:
npm run previewBecause Vite is configured with base: '/GeoCollabForm/', the local dev URL and the deployed GitHub Pages URL use that repository path.
This project uses two Vite variables:
VITE_MAPBOX_TOKENVITE_GOOGLE_SHEETS_WEBHOOK_URL
Important:
- Any variable prefixed with
VITE_is bundled into the frontend and should be treated as public. - The Mapbox token must therefore be a restricted public token, not a secret server token.
- The Google Sheets webhook URL is also exposed to the browser at runtime, so it should be treated as a public submission endpoint.
.envand.env.localare ignored by git and should not be committed.
The app reads its species reference data from:
src/data/avilist_soi_avonet_joined.csv
This file is a generated frontend dataset built from local source spreadsheets. The raw Excel inputs in data/*.xlsx are intentionally ignored by git and are not meant to be distributed with the repository or included in archival releases.
Current generated data outputs:
src/data/avilist_soi_avonet_joined.csv: frontend input used at build timedata/avilist_soi_avonet_joined.csv: local generated copydata/missing_mass_species.csv: local review file for missing mass values
To regenerate the joined dataset locally:
python3 scripts/join_reference_tables.pyThe script expects the raw Excel inputs to exist locally under data/.
The same app has a logger planner at #/planner (#/history opens it at the list of past deployments). It helps choose the geolocator, battery and recording intervals for a species, and checks them against:
- the tag weight of past SOI tags with the same battery and harness, as % of body mass
- the battery model and the per-sensor memory model from
SimulationGDL3pam v2.4_StromSpeicher.xlsx(src/lib/loggerModel.js) - how long past tags with the same settings recorded
It also writes the agreement text (intervals in minutes) and the production settings (base interval and factors).
The past deployments come from src/data/planner_history.json. This public file holds only summaries by order, plus anonymous tag and sensor records. It has no client names, ring numbers, tag IDs, coordinates or remarks, and it leaves out test orders. To rebuild it (macOS/Linux with mdbtools):
Rscript scripts/build_planner_data.R "/Volumes/20 Geolocator/40 Database/GDL_Data.accdb" <sensor_stops.csv>sensor_stops.csv is made from the raw logger files by the scripts in the GeoCollab folder 90_internal_document/sensor_stops/.
Form submissions are sent from the frontend to a Google Apps Script web app. A minimal script looks like this:
const SHEET_NAME = "Submissions";
function doPost(e) {
const sheet = getOrCreateSheet_();
const payload = JSON.parse(e.postData.contents || "{}");
const headers = [
"submitted_at_iso",
"species_common_name",
"species_body_mass_g",
"species_migration_status",
"species_birds_of_the_world_url",
"location_site_name",
"location_country",
"location_latitude",
"location_longitude",
"location_radius_km",
"project_description",
"project_expected_number_equipped",
"project_recapture_rationale",
"project_permits_status",
"contact_full_name",
"contact_email",
"contact_affiliation",
"contact_address",
"contact_comments",
];
ensureHeader_(sheet, headers);
const row = headers.map((key) => payload[key] ?? "");
sheet.appendRow(row);
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
}
function getOrCreateSheet_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
return ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
}
function ensureHeader_(sheet, headers) {
if (sheet.getLastRow() === 0) {
sheet.appendRow(headers);
}
}Deploy it as a Web App and set the resulting URL as VITE_GOOGLE_SHEETS_WEBHOOK_URL.
GitHub Pages deployment is handled by:
.github/workflows/deploy-pages.yml
The workflow:
- runs on pushes to
main - installs dependencies with
npm ci - builds the app with the required Vite environment variables
- deploys
dist/to GitHub Pages
Before the first deployment, configure the repository:
- In GitHub, set
Settings -> Pages -> SourcetoGitHub Actions. - Add these repository secrets under
Settings -> Secrets and variables -> Actions:VITE_MAPBOX_TOKENVITE_GOOGLE_SHEETS_WEBHOOK_URL
Once that is in place, pushing to main will trigger deployment.
dist/is ignored and should not be committed..envis ignored and should remain local.- The raw source spreadsheets in
data/*.xlsxare ignored and untracked on purpose. - The tracked generated CSV in
src/data/is required for the app to work from a fresh clone.