<repo_name>thinkful-ei-quail/johna-bookmarks-app<file_sep>/src/bookmarks.js
import $ from 'jquery';
import api from './bookmarksApi.js';
import store from './bookmarksStore.js';
import trashIcon from './trash.png';

const ifRating = (val, fallback) => ((val >= 1) && (val <= 5)) ? val : fallback;

const bookmarkIdFrom$Target = $target => $target.closest('li').data('id');

// IMPORTANT -- :indeterminate selector only works if these are wrapped by a form
// any call site that isn't naturally inside a form needs to wrap this result w/ <form></form>
const generateRatingEditorHtml = current => `<div class="stars">
  <input name="rating" type="radio" ${(current===1)?'checked ':''}value="1" aria-label="1 Star">
  <input name="rating" type="radio" ${(current===2)?'checked ':''}value="2" aria-label="2 Stars">
  <input name="rating" type="radio" ${(current===3)?'checked ':''}value="3" aria-label="3 Stars">
  <input name="rating" type="radio" ${(current===4)?'checked ':''}value="4" aria-label="4 Stars">
  <input name="rating" type="radio" ${(current===5)?'checked ':''}value="5" aria-label="5 Stars">
  <button class="no-rating" aria-label="No rating" type="button"><span>Reset</span></button>
</div>`;

const generateNewBookmarkFormHtml = expanded => `<form id="new-bookmark-form" class="${expanded?'':'collapsed'}"
  role="dialog" aria-modal="true" aria-labelledby="new-bookmark-form-title"><h2 id="new-bookmark-form-title"
  >Add new bookmark</h2><input name="url" class="http" placeholder="http://samplelink.code/toInfinityAndBeyond"><input
  placeholder="Title" name="title">${generateRatingEditorHtml()}<textarea placeholder="Add a description (optional)"
  id="desc"></textarea><input type="reset" id="cancel-add" value="Cancel"><input type="submit" value="Create">
</form>`;

const generateBookmarkHtml = (id, title, url, desc, rating, expanded) => {
  const ratingText = ifRating(rating,'');
  return `<li data-id="${id}" ><h2 class="bookmark"><button aria-controls="expanded-${id}" aria-expanded="${expanded}"
  class="bookmark-header" id="header-${id}"><span class="header-rating" aria-label="${ratingText?ratingText+' stars':''}"
  data-rating="${ratingText}">${ratingText?'★':''}</span>${title}</button></h2><div
  aria-labelledby="header-${id}" id="expanded-${id}" class="bookmark-details ${expanded?'expanded':'collapsed'}"
  ><div><form>${generateRatingEditorHtml(rating)}</form><a href="${url}">Visit Site</a></div><button
  aria-label="Delete bookmark" class="bookmark-killer"
  ><img src="${trashIcon}"></button><p>${typeof(desc) === 'string' ? desc : ''}</p></div></li>`;
};

const generateBookmarkListHtml = bookmarks => 
  bookmarks.map(b => generateBookmarkHtml(b.id, b.title, b.url, b.desc, b.rating, b.expanded)).join('');

const generateFilterHtml = current => `<select id="filter">
  <option${(current===0)?' selected':''}>No filter</option>
  <option${(current===1)?' selected':''}>1 Star or better</option>
  <option${(current===2)?' selected':''}>2 Star or better</option>
  <option${(current===3)?' selected':''}>3 Star or better</option>
  <option${(current===4)?' selected':''}>4 Star or better</option>
  <option${(current===5)?' selected':''}>5 Star only</option>
</select>`;

const render = () => {
  const minStarRating = store.filter();
  const err = store.error();
  const errText = err ? err : '';
  let bookmarks = store.getAllBookmarks();
  let emptyViewMessage;
  if(minStarRating)
  {
    bookmarks = bookmarks.filter(bookmark => bookmark.rating >= minStarRating);
    emptyViewMessage = '<li><p class="error">None of your bookmarks match the current filter.</p></li>';
  } else {
    emptyViewMessage = `<li><p class="error">You don't have any bookmarks yet. Why not create one?</p></li>`;
  }
  $('main').html(`<div id="controls"><button id="add"><span>+ Add</span></button>${
    generateFilterHtml(store.filter())}</div>${generateNewBookmarkFormHtml(store.adding())}<p
    class="error">${errText}</p><ul>${(bookmarks.length > 0) ? generateBookmarkListHtml(bookmarks) : emptyViewMessage}</ul>`);
  store.error(null);
};

const renderRatingEditor = where => {
  where.replaceWith(generateRatingEditorHtml());
};

const onAddClicked = () => {
  store.adding(!store.adding());
  render();
};

const onAddFormSubmitted = e => {
  e.preventDefault();
  const form = e.target;
  const rating = form.rating.value;
  api.insert(store.encodedUserName(), { title: form.title.value, url: form.url.value, desc: form.desc.value,
    rating: ifRating(rating)})
    .then(bookmark => store.addBookmark(bookmark))
    .then(() => store.adding(false))
    .catch(err => store.error(err))
    .finally(render);
};

const onFilterChanged = e => {
  store.filter(e.target.selectedIndex);
  render();
};

const onHeaderClicked = e => {
  expand(bookmarkIdFrom$Target($(e.target)), true);
};

const onBookmarkKillerClicked = e => {
  const id = bookmarkIdFrom$Target($(e.target));
  api.delete(store.encodedUserName(), id)
    .then(() => store.deleteBookmark(id))
    .catch(err => store.error(err))
    .finally(render);
};

const onBookmarkRated = e => {
  const $target = $(e.target);
  const rating = +$target.val();
  const id = bookmarkIdFrom$Target($target);
  api.update(store.encodedUserName(), id, { rating })
    .then(() => store.findBookmark(id).rating = rating)
    .catch(err => store.error(err))
    .finally(render);
};

const onZeroRatingClicked = e => {
  const $target = $(e.target);
  if($target.closest('#new-bookmark-form')) // for the adder, no api calls yet; just re-render the editor
    renderRatingEditor($target.closest('.stars'));
  else { // the api doesn't seem to allow an existing rating to be nulled or zeroed; we'll hide this feature with css
/*    const bookmark = store.findBookmark(bookmarkIdFrom$Target($target));
    api.update(store.encodedUserName(), bookmark.id, { rating: 0 })
      .then(() => bookmark.rating = 0)
      .catch(err => store.error(err))
      .finally(render);
*/  }
};

const expand = (linkedBookmarkId, toggle) => {
  const linked = store.findBookmark(linkedBookmarkId);
  if(linked) {
    linked.expanded = toggle ? !linked.expanded : true;
    render();
    $(`#header-${linkedBookmarkId}`)[0].scrollIntoView();
  }
};

export default {
  init: function(linkedBookmarkId) {
    api.getAll(store.encodedUserName())
      .then(bookmarks => bookmarks.forEach(bookmark => store.addBookmark(bookmark)))
      .then(() => this.expand(linkedBookmarkId))
      .catch(err => store.error(err))
      .finally(render);
    const $bodyElement = $('body');
    $bodyElement.on('click', '#add', onAddClicked);
    $bodyElement.on('input', '#filter', onFilterChanged);
    $bodyElement.on('click', '#cancel-add', onAddClicked);
    $bodyElement.on('submit', '#new-bookmark-form', onAddFormSubmitted);
    $bodyElement.on('change', 'li .stars input', onBookmarkRated);
    $bodyElement.on('click', '.no-rating', onZeroRatingClicked);
    $bodyElement.on('click', '.bookmark-header', onHeaderClicked);
    $bodyElement.on('click', '.bookmark-killer', onBookmarkKillerClicked);
  },

  expand,
};<file_sep>/src/bookmarksStore.js
const Bookmark = function() {
  this.id = null;
  this.title = 'New Bookmark';
  this.url = 'https://';
  this.desc = null;
  this.rating = null;
  this.expanded = false;
};

const store = {
  adding: false,
  bookmarks: [],
  error: null,
  filter: 0,
  user: null,
};

export default {

  assignUser: (encodedUser, decodedUser) => store.user = { encodedUser, decodedUser },
  decodedUserName: () => store.user.decodedUser,
  encodedUserName: () => store.user.encodedUser,
  error: err => (err!==undefined) ? (store.error = err) : store.error,

  addBookmark: bookmark => store.bookmarks.unshift(Object.assign(new Bookmark(), bookmark)),
  adding: bool => (bool!==undefined) ? (store.adding = bool) : store.adding,
  deleteBookmark: id => store.bookmarks.splice(store.bookmarks.findIndex(bookmark => bookmark.id === id), 1),
  filter: level => (level!==undefined) ? (store.filter = level) : store.filter,
  findBookmark: id => store.bookmarks.find(bookmark => bookmark.id === id),
  getAllBookmarks: () => store.bookmarks,

};<file_sep>/README.md
Bookmarks App

Live Example: <https://thinkful-ei-quail.github.io/johna-bookmarks-app/>

Required Implemented Features:

* I can add bookmarks to my bookmark list. Bookmarks contain:
  * title
  * url link
  * description
  * rating (1-5)
* I can see a list of my bookmarks when I first open the app
* All bookmarks in the list default to a "condensed" view showing only title and rating
* I can click on a bookmark to display the "detailed" view
* Detailed view expands to additionally display description and a "Visit Site" link
* I can remove bookmarks from my bookmark list
* I receive appropriate feedback when I cannot submit a bookmark
* I can select from a dropdown (a &lt;select> element) a "minimum rating" to filter the list by all bookmarks rated at or above the chosen selection


Bonus Implemented Features:

* I can change the rating of an existing bookmark
* I can view and/or change any user's bookmarks by changing the query portion of the URL. For example, JaneLane's bookmarks are at <https://thinkful-ei-quail.github.io/johna-bookmarks-app/?JaneLane>
* I can link to a specific bookmark using the fragment identifier (hash sign); for example, if JaneLane had a bookmark with id 1, I could link to it with <https://thinkful-ei-quail.github.io/johna-bookmarks-app/?JaneLane#header-1>. When the page loads, the requested bookmark is expanded and scrolled into view.
