DEV Community

Cover image for Let's build: Art Recommending App in Vanilla JS using the Harvard Art API - part 3: API
sanderdebr
sanderdebr

Posted on

Let's build: Art Recommending App in Vanilla JS using the Harvard Art API - part 3: API

View demo
View code

6. Setting up the API

6.1 Async and adding loading spinner

For retrieving data from the API we need an asynchronous function, because we do not want the rest of our code to stop. Change the controlsettings function in index to the following:

// SAVE NEW SETTINGS const controlSettings = async () => { // Remove current paintings paintingView.clear(); // Render loader icon paintingView.renderLoader(); // Retrieve settings from settingsView const newSettings = settingsView.getSettings(); // Update state with new settings state.settings.userSettings = newSettings; // New Search object and add to state state.search = new Search(newSettings); paintingView.renderPaintings('test'); } 

Now we will add the methods in the paintingView file by adding the following code:

// CLEAR PAINTINGS export const clear = () => { elements.paintings.forEach(painting => { painting.style.opacity = 0; }) } // RENDER LOADER export const renderLoader = () => { const loader = '<div class="lds-dual-ring"></div>'; elements.artWrapper.insertAdjacentHTML('afterbegin', loader); } 

Our elements.js now contains a couple more query selectors:

export const elements = { settings: document.querySelector('.settings'), buttons: document.querySelectorAll('.box__item'), arrowLeft: document.querySelector('.circle__left'), arrowRight: document.querySelector('.circle__right'), artWrapper: document.querySelector('.art__wrapper'), paintings: document.querySelectorAll('.painting'), paintingImg: document.querySelectorAll('.painting img'), generate: document.querySelector('.box__generate'), classification: document.querySelector('.classification'), period: document.querySelector('.period'), }; 

And add the following code for the loader spinner in main.scss:

// Loader spinner .lds-dual-ring { display: inline-block; width: 80px; height: 80px; position: absolute; z-index: 1; color: $color1; } .lds-dual-ring:after { content: " "; display: block; width: 64px; height: 64px; margin: 8px; border-radius: 50%; border: 6px solid $color1; border-color: $color1 transparent $color1 transparent; animation: lds-dual-ring 1.2s linear infinite; } @keyframes lds-dual-ring { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } 

6.2 Retrieving new paintings from the Harvard Art API

We first need to get our API key from Harvard. You can get one here: https://www.harvardartmuseums.org/collections/api

Then we can go to the documentation and see what we have to do:
https://github.com/harvardartmuseums/api-docs

But let’s first set up our API call in our application. Add the following code in the controlSettings method:

// Retrieve paintings try { // 4) Search for paintings await state.search.getPaintings(); // 5) Render results paintingView.renderPaintings(state.search.result); } catch (err) { alert('Something wrong with the search...'); } 

Then run the command npm install axios this will make it easier for us to do API calls. Then make sure your /models/Search.js looks like this:

import axios from 'axios'; import { key } from '../config'; export default class Search { constructor(query) { this.query = query; } async getResults() { try { const res = await axios(`${proxy}http://food2fork.com/api/search?key=${key}&q=${this.query}`); this.result = res.data.recipes; // console.log(this.result); } catch (error) { alert(error); } } } 

In the main JS folder, create a file called config.js - here we will place our API key.

export const key = ...; 

We want to retrieve at least:
The image path
Name of the artist
Name of the painting

Let’s check how we can do that. With an object we have all the information we need:
https://github.com/harvardartmuseums/api-docs/blob/master/sections/object.md

We will try to run a query in Search.js using the following code

async getPaintings() { try { const res = await axios(`https://api.harvardartmuseums.org/object?person=33430&apikey=${key}`); this.result = res.data; console.log(this.result); } catch (error) { alert(error); } } 

Press generate in the app and check your console.log, it works! We received an object will all kinds of data. Now let’s build the correct query.

6.3 Retrieving data based on users input

Now we need to actually have the real classifications and periods which Harvard Art uses. Let’s get them from the website so your data file looks like this.

export const data = { classification: ['Paintings', 'Photographs', 'Drawings', 'Vessels', 'Prints'], period: ['Middle Kingdom', 'Bronze Age', 'Roman period', 'Iron Age'] } 

Our complete Search.js now looks like:

import axios from 'axios'; import { key } from '../config'; export default class Search { constructor(settings) { this.settings = settings; } buildQuery(settings) { let classification = []; settings.classification.forEach(el => classification.push('&classification=' + el)); classification = classification.toString(); let period = []; settings.period.forEach(el => period.push('&period=' + el)); period = period.toString(); let query = classification + period; query = query.replace(',', ''); this.query = query; } async getPaintings() { try { this.buildQuery(this.settings); const res = await axios(`https://api.harvardartmuseums.org/object?apikey=${key}${this.query}`); console.log(res); this.result = res.data.records; console.log(this.result); } catch (error) { alert(error); } } } 

With our buildQuery function we are setting up our query based on the user settings.

Now let’s render the resulting paintings on the screen, update your renderPaintings function in paintingView with the following:

export const renderPaintings = paintings => { // Remove loader const loader = document.querySelector(`.lds-dual-ring`); if (loader) loader.parentElement.removeChild(loader); console.log(paintings); // Replace paintings elements.paintingImg.forEach((img, i) => { img.src = paintings[i].primaryimageurl; }) // Show paintings again elements.paintings.forEach(painting => { painting.style.opacity = 1; }) } 

6.4 Combining different user preferences

We have a bug now, we can not combine any classifications or periods with each other. Only single requests e.g. period=Iron Age is possible unfortunately with the API. We will solve this by limiting the user to 1 classification and 1 period. Then we will filter the data by period.
We can limit the classification and period by changing our button toggle function:

elements.settings.addEventListener('click', (e) => { if (!e.target.classList.contains('box__generate')) { const activeClassification = document.querySelector('.box__item.active[data-type="classification"]'); const activePeriod = document.querySelector('.box__item.active[data-type="period"]'); const target = e.target.closest('.box__item'); if (target.dataset.type == 'classification' && activeClassification) { settingsView.toggle(activeClassification); } if (target.dataset.type == 'period' && activePeriod) { settingsView.toggle(activePeriod); } settingsView.toggle(target); } }) 

And adding the settingsView.toggle method:

export const toggle = target => { target.classList.toggle("active"); } 

Now the classification part is working! Let’s filter our data if the user has select a period.

Not so many object actually have a period, so lets change the period to century. You can make a folder wide replace in visual code by using SHIFT+CTRL+F and then search and replace for ‘period’ to ‘century’.

Now the data.js file looks like:

export const data = { classification: ['Paintings', 'Jewelry', 'Drawings', 'Vessels', 'Prints'], century: ['16th century', '17th century', '18th century', '19th century', '20th century'] } 

Then remove /models/Settings.js as we do not need the settings state anymore, the search state is enough. Also remove it in the index.js file.

Our complete Search.js file then looks like

import axios from 'axios'; import { key } from '../config'; export default class Search { constructor(settings) { this.settings = settings; } filterByCentury(results) { const century = this.settings.century.toString(); const filtered = results.filter(result => result.century == century); return filtered; } async getPaintings() { try { this.classification = this.settings.classification; const res = await axios(`https://api.harvardartmuseums.org/object?apikey=${key}&classification=${this.classification}&size=100`); this.result = this.filterByCentury(res.data.records); } catch (error) { alert(error); } } } 

Now we can filter the classification that the user has chosen. The resulting artworks are the same every time, let’s make them random by adding a randomize method in Search.js

randomize(data, limit) { let result = []; let numbers = []; for (let i = 0; i <= limit; i++) { const random = Math.floor(Math.random() * data.length); if (numbers.indexOf(random) === -1) { numbers.push(random); result.push(data[random]); } } console.log('result', result); return result; } 

We can filter the limit the data we get back from randomize by the limit variable. The other methods then look like:

filterByCentury(results) { const century = this.settings.century.toString(); const filtered = results.filter(result => result.century == century); const result = this.randomize(filtered, 5); return result; } async getPaintings() { try { this.classification = this.settings.classification.toString(); const res = await axios(`https://api.harvardartmuseums.org/object?apikey=${key}&classification=${this.classification}&size=100`); this.result = this.filterByCentury(res.data.records); } catch (error) { alert(error); } } 

Then we need to update out paintingView.js:

 // RENDER PAINTINGS export const renderPaintings = paintings => { console.log('paintings', paintings); // Show paintings again elements.paintings.forEach(painting => { painting.style.opacity = 1; }) // Replace paintings paintings.forEach((painting, i) => { const imgPath = paintings[i].primaryimageurl; if(imgPath) elements.paintingImg[i].src = imgPath; }) // Remove loader const loader = document.querySelectorAll(`.lds-dual-ring`); if (loader) { loader.forEach(loader => loader.parentElement.removeChild(loader)); } } 

6.5 Loading default artworks

To load a default query we will add the following method to the init function:

// Render default artworks settingsView.renderDefault('Prints', '20th century'); controlSettings(); 

And the in settingsView we will make the selected items active by toggling their classes. We have to select them again because they are rendered later than elements.js select them.

export const renderDefault = (classification, century) => { const buttons = document.querySelectorAll('.box__item'); buttons.forEach(button => { if (button.innerHTML == classification || button.innerHTML == century) { button.classList.toggle('active'); } }) } 

Let’s improve our error handling. We can do this by throwing an error back when no images have been found. Also we will place a loading spinner remove function outside the renderPaintings function so we can call it from the controller.

// RENDER PAINTINGS export const renderPaintings = paintings => { if (paintings.length > 1) { // Show paintings again elements.paintings.forEach(painting => { painting.style.opacity = 1; }) // Replace paintings paintings.forEach((painting, i) => { const imgPath = paintings[i].primaryimageurl; if(imgPath) elements.paintingImg[i].src = imgPath; }) } else { throw "No images found"; } } // Remove loader export const removeLoader = () => { const loader = document.querySelectorAll(`.lds-dual-ring`); if (loader) { loader.forEach(loader => loader.parentElement.removeChild(loader)); } } 

Top comments (0)