Chapter 2: Experiences, Classes, and More
Now that we have a home page to show off Inner Light Wellness and its membership options, Bianca wants to highlight the various experiences she has created for her members. The ALANA Platform is perfect for this, because it was designed from the start around the idea of doing things together. In this section of the tutorial, we'll merely scratch the surface of the deep support the SDK provides.
Types of experiences
Most "experiences" are just that. A spa day, for example, could be an ALANA experience. A guest pass for a gym, a luxury dinner and dessert, and a horseback ride through Virginia wine country are all experiences. They're things you can do, activities that have at least some social part to them.
A few are more than this, however, and the ALANA Platform gives them some special treatment. As of this writing, the Platform defines four additional types of experiences:
Events - These are activities that happen at specific times, such as concerts or movies or parties. Some may be recurring, such as an open mic night every Friday, and tickets that are purchased are always for a specific instance of the event.
Tours - These have a relatively fixed itinerary. They take place at a specific time, go to one or more specific places, and generally follow a pre-planned schedule.
Classes - These are, well, classes. They're educational in nature, though that can include things like self-help sessions and online training. Classes meet on a schedule, and may be at a physical location or in a virtual setting such as a Zoom or Teams call.
Adventures - These are experiences, usually outdoors, which are focused on being active. Some are guided, some are team-building exercises, and a few defy classification. Adventures might be seasonal or weather-dependent, and they may require buyers to sign a waiver.
An experience catalog
Inner Light doesn't really have any adventures or tours, but it will fill the other three categories. Our first task, also the simplest, is to get a list of every experience Bianca has created in her brand's Dashboard.
After the showMemberships function we created in the last section, we'll add a new listExperiences function that does, well, exactly what it says. We're going to put this on a new page, inside a new folder, so let's create experiences/index.html and give it some content, too.
import Alana from "https://sdk.alana4.com/js/alana.js";
const API_KEY = "911e5ddf75e11f976c6ed21df4c24834a92f65b0ce38484c7a58304b2a353aa0";
const API_USERNAME = "innerlightdemo";
const Api = Alana({
apiKey: API_KEY,
apiUsername: API_USERNAME,
mode: 'development',
});
////
// (removed showMemberships() for clarity)
////
export async function listExperiences() {
// Get a list of experiences and put them into cards.
const experiences = await Api.Experiences.list();
const section = document.querySelector("#experiences-list");
const experienceTemplate = document.querySelector("#experience-card");
experiences.forEach(experience => {
const clone = experienceTemplate.content.cloneNode(true);
let title = clone.querySelector(".experience-card-title");
let description = clone.querySelector(".experience-card-description");
let price = clone.querySelector(".experience-card-price");
let buyButton = clone.querySelector(".experience-card-buy");
let seeMoreButton = clone.querySelector(".experience-card-see-more");
title.textContent = experience.name;
description.textContent = experience.description;
price.textContent = `$${experience.retailPrice || experience.memberPrice} / guest`;
section.appendChild(clone);
buyButton.addEventListener('click', () => location.href = `/experiences/buy.html?experience=${experience.id}`);
seeMoreButton.addEventListener('click', () => location.href = `/experiences/?experience=${experience.id}`);
});
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alana Platform SDK test</title>
<script type="module">
import { listExperiences } from "/site.js";
await listExperiences();
</script>
<style>
.hidden {
display: none;
}
</style>
</head>
<body>
<header>
<h1>Experiences by Inner Light Wellness</h1>
</header>
<main>
<div id="experiences-list">
</div>
</main>
<footer>
<p>Powered by <a href="https://alana4.com">ALANA4 and the ALANA SDK</a></p>
</footer>
<template id="experience-card">
<section>
<h1 class="experience-card-title"></h1>
<p class="experience-card-description"></p>
<div>
<span class="experience-card-price"></span>
<div>
<button type="button" class="experience-card-see-more">See More</button>
<button type="button" class="experience-card-buy">Get This Plan</button>
</div>
</div>
</section>
</template>
</body>
</html>
Now, visiting /experiences will show this new page, complete with the experiences that have been created for Inner Light. The listExperiences() function is where the magic happens, using the Experiences.list() SDK method.
Different ways to list
Of course, there are a lot of different ways we could do this. list() is the easiest for demonstration, because it simply grabs everything created by your brand.
Another option is listByCategory(), which takes the name of a category and filters out any of your brand's experiences that aren't in that category. For example, Bianca might want a page that shows only the recreational activities Inner Light occasionally holds, so that page would call Experiences.listByCategory("Recreation").
For an even finer cut, we can go down to the level of the theme. These are simple tags that can be attached to experiences to let customers know what they're getting into. They're defined in the ALANA Dashboard, and the SDK provides listByTheme() to get all of your brand's experiences with one specific tag. In the case of Inner Light, we could use Experiences.listByTheme("Yoga"), as one example.
About Categories and Themes
You can get a list of categories by calling Experiences.categories(), which returns an array of strings for the categories in which your brand has experiences. If you pass true to this method, the result will include all categories known to the platform.
Similarly, Experiences.themes() will get a list of theme tags, but the format is a little different. Instead of an array of simple strings, the method returns an array of objects, one for each category, with name and tags properties.
The names of both categories and themes are case-insensitive.
Searching for experiences
Most complex of all, but also the most useful, is the search() method. With this one method, you can build a full-featured, granular search system for your brand's experiences that includes filtering by date, budget, number of guests, and much more. Or, if you prefer, you can use it as nothing more than a basic full-text search function. The choice is yours, because search() is one of the most flexible methods in the entire SDK.
search() needs to be called with a search object, which will contain all the different parameters for your search. The API docs contain the full list, but some of the most important ones include:
query(string): A query string to search for. This will be split into individual words, and the results will contain only experiences that have at least one of those words.themes(array of strings): A list of theme tags to filter on. If this is given, an experience must have at least one of the themes to be returned.budget(number): The maximum amount of money the user wants to spend on the experience. There are some calculations that go on behind the scenes to make this less simplistic (but more realistic) than you might think.guests(number): The number of people who will attend the experience in this group. Some experiences might have restrictions or different price points for different party sizes, so it's good to be able to hide the ones that are poor matches.ageRange(object with optionalyoungestandoldestnumber properties): If used, this allows filtering of experiences based on age. Only experiences that are listed as appropriate for the whole age range will be included in the results, and those that are "ideal" for the whole range will be prioritized. For example, if you setyoungestto 13, then you won't get any experiences that are `8+.
The Experience object
Whatever method you use to get a list of experiences, the result is going to be an array of Experience objects. This is one of the most detailed resource objects in the entire ALANA SDK, and it's hard to really do it justice here. The code example uses only a handful of the 30+ data properties available to you.
Experiences all have IDs. These are unique, hash-based identifiers, the same as with most resources in the SDK. We'll even see a use of the id property below.
Some of the other useful properties are name and description, which are just what you'd expect, images, which is an array of image URLs for the experience, and rating, which is a numeric "star" rating for the experience.
On top of those, Experience objects have most of the fields you can edit in the Dashboard. So you can get the price for non-members as retailPrice, while the members-only price would be memberPrice. The list of amenities for the experience is, unsurprisingly, the amenities array. And so on. Check the full docs for the Experience resource for the complete list, because there really are too many of them to go into here.
There are also a few methods that allow you to like or share an experience, but those require us to be logged in, so we'll postpone discussion of them for the moment.
The details view
We have a button to "See More", you may have noticed, but it doesn't actually do anything. Let's fix that.
Frameworks note
If you're using a front-end framework (or even a full-stack one), you likely have a router that will handle all this for you. The convoluted code here is a product of our restrictions for this tutorial.
import Alana from "https://sdk.alana4.com/js/alana.js";
const API_KEY = "911e5ddf75e11f976c6ed21df4c24834a92f65b0ce38484c7a58304b2a353aa0";
const API_USERNAME = "innerlightdemo";
const Api = Alana({
apiKey: API_KEY,
apiUsername: API_USERNAME,
mode: 'development',
});
////
// (removed showMemberships() for clarity)
////
export async function listExperiences() {
// Get a list of experiences and put them into cards.
const experiences = await Api.Experiences.list();
const section = document.querySelector("#experiences-list");
const experienceTemplate = document.querySelector("#experience-card");
experiences.forEach(experience => {
const clone = experienceTemplate.content.cloneNode(true);
let title = clone.querySelector(".experience-card-title");
let description = clone.querySelector(".experience-card-description");
let price = clone.querySelector(".experience-card-price");
let buyButton = clone.querySelector(".experience-card-buy");
let seeMoreButton = clone.querySelector(".experience-card-see-more");
title.textContent = experience.name;
description.textContent = experience.description;
price.textContent = `$${experience.retailPrice || experience.memberPrice} / guest`;
section.appendChild(clone);
buyButton.addEventListener('click', () => location.href = `/experiences/buy.html?experience=${experience.id}`);
seeMoreButton.addEventListener('click', () => location.href = `/experiences/?experience=${experience.id}`);
});
}
export async function getExperience(experienceId) {
const section = document.querySelector("#experiences-list");
const errorSection = document.querySelector("#experiences-error");
const experienceTemplate = document.querySelector("#experience-card");
// Get the experience from the platform.
try {
const experience = await Api.Experiences.get(experienceId);
const clone = experienceTemplate.content.cloneNode(true);
let title = clone.querySelector(".experience-card-title");
let description = clone.querySelector(".experience-card-description");
let price = clone.querySelector(".experience-card-price");
let buyButton = clone.querySelector(".experience-card-buy");
let seeMoreButton = clone.querySelector(".experience-card-see-more");
title.textContent = experience.name;
description.textContent = experience.description;
price.textContent = `$${experience.retailPrice || experience.memberPrice} / guest`;
section.appendChild(clone);
buyButton.addEventListener('click', () => location.href = `/experiences/buy.html?experience=${experience.id}`);
} catch (e) {
// This will only happen if the ID is of for an experience that doesn't exist.
// (or if it isn't for our business)
console.error(e)
section.classList.toggle("hidden");
errorSection.classList.toggle("hidden");
}
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alana Platform SDK test</title>
<script type="module">
import { getExperience, listExperiences } from "/site.js";
let params = new URLSearchParams(document.location.search);
// If there was no "experience" query parameter, show the full list.
// Otherwise, only fetch the requested experience.
const experienceId = params.get("experience")
if (experienceId != null) {
await getExperience(experienceId);
} else {
await listExperiences();
}
</script>
<style>
.hidden {
display: none;
}
</style>
</head>
<body>
<header>
<h1>Experiences by Inner Light Wellness</h1>
</header>
<main>
<div id="experiences-list">
</div>
<div id="experiences-error" class="hidden">
<p>The experience you requested doesn't exist. Please try again.</p>
<a href="/experiences">Back to Experiences List</a>
</div>
</main>
<footer>
<p>Powered by <a href="https://alana4.com">ALANA4 and the ALANA SDK</a></p>
</footer>
<template id="experience-card">
<section>
<h1 class="experience-card-title"></h1>
<p class="experience-card-description"></p>
<div>
<span class="experience-card-price"></span>
<div>
<button type="button" class="experience-card-see-more">See More</button>
<button type="button" class="experience-card-buy">Get This Experience</button>
</div>
</div>
</section>
</template>
</body>
</html>
Rather than create a new page for each experience, which would be impossible to maintain, we simply check for a query parameter on the same experiences/ page. If we have one, we assume the user is trying to see the details view for the experience with a specific ID, so we call the new getExperiences() function.
That function uses Experiences.get(), passing the ID, to retrieve a more detailed listing for the experience. Once we have that, we fill in the new card. Adding more content to that card is an exercise for the reader.
Also note in getExperiences() that the SDK call and everything that depends on it are wrapped in a try-catch block. If you attempt to get() an experience that doesn't exist (or that your brand doesn't have access to), the SDK will throw an error. We don't particularly care about the details of that error here, so we just catch it and show a vague "not found" message.
Full and abbreviated resources
Experiences.list() only returns an "abbreviated" object for each experience, while get() returns a full one. They still use the same object class, but the abbreviated version will have null in many of the less important fields.
The logic is that list() should return enough for a listing, as in a search engine or catalog, and that the application would call get() once it needs the full data. This pattern is repeated throughout the SDK.
Next steps
Experiences are a deep subject, and they're worthy of a full tutorial in their own right. Indeed, they're worthy of an entire ALANA Platform site: Local Outing, the first site which truly used the ALANA API.
To keep things moving, we'll leave deeper dives into experiences for a more focused guide. For Inner Light, it's time to go forward and look at the most important part of a user-oriented site: the users!