Select Page

This is how the Poem Pick App ended up being.

The App queries the PoetryDB.org API 

Description:

Initial display:

The title “Poem Pick”;
A message (telling user to start by choosing an author, and that they can have us randomly choose one with the “wild card” button);
A row with a search bar and a “wild card” button;
And a list of author names (in three columns at larger screen sizes).

The user searches for a specific author name and clicks on the name; or just scrolls down through the list and clicks on a name.

Poem title display:

The message changes to say the user should select the title of the poem they’d like displayed.
And the list of author names is replaced by a list of poem titles by that author.
And a new button is added to the row with the search bar: “Forget This. Start Over.”.

The user chooses a specific poem title the same way they’d chosen the author’s name.

Poem display:

The list of poem titles is replaced by a poem.
The message at top disappears.
The row where the search bar used to be now has only a “Start Over” button and a “Download” button.

User can download the file and/or go back to the beginning.

DOM variables:

Initial Screen:

message – connects to the top message (tells user to pick an author or let us randomly pick one; then changes to say pick a poem or let us randomly pick one; then disappears)
filter – connects to the search bar
wildAuthor – connects to the Wild Card button that displays on the initial screen
wildTitle – connects to the Wild Card button that displays on the title screen
backHome – connects to the Forget This. Start Over. button displayed on the page listing all the titles by the selected author
authorsOrTitles – connects to the section with the search bar, the buttons, and the unordered list (on the first two screens)
list – connects to a list of either author names or title names

Final Screen:

poemAndButtons – connects to both the buttons and poem 
thePoem – connects to the poem div in the display-poem section
startOver – connects to the Start Over button
downloadPoem – connects to the Download button

Functions & App flow

Global Variables:

let authorNames = "";
let titles ="";
let selectedAuthor = "";
let thisTextFormatted = "";

We chose these variables because we realized that we needed to use them in functions that we could not easily pass to (from a calling function, like we do when the function-flow goes straight from one function to the next).

getAuthorNames: Fetch a list of author names from the api. Send the list as a JSON file via the authorNames variable to the displayAuthorNames function. 

This function is called right after it is declared. It starts the app.

displayAuthorNames: Loop over every author name from the authorNames list and for each one create a list item in an h4 tag and append it to the unordered “list” DOM variable. The class “author-item” is added to each list item. The function also unhides the search bar and initial author Wild Card button.  Also sets the initial message (instructions at the top of the screen).

wildAuthor.addEventListener: If user clicks on the Wild Card button while on the page with the list of author names, one of the names is randomly selected and sent to the getTitleNames function.

filter.addEventListener: Gives functionality to the search bar. Together with the fact that at any given time either all the author names or all the title names are hidden, this function displays only items that match the user’s input and that are in the correct category (either author names or title names, depending on which screen the user is on.) 

list.addEventListener: Goes off if someone clicks on either an author’s name (from the page showing all the author names) or a title (from the titles page). Because authors are all in h4 and titles are all in h5 tags, it checks to see if the clicked-on item was wrapped in an h4 or an h5 tag, and then sends the clicked on name (of either an author or a title) to either the getTitleNames or the getPoem functions.

getTitleNames: The selectedAuthor parameter is passed here and is used to fetch a list of titles written by that author. We specify that we want an exact match from the list of author’s names (not just any author name that contains the searched-for author’s name) in the poetrydb.org app. The function hides the authors’ page Wild Card button and unhides the titles’ page Wild Card button. Then it sends the list of titles (as a JSON file) to the displayTitles function.

displayTitles: Receives the titles JSON file as a parameter. Hides the Forget This. Start Over. button. Empties everything in the list (so removes all the author names). Sets the value inside the search bar back to an empty string. Loops over all the titles, wrapping them in h5 tags and creating list items out of each one, and appending them to the list (thus creating a list of title names). The function adds the class “title-item” to each list item. Also changes the message to “Select a title by THE SELECTED AUTHOR’S NAME”

wildTitle.addEventListener: If someone clicks on the Wild Card button while on the page listing the titles by the selected author, one of those titles is randomly selected and passed to the displayPoem function.

backHome.addEventListener: If someone clicks on the Forget This. Start Over. button on the list of titles by the selected author, they are taken back to the initial screen. To achieve this the function hides the wildTitle button and unhides the wildAuthor button (switching to the correct Wild Card button); sets the search bar value to an empty string; deletes all title names from the list, hides the Forget This. Start Over. button; and calls the getAuthorNames() function to start the app over again, which unhides the buttons and search bar, displays the correct message, and populates the list with all the author names in the database. 

getPoem: Accepts the selectedTitle parameter from either the link.addEventListener or the WildTile.addEventListener functions. Uses selectedTitle along with the global value selectedAuthor and the abs: keyword to specify the exactly-matching poem by the exactly-matching selected author from the API. Then sends that poem as a text file to the displayPoem app. It sends it as a text file because I was not able to successfully manipulate the JSON file. The text file includes the author’s name, the title, and the poem itself.

displayPoem: Accept the poem parameter as a text file from getPoem. Hide the class for the section that displays author names or titles. Unhide the class for the section that displays the poem. Delete all the titles from the list. Set the message to an empty string. Split the poem up at the line breaks. Use indexing to extract thisAuthor, thisTitle and thisText parts of the poem text. Use splice to lop off everything except the poem text. Then, since you have been working with an array of text items and now need the text to read like a text, use the join method to turn the array into a readable poem. Finally, create a poemDiv div, give it the class poem-div, and put the poem title, author name, and poem text in it, and attach it to the thePoem variable / DOM object.

download(file, text): First creates an invisible element equivalent to

 ahref=”path of file (text)” download=”file name (file)”
Then append that element to the body of the document.
    Then the element.click() event simulates a click on the ahref tag we’d created and the text is downloaded.
    The element is then deleted from the body of the element

downloadPoem.addEventListener: If user clicks on download button, this function grabs the global thisText variable and converts it from html into  a more readable format with thisTextFormatted.replace(/ BreakTag /g, "\n").

The function then creates the generic filename Poem.txt and sends the reformatted text and the generic filename to the download function.

startOver.addEventListener: Reset all to original. Delete everything you have on the poem display page. The trickiest part was:


//delete the existing poem
const poemDivToRemove = document.querySelectorAll(".poem-div");
for (element of poemDivToRemove) {
element.remove();
}

To delete the existing poem, we had to first use querySelectorAll to connect to the .poem-div class and then loop through every element within poemDivToRemove and remove it. I don’t understand why poemDivToRemove acts like an array. But then it turned out I could also achieve the same end with
poemDivToRemove.innerHTML = “”; And that’s easier.

//fetch author names info from poetry db
async function getAuthorNames() {
const authorRequest = await fetch(`https://poetrydb.org/author`);
const authorArray = await authorRequest.json();
authorNames = authorArray.authors;
displayAuthorNames(authorNames);
}

Standard await fetch to get a json file


//display search bar and wild card button, and all the author names
const displayAuthorNames = function(authorNames) {
// first unhide search bar and wild card button
authorsOrtitles.classList.remove("hide");
// loop through all the author names and append each to the unordered list
for (const author of authorNames) {
const nameItem = document.createElement("li");
nameItem.classList.add("author-item");
nameItem.innerHTML = OPENINGTICK
h4 Author: ${author} ENDh4
CLOSINGTICK
list.append(nameItem);
}
// Set the initial message
message.innerHTML = "Select an Author's Name. Or push the Wild Card button, and we'll select one."
}

Creates a list of clickable items with a for loop looping through all the author names in the json file, along with document.createElement(“li”) and writing what’s inside with nameItem.innerHTML. By enclosing all the author names in h4 tags, we differentiate them from the title names, which we will wrap in h5 tags


/ pick random author if user clicks on the wild card when on the author page
wildAuthor.addEventListener("click", () => {
const randomIndex = Math.floor(Math.random() * authorNames.length);
console.log(authorNames);
console.log(randomIndex);
const randomAuthor = authorNames[randomIndex].trim();
selectedAuthor = randomAuthor;
getTitleNames(selectedAuthor);
});

Create a random index and use it to randomly select an item in an array


filter.addEventListener("input", function (e) {
const searchText = e.target.value;
// turn the captured text to lower case
const lowerSearchText = searchText.toLowerCase();
//select all the author names and title names in document
const searchables = document.querySelectorAll(".author-item, .title-item");
for (const searchable of searchables) {
// looping through all the author and title items, capturing lower case versions of all inner text
const lowerSearchable = searchable.innerText.toLowerCase();
// only show items that include some of the searched text
// we already have the list showing either only author names or title names
// so combined with the below code block, we now show only the searched for
// items within the relevant category (either author names or title names)
if (lowerSearchable.includes(lowerSearchText)) {
searchable.classList.remove("hide");
}
else {
searchable.classList.add("hide");
} // if else clause ends here
} //for look ends here
});

Code for a search bar. We capture the typed-in text with e.target.value.  We use toLowerCase(); on both the typed-in text and all the author names to compare apples to apples. To get a list of all searchable items, we used document.querySelectorAll(“.author-item, .title-item”); Since we are already only displaying only author-items or title-items, even if there’s a match with say a title-item when we’re supposed to be searching through author-items, we won’t see that match for the title-item. At first, I was worried that by removing the “hide” from the searchable class I’d reveal author-items when I meant to only reveal title-items and vice-versa. But then I remembered that I only added the “hide” class here in this function, so the fact that only h4s or only h5s are showing is not affected by the adding and removing of hide classes here.


//if an h4 list item is selected, that author name is sent to the getAuthorDetails function
//if an h5 list item is selected, that poem title is sent to the displayPoem function.
list.addEventListener("click", function(e) {
if (e.target.matches("h4")) {
selectedAuthor = e.target.innerText;
getTitleNames(selectedAuthor);
} else if (e.target.matches("h5")) {
const selectedTitle = e.target.innerText;
getPoem(selectedTitle);
}
});

 This event listener figures out if you clicked on an item wrapped in an h4 or an h5 with e.target.matches(“h4”) and e.target.matches(“h5”). Why does that work? Because the matches method has to do with matching selectors. An if else statement together with e.target.innerText sets either selectedAuthor or selectedTitle equal to the inner text of the clicked-on item. And sends that variable to either getTitleNames(selectedAuthor) or getPoem(selectedTitle). So this function works for both the author list and title list.

//get the names of the poems written by the selected author
getTitleNames = async function(selectedAuthor) {
// the :abs in the fetch url specifies an exact match
// (as opposed to just containing the sought string)
const titlesRequest = await fetch(`https://poetrydb.org/author/${selectedAuthor}:abs`);
titles = await titlesRequest.json();
//hides the authors' Wild Card button and unhides the titles' Wild Card button
wildAuthor.classList.add("hide");
wildTitle.classList.remove("hide");
//send the selected title to the displayTitles function
displayTitles(titles)
}

Basic await fetch, but note that to tell the API that I didn’t want all the authors whose names contained the string I requested, but only the author whose name 100% matched that string, I had to use :abs in the fetch


const displayTitles = function(titles) {
backHome.classList.remove("hide");
list.innerHTML ="";
filter.value="";
for (const title of titles) {
const titleItem = document.createElement("li");
titleItem.classList.add("title-item");
//if I don't include the .title here, the title variable has a tilte, and author, and the poem; so it is an
//array of three objects, each containing strings of letters, instead of a single string of letters.
titleItem.innerHTML = `
${title.title}
`
list.append(titleItem);
}
message.innerHTML=`Select a title by ${selectedAuthor}`;
}

Loop through the array of titles and create a list item for each one, add the title-item class, and then append all those list items to the list. Why do I have to use title.title here? I didn’t use author.author for the list of author’s names. 

// pick random title if user clicks on the wild card when on the title page
wildTitle.addEventListener("click", () => {
const randomIndex = Math.floor(Math.random() * titles.length);
console.log(randomIndex);
console.log(titles);
//if I don't include the .title here, the titles array has titles, authors, and lines; so indexing results in undefined
const randomTitle = titles[randomIndex].title.trim();
getPoem(randomTitle);
});

 Pick a random title. Same system as picking a random author. Except here again I have to use the .title to select only the title and not other info. Why does calling titles give me also the author info and the lines, but calling authors only gave me the author names? I guess I should’ve added /title to the end of my request. With authors, the API just returns their names, but with titles, you have to specify what you want to get. I guess that’s how it is.

/go back to the start if the Forget This. Start Over button is clicked.
// (That button is visible only on the page with the list of titles
backHome.addEventListener("click", () => {
//hide the button configuration for the page displaying the titles
wildTitle.classList.add("hide");
//unhide the wild card button
wildAuthor.classList.remove("hide");
//set search back to default;
filter.value="";
//delete all the titles from the list
const titles = document.querySelectorAll(".title-item");
console.log(titles);
for (const title of titles) {
title.remove();
console.log(titles);
}
//hide the backHome button
backHome.classList.add("hide");
//start the app again
// this will unhide the authorsOrTitles buttons and search bar, list the author names, and reinstate the original message
getAuthorNames();
});

This function sets everything back to how it was originally. I learned that the way to get the search form back to its original state was filter.value=””; I was surprised I had to delete all the title names. If I didn’t, they appeared at the top of the author list. To delete the titles, I looped through the titles array and removed each title with the .remove() method.

 

getPoem = async function(selectedTitle) {
//the :abs selects for exact matches (as opposed to partial string matches)
const poemRequest = await fetch(`https://poetrydb.org/title,author/${selectedTitle}:abs;${selectedAuthor}:abs/author,title,lines.text`);
const poem = await poemRequest.text();
displayPoem(poem);
}

 Fetching the selected title by the selected author. I don’t have to pass selectedAuthor here because it is a global variable.  I use :abs to specify exact matches. And I request author, title, and lines from the API. I asked for a text rather than a json file because I’d had trouble manipulating the json file.

displayPoem = function(poem) {
authorsOrtitles.classList.add("hide");
poemAndButtons.classList.remove("hide");
//delete all the titles from the list
const titles = document.querySelectorAll(".title-item");
for (const title of titles) {
title.remove();
}
message.innerHTML = ""
const splitPoem = poem.split("\n");
console.log(splitPoem);
wildTitle.classList.add("hide");
const thisTitle = splitPoem[1];
const thisAuthor = splitPoem[3];
const thisText = splitPoem.splice(5, splitPoem.length);
thisTextFormatted = thisText.join("
")
const poemDiv = document.createElement("div");
poemDiv.classList.add("poem-div")
poemDiv.innerHTML =
STARTTIC
h2Title: ${thisTitle}h2
h4by ${thisAuthor}h4:
${thisTextFormatted}
ENDTIC
thePoem.append(poemDiv);
}

Displaying the poem was a little tricky. I’d had trouble using the json file, so I used instead a text file and spilt it into segments at each new line with .split(“\n”). Then I could use indexing to get at the author name and title. But for text, I needed not just one line, but multiple lines; so for text I used .splice(5, splitPoem.length): to lop off all the lines before the poem (author name, title name, etc) and capture the poem until the last line of the poem. But that was still an array. So I joined the lines into a text with .join(); Why did it automatically add in the new lines?  Were the line breaks encoded in the array? I created a div, gave it the class poem-div, put the title in an h2, the author in an h4, and the poem in a paragraph, and appended the whole thing to thePoem variable/DOM object

 

function download(file, text) {
//creating an invisible element with a path to the text
// and the download attribute set to the name of the file
const element = document.createElement('a');
element.setAttribute('href',
'data:text/plain;charset=utf-8, '
+ encodeURIComponent(text));
element.setAttribute('download', file);
// Above code is equivalent to
// a href="path of file" download="file name"
document.body.appendChild(element);
//onClick property
element.click();
//after download, delete the element we'd created for the download
document.body.removeChild(element)

 

/ Start file download.
downloadPoem.addEventListener("click", function() {
// Generate download of Poem.txt
// Right now the thisTextFormatted is in html
const html = thisTextFormatted;
// turn it into a more readable form by replacing all the
// breaks with new lines
const text = thisTextFormatted.replace(/ BR g, "\n");
const filename = "Poem.txt";
// send the text with the generic filename to the download function
download(filename, text);
//what is the below 'false' for?
// From what I can see, this is set to false by default anyway
}, false)

 

startOver.addEventListener("click", () => {
//hide the buttons and poem on the poem display page
poemAndButtons.classList.add("hide");
//set search back to default
filter.value="";
//start app over
getAuthorNames();
//delete all the titles from the list
const titles = document.querySelectorAll(".title-item");
console.log(titles);
for (let title of titles) {
title.remove();
}
//hide the backHome button
backHome.classList.add("hide");
//delete the existing poem
const poemDivToRemove = document.querySelectorAll(".poem-div");
poemDivToRemove.innerHTML = "";
for (element of poemDivToRemove) {
element.remove();
}
//unhide the author wild card button
wildAuthor.classList.remove("hide");
});