Building a Heritage Trees Registry with Velo

Written by ritwik | Published 2021/07/27
Tech Story Tags: velo | build-velo-web-app | wix | wix-data | javascript | web-development | api | hackernoon-top-story

TLDR The Heritage Trees Registry is a digital registry for the city of Dehradun in the Himalayan foothills. It was created by a heritage walk's group (where I also volunteer) and INTACH or the Indian National Trust for Art and Cultural Heritage. The project is a complex and dynamic project which requires a flexible platform such as Velo. With Velo, a project such as this heritage project can be spread at a global level and not stay limited to serve only the memories of our city. I achieved this with the following code (with help from the interwebs)via the TL;DR App

"Every conservation effort starts with documentation and listing". With this philosophy in mind I was approached by the team at Been There, Doon That?, a heritage walk's group (where I also volunteer) and INTACH or the Indian National Trust for Art and Cultural Heritage to build a digital registry for the beautiful city of Dehradun and its surrounding areas.
With the prospect of registering thousands of trees and the memories we share with them, I knew that this was a complex and dynamic project which requires a flexible platform. Luckily, my experience with Wix and the Velo integrated development environment (IDE) made me know almost instantly that Velo would be the right fit. With Velo, a project such as this Heritage Trees Registry can be spread at a global level and not stay limited to serve only the memories of our city in the Himalayan foothills.
In the course of explaining how I built the registry, I will also highlight the importance of key features of Velo and the Velo API which not only made this possible but a breeze to develop.
These are:
  1. Custom Forms.
  2. Velo / Wix Collections.
  3. Dynamic Category and Item Pages.
  4. Several APIs within the Wix Data, Wix Dataset and Wix Editor Elements API Docs.
However, please note that I will only touch upon some unique things that I did and will give references to docs and forum posts for general guidance on Velo functionality. If you are trying to build a similar registry and need further guidance, feel free to reach out to me on LinkedIn.
Note: Here are the separate topics covered in this article:
  1. Getting users to create a unique ID.
  2. Building advanced filters with a seamless reset button for dynamic category pages (applicable to normal pages as well).
  3. Using multi-reference fields with dynamic item pages.
  4. Building a leaderboard or stats page with monthly count for scores or in our case tree info/story contributions using wixData.aggregate and other APIs.]
So here we go...
First up, an overview of how the registry works at a conceptual level: (refer to the infographic below)

The Custom Form:

The Form was built using custom input elements (ref. custom elements and my previous article on building advanced forms to learn how to create the underlying form).
Although this is a standard custom form in wix, what is different is that it has a captcha element to protect submissions. While also using the first 4 letters of the thana (Police Station) or area and the first 3 letters of the registrant's name to generate a unique tree ID in combination with the submission time in UNIX Epoch Time (ref. this article on unique ID generation by Rahman Fadhil on dev.to, Epoch Converter and this REPL from w3schools).
I achieved this with the following code (with help from the interwebs):
$w.onReady(function () {
    
  $w("#captcha").onVerified(() => {
      $w("#errorMessage").hide();
	  $w("#successMessage").hide();    
    $w("#submitButton").enable();
  })
  
  $w("#captcha").onError(() => {
    $w("#errorMessage").text = "The reCAPTCHA element lost connection with the CAPTCHA provider. Try again later.";
    $w("#errorMessage").show()
    .then(() => {
      $w("#errorMessage").hide("fade", {"delay": 10000});
    } );
  })

  $w("#captcha").onTimeout(() => {
    $w("#submitButton").disable();
  })
 
});

/**
    *	Adds an event handler that runs when the element is clicked.
    *	 @param {$w.MouseEvent} event
    */
    export function generateID_click(event) {
	  // This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4
	  // Add your code for this event here: 
      let t = new Date().getTime();
      let numCode = t.toString();
      let thanaCode = $w('#thana').value;
      thanaCode = thanaCode.replace(/\s/g,'');
      thanaCode = thanaCode.slice(0,4);
      thanaCode = thanaCode.toUpperCase();
      let personCode = $w('#contributorName').value;
      personCode = personCode.replace(/\s/g,'');
      personCode = personCode.slice(0,3);
      personCode = personCode.toUpperCase();
      let customID = numCode.concat(personCode);
      let gencode = thanaCode.concat(customID);
      if($w('#thana').valid && $w('#contributorName').valid){
         $w('#dataset1').setFieldValue('title', gencode);
      }   
    }
[Note: For input sanitization, ref. above code, this REPL from w3schools and the following MDN Web Docs on Assertions and Regular Expressions. Also note that #dataset1 is the Heritage Trees Registry Dataset.]
In addition, this form feeds data to the same collection which feeds the dynamic pages of the Heritage Tree Registry. With each registry item having its own dynamic item page once it is approved. This approval mechanism being controlled by a filter on the dynamic dataset on the dynamic category page.

The Dynamic Category/List Page:

This is a simple dynamic category page (simple, courtesy of the good folks at Wix as its pretty powerful in terms of functionality). Even the code for filters I have used is inspired from this video. But with some modification to the clear/reset button which is an improvement upon the discussions here and here in the forums.
import wixData from 'wix-data';

$w.onReady(function () {
	wixData.query('DehradunPoliceStations')
	.find()
	.then(res => {
		let options = [{"value":'','label':'All Thanas'}];
		options.push(...res.items.map(DehradunPoliceStations => {
			return {'value': DehradunPoliceStations.title,'label' : DehradunPoliceStations.title};
		}));
		$w('#iThana').options = options;
	})
});

let lastFilterTitle;
let lastFilterThana;
let lastFilterTree;

let debounceTimer;
export function iTreeID_keyPress(event) {
	// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4
	// Add your code for this event here: 
	if (debounceTimer) {
		clearTimeout(debounceTimer);
		debounceTimer = undefined;
	}
	debounceTimer = setTimeout(() => {
		filter($w('#iTreeID').value, lastFilterThana, lastFilterTree);
	}, 200);
}


function filter(title, thana, tree) {
	if (lastFilterTitle !== title || lastFilterThana !== thana || lastFilterTree !== tree) {
		let newFilter = wixData.filter();
		if (title) 
			newFilter = newFilter.contains('title', title);
		if (thana)
			newFilter = newFilter.eq('thana', thana);
		if (tree)
			newFilter = newFilter.contains('treeName', tree);
		newFilter = newFilter.eq('status',1);
		$w('#dataset1').setFilter(newFilter);
		lastFilterTitle = title;
		lastFilterThana = thana;
		lastFilterTree = tree;
	}
}


/**
*	Adds an event handler that runs when an input element's value
 is changed.
*	 @param {$w.Event} event
*/
export function iThana_change(event) {
	// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4
	// Add your code for this event here: 
	filter(lastFilterTitle, $w('#iThana').value, lastFilterTree);
}



/**
*	Adds an event handler that runs when the cursor is inside the
 input element and a key is pressed.
*	 @param {$w.KeyboardEvent} event
*/
export function iTree_keyPress(event) {
	// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4
	// Add your code for this event here: 
	if (debounceTimer) {
		clearTimeout(debounceTimer);
		debounceTimer = undefined;
	}
	debounceTimer = setTimeout(() => {
		filter(lastFilterTitle, lastFilterThana, $w('#iTree').value);
	}, 200);
}

/**
 *	Adds an event handler that runs when the element is clicked.
 *	 @param {$w.MouseEvent} event
 */
export function clearFilters_click(event) {
	// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4
	// Add your code for this event here: 

		$w("#iTreeID").value= null;
		$w("#iTreeID").resetValidityIndication();

		$w("#iTree").value= null;
		$w("#iTree").resetValidityIndication();

		$w("#iThana").value = '';
		$w("#iThana").resetValidityIndication();

		if (debounceTimer) {
		clearTimeout(debounceTimer);
		debounceTimer = undefined;
	}
	debounceTimer = setTimeout(() => {
		filter($w('#iTreeID').value, $w('#iThana').value, $w('#iTree').value);
	}, 200);
}
[Note: Notice how using the same filter function across the board makes the filtering logic consistent in the above code. Also please accept my apologies on the autocorrect on the formatting front in the above code.]
Another thing that I had to do differently was using an alternate dataset to replace the dynamic dataset for diplaying items in the the list repeater on the dynamic list/category page. This was essential in order to enable filtering the dynamic category page without negatively affecting the dynamic URLs being controlled by the filtered dynamic dataset.

The Dynamic Item Page:

The Dynamic Item Page is a testament to how simple the process of handling dynamic content on Wix is.
The only additional step I did on this page was to display OtherContributors and OtherStories about already registered trees via multi-reference fields for contributors and a reference field for stories, that too without any code. Where stories and contributors will be identified by team Treesofdehradun from the Facebook comments plugin comments, a contribution/guest blogger form and the registrants in the Heritage Trees Registry.

The Stats Page:

It took me some time to build the stats page. On this one, I had almost given up as I couldn't find what I was looking for in the forums as well as elsewhere. But as the stats page will be essential to the future rewards and recognition drives it was essential to push through. Slowly pieces started to come together and I was able to create a leaderboard style top 3 monthly contributors table. In hindsight the code is pretty simple and is as follows:
import wixData from 'wix-data';

$w.onReady(function () {
	// Write your JavaScript here

	// To select an element by ID use: $w("#elementID")
    const today = new Date();
 	const options = {
        //weekday: "long",
        //day: "numeric",
        month: "long",
        year: "numeric"
    };
    $w("#month").text = today.toLocaleDateString('en-US', options);

	let currentMonth = new Date().getMonth();
	let nextMonth = currentMonth + 1;
	let currentYear = new Date().getFullYear();
	console.log(currentMonth);
	console.log(nextMonth);
	console.log(currentYear);
		
  // set the table columns
  $w("#table1").columns = [
    // the column that shows the document name
    {
      "id": "col1",
      "dataPath": "title", // matches field key from collection
      "label": "Name",
      "width": 130,
      "visible": true,
      "type": "string",
    },
    // the column that shows the document description
    {
      "id": "col2",
      "dataPath": "count", // matches field key from collection
      "label": "Contributions",
      "width": 130,
      "visible": true,
      "type": "string",
    }
  ];

   const beginDate = new Date(currentYear, currentMonth, 1, 0, 0, 0) // first day of month
   const endDate = new Date(currentYear, nextMonth, 1, 0, 0, 0) // first day of next month
	let filter = wixData.filter().gt("_createdDate",beginDate).lt("_createdDate",endDate);
	// Click "Preview" to run your code
	wixData.aggregate("ContributionTable")
	.filter(filter)
	.group("contributionBy","title")
	.count()
	.run()
	.then( (results) => {
		let items = results.items;
		items = items.reverse();
		console.log(results.items);
		let ItemsTable;
		if(items.length >= 3)
		ItemsTable = [items[0],items[1],items[2]];
		if(items.length === 2)
		ItemsTable = [items[0],items[1]];
		if(items.length === 1)
		ItemsTable = [items[0]];
    if(items.length === 0)
    ItemsTable = [];
		$w('#table1').rows = ItemsTable;
	});

});
The above code can be broken down in 4 steps:
1. Get data on the current month.
2. Set Table Columns.
3. Prepare filter for aggregate function.
4. Run aggregate with filter and grouping.
In step 4, I had to reverse the order of results array to get values in the descending order (the default being ascending). I also had to check exceptions with a few if-statements after which I could proceed with inserting the data array into a table with the following output:
[Note: This forum discussion on date formatting, the column API and this forum post on filtering date by month were especially helpful in solving for the stats page. As the final code is more comprehensive than the sources referred, feel free to adapt it to your scenario.]
I hope this article helped you. Follow the inline references in the article above for more details on the Velo API. The references combined with this article will surely help you build any kind of registry or dynamic data oriented solution using Velo by Wix.
All images used are original.
This article is part of The Velo Writing Contest hosted by Hacker Noon in partnership with Wix.

Written by ritwik | A detail oriented person who believes in lifelong learning. Has basic knowledge of C++, HTML, CSS, JavaScript and SQL.
Published by HackerNoon on 2021/07/27