")
+ setTimeout(()=> $("p.error-msg").remove(), 5000);
+ }
+ }
+ const favoritesArrResponse = response.data.user.favorites
+ currentUser.favorites = favoritesArrResponse;
+ console.debug("favorites modified", currentUser.favorites);
+
+}
+
+/** Gets list of stories from server, generates their HTML, and puts on page.
+ * - default param is $allStoriesList */
+
+function putStoriesOnPage() {
+ console.debug("putStoriesOnPage");
+
+ $allStoriesList.empty();
+
+ // loop through all of our stories and generate HTML for them
+ for (let story of storyList.stories) {
+ const $story = generateStoryMarkup(story);
+ $allStoriesList.append($story);
+
+ if (currentUser) {
+ markFavoriteStories(story);
+ }
+ }
+
+ $allStoriesList.show();
+
+ $("input.favorite-box").on("change", modifyFavoriteStories);
+}
+
+/** Takes user input and sends it to backend. */
+
+async function submitStoriesToAPI(evt){
+ console.debug("submit story", evt);
+ evt.preventDefault();
+
+ //grab title, url, and author
+ const $title = $("#submit-title");
+ const $url = $("#submit-url");
+ const $author = $("#submit-author");
+
+ try{
+ await StoryList.addStory(
+ currentUser, {
+ author: $author.val(),
+ title: $title.val(),
+ url: $url.val(),
+ })
+ }
+ catch({name, message}){
+ $submissionForm.after(`
Unssuccesful Connection to database. Try Again. | Error: ${message} |`)
+ setTimeout(()=> $("p#error-msg").remove(), 5000);
+ return;
+ }
+
+ $submissionForm.trigger("reset");
+ $submissionForm.hide();
+
+ putStoriesOnPage();
+}
+
+$submissionForm.on("submit", submitStoriesToAPI);
+
+/** Checks stories to determine if any of them are favorites.
+ * Marks them if necessary. */
+
+function markFavoriteStories(story){
+ const storyId = story.storyId;
+ const favoritesIds = currentUser.favorites.map(f=> f.storyId);
+
+ if(favoritesIds.includes(storyId)) $(`#${storyId}`).children("input").prop("checked", true);
+}
+
+/** handles event for removing stories */
+async function removeOwnedStories(evt){
+ console.debug("remove story", evt);
+ const storyId = evt.target.parentElement.id;
+
+ const removedStory = await StoryList.delStory(currentUser, storyId);
+
+ $(`#${removedStory.storyId}`).remove();
+
+}
diff --git a/public/Hacker News Clone/js/user.js b/public/Hacker News Clone/js/user.js
new file mode 100644
index 0000000..4255947
--- /dev/null
+++ b/public/Hacker News Clone/js/user.js
@@ -0,0 +1,130 @@
+"use strict";
+
+// global to hold the User instance of the currently-logged-in user
+let currentUser;
+
+/******************************************************************************
+ * User login/signup/login
+ */
+
+/** Handle login form submission. If login ok, sets up the user instance */
+
+async function login(evt) {
+ console.debug("login", evt);
+ evt.preventDefault();
+
+ // grab the username and password
+ const username = $("#login-username").val();
+ const password = $("#login-password").val();
+
+ // User.login retrieves user info from API and returns User instance
+ // which we'll make the globally-available, logged-in user.
+ currentUser = await User.login(username, password);
+
+ $loginForm.trigger("reset");
+
+ saveUserCredentialsInLocalStorage();
+ updateUIOnUserLogin();
+
+ $loginForm.hide();
+ $signupForm.hide(
+ );
+}
+
+$loginForm.on("submit", login);
+
+/** Handle signup form submission. */
+
+async function signup(evt) {
+ console.debug("signup", evt);
+ evt.preventDefault();
+
+ const name = $("#signup-name").val();
+ const username = $("#signup-username").val();
+ const password = $("#signup-password").val();
+
+ // User.signup retrieves user info from API and returns User instance
+ // which we'll make the globally-available, logged-in user.
+ try{
+ currentUser = await User.signup(username, password, name);
+ }
+ catch{
+ $signupForm.trigger("reset");
+ $('nav').after("
Unable to create user. Check your connection and perhaps try a different username.
")
+ setTimeout(()=> $("p.error-msg").remove(), 5000);
+ return;
+ }
+
+ saveUserCredentialsInLocalStorage();
+ updateUIOnUserLogin();
+
+ $signupForm.trigger("reset");
+ $loginForm.hide();
+ $signupForm.hide();
+}
+
+$signupForm.on("submit", signup);
+
+/** Handle click of logout button
+ *
+ * Remove their credentials from localStorage and refresh page
+ */
+
+function logout(evt) {
+ console.debug("logout", evt);
+ localStorage.clear();
+ location.reload();
+}
+
+$navLogOut.on("click", logout);
+
+/******************************************************************************
+ * Storing/recalling previously-logged-in-user with localStorage
+ */
+
+/** If there are user credentials in local storage, use those to log in
+ * that user. This is meant to be called on page load, just once.
+ */
+
+async function checkForRememberedUser() {
+ console.debug("checkForRememberedUser");
+ const token = localStorage.getItem("token");
+ const username = localStorage.getItem("username");
+ if (!token || !username) return false;
+
+ // try to log in with these credentials (will be null if login failed)
+ currentUser = await User.loginViaStoredCredentials(token, username);
+}
+
+/** Sync current user information to localStorage.
+ *
+ * We store the username/token in localStorage so when the page is refreshed
+ * (or the user revisits the site later), they will still be logged in.
+ */
+
+function saveUserCredentialsInLocalStorage() {
+ console.debug("saveUserCredentialsInLocalStorage");
+ if (currentUser) {
+ localStorage.setItem("token", currentUser.loginToken);
+ localStorage.setItem("username", currentUser.username);
+ }
+}
+
+/******************************************************************************
+ * General UI stuff about users
+ */
+
+/** When a user signs up or registers, we want to set up the UI for them:
+ *
+ * - show the stories list
+ * - update nav bar options for logged-in user
+ * - generate the user profile part of the page
+ */
+
+function updateUIOnUserLogin() {
+ console.debug("updateUIOnUserLogin");
+
+ $allStoriesList.show();
+
+ updateNavOnLogin();
+}
diff --git a/public/Hacker News Clone/solution/css/nav.css b/public/Hacker News Clone/solution/css/nav.css
new file mode 100755
index 0000000..96c898f
--- /dev/null
+++ b/public/Hacker News Clone/solution/css/nav.css
@@ -0,0 +1,30 @@
+/* Navigation bar */
+
+nav {
+ display: flex;
+ background-color: #ff6600;
+ align-items: center;
+ padding: 25px 20px;
+ border-radius: 3px 3px 0 0;
+}
+
+.navbar-brand {
+ font-weight: bold;
+}
+
+.main-nav-links {
+ margin: 0 10px;
+ font-size: 20px;
+ justify-content: center;
+ align-items: center;
+}
+
+.nav-link {
+ font-size: 0.85rem;
+ margin: 0 3px;
+}
+
+.nav-right {
+ margin-left: auto;
+ text-align: right;
+}
diff --git a/public/Hacker News Clone/solution/css/site.css b/public/Hacker News Clone/solution/css/site.css
new file mode 100755
index 0000000..06bb4fc
--- /dev/null
+++ b/public/Hacker News Clone/solution/css/site.css
@@ -0,0 +1,131 @@
+/* General typography */
+
+body {
+ font-family: Arimo, sans-serif;
+ margin: 8px 7.5vw;
+ height: 100vh;
+ background-image: radial-gradient(circle, rgb(238, 174, 202) 0%, rgb(148, 187, 233) 100%);
+}
+
+h1 {
+ font-size: 1.1rem;
+ margin: 0;
+}
+
+h4 {
+ font-size: 1rem;
+ margin: 0;
+}
+
+h5 {
+ font-size: 0.9rem;
+ font-weight: lighter;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+
+/* Site layout */
+
+/* This is the basic box that the main part of the page goes into */
+.container {
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ background-color: #f6f6ef;
+
+}
+
+.stories-container{
+ height: calc(100vh - 100px);
+ overflow: scroll;
+}
+
+.hidden {
+ display: none;
+}
+
+
+/* Forms */
+
+form {
+ display: flex;
+ flex-direction: column;
+ margin: 8px 18px 0;
+}
+
+form > * {
+ margin: 10px 0;
+}
+
+form label {
+ font-size: 0.9rem;
+ font-weight: 700;
+ display: inline-block;
+ width: 3.5rem;
+ text-align: right;
+ margin-right: 5px;
+}
+
+form input {
+ font-size: 0.8rem;
+ border: none;
+ border-radius: 2px;
+ padding: 8px;
+ width: 300px;
+ box-shadow: 0 0 3px 1px lightgray;
+}
+
+form input:focus {
+ outline: none;
+ box-shadow: 0 0 4px 1px darkgray;
+}
+
+form > button {
+ width: 4rem;
+ margin: 5px 0 15px 65px;
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 0.85rem;
+ background-color: lightslategray;
+ color: white;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+form > button:hover {
+ background-color: dimgray;
+}
+
+form > hr {
+ margin: 0;
+ border: 1px solid lightgray;
+}
+
+.login-input label {
+ width: 70px;
+}
+
+
+/* responsive queries for tightening things up for mobile. */
+
+@media screen and (max-width: 576px) {
+ body {
+ margin: 0;
+ }
+}
+
+@media screen and (min-width: 992px) {
+ body {
+ max-width: 900px;
+ margin: 8px auto;
+ }
+}
diff --git a/public/Hacker News Clone/solution/css/stories.css b/public/Hacker News Clone/solution/css/stories.css
new file mode 100755
index 0000000..fee3ffb
--- /dev/null
+++ b/public/Hacker News Clone/solution/css/stories.css
@@ -0,0 +1,64 @@
+/* Lists of stories */
+
+.stories-list {
+ margin: 20px 5px;
+}
+
+.stories-list > li {
+ color: gray;
+ font-size: 0.8rem;
+ margin: 10px 0;
+ border-bottom: 1px solid #e5e5e5;
+ padding-bottom: 10px;
+}
+
+#favorited-stories,
+#my-stories {
+ list-style: none;
+ padding-left: 20px;
+}
+
+#stories-loading-msg {
+ font-weight: bold;
+ font-size: 150%;
+ margin: 20px 30px;
+}
+
+
+/* Individual stories */
+
+.star,
+.trash-can {
+ font-size: 0.75rem;
+ margin: 0 5px;
+ cursor: pointer;
+}
+
+.trash-can:hover {
+ color: crimson;
+}
+
+.story-link {
+ color: black;
+ font-size: 0.85rem;
+ font-weight: normal;
+ margin: 18px 0;
+}
+
+.story-link:hover {
+ text-decoration: none;
+}
+
+.story-author {
+ margin-left: 2em;
+ font-size: 0.85rem;
+ color: green;
+ padding: 6px 0;
+}
+
+.story-user {
+ display: block;
+ margin-left: 2em;
+ font-size: 0.85rem;
+ color: orange;
+}
diff --git a/public/Hacker News Clone/solution/css/user.css b/public/Hacker News Clone/solution/css/user.css
new file mode 100755
index 0000000..b382f00
--- /dev/null
+++ b/public/Hacker News Clone/solution/css/user.css
@@ -0,0 +1,26 @@
+/* Login and signup forms */
+
+.account-form button {
+ width: 4rem;
+ margin-left: 80px;
+}
+
+#signup-form button {
+ width: 8rem;
+}
+
+.account-forms-container {
+ padding-left: 20px;
+}
+
+
+/* User profile */
+
+.user-profile-box {
+ padding: 10px 20px 20px;
+ font-size: 0.9rem;
+}
+
+.user-profile-box > * {
+ margin: 10px 0;
+}
diff --git a/public/Hacker News Clone/solution/index.html b/public/Hacker News Clone/solution/index.html
new file mode 100755
index 0000000..0d4e495
--- /dev/null
+++ b/public/Hacker News Clone/solution/index.html
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+ Hack or Snooze
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
User Profile Info
+
+
Name: Name:
+
Username: Username:
+
Account Created:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/Hacker News Clone/solution/js/main.js b/public/Hacker News Clone/solution/js/main.js
new file mode 100755
index 0000000..72e92e9
--- /dev/null
+++ b/public/Hacker News Clone/solution/js/main.js
@@ -0,0 +1,64 @@
+"use strict";
+
+// So we don't have to keep re-finding things on page, find DOM elements once:
+
+const $body = $("body");
+
+const $storiesLoadingMsg = $("#stories-loading-msg");
+const $allStoriesList = $("#all-stories-list");
+const $favoritedStories = $("#favorited-stories");
+const $ownStories = $("#my-stories");
+const $storiesContainer = $("#stories-container")
+
+
+// selector that finds all three story lists
+const $storiesLists = $(".stories-list");
+
+const $loginForm = $("#login-form");
+const $signupForm = $("#signup-form");
+
+const $submitForm = $("#submit-form");
+
+const $navSubmitStory = $("#nav-submit-story");
+const $navLogin = $("#nav-login");
+const $navUserProfile = $("#nav-user-profile");
+const $navLogOut = $("#nav-logout");
+
+const $userProfile = $("#user-profile");
+
+/** To make it easier for individual components to show just themselves, this
+ * is a useful function that hides pretty much everything on the page. After
+ * calling this, individual components can re-show just what they want.
+ */
+
+function hidePageComponents() {
+ const components = [
+ $storiesLists,
+ $submitForm,
+ $loginForm,
+ $signupForm,
+ $userProfile
+ ];
+ components.forEach(c => c.hide());
+}
+
+/** Overall function to kick off the app. */
+
+async function start() {
+ console.debug("start");
+
+ // "Remember logged-in user" and log in, if credentials in localStorage
+ await checkForRememberedUser();
+ await getAndShowStoriesOnStart();
+
+ // if we got a logged-in user
+ if (currentUser) updateUIOnUserLogin();
+}
+
+// Once the DOM is entirely loaded, begin the app
+
+console.warn("HEY STUDENT: This program sends many debug messages to" +
+ " the console. If you don't see the message 'start' below this, you're not" +
+ " seeing those helpful debug messages. In your browser console, click on" +
+ " menu 'Default Levels' and add Verbose");
+$(start);
diff --git a/public/Hacker News Clone/solution/js/models.js b/public/Hacker News Clone/solution/js/models.js
new file mode 100755
index 0000000..097e29c
--- /dev/null
+++ b/public/Hacker News Clone/solution/js/models.js
@@ -0,0 +1,267 @@
+"use strict";
+
+const BASE_URL = "https://hack-or-snooze-v3.herokuapp.com";
+
+/******************************************************************************
+ * Story: a single story in the system
+ */
+
+class Story {
+
+ /** Make instance of Story from data object about story:
+ * - {storyId, title, author, url, username, createdAt}
+ */
+
+ constructor({ storyId, title, author, url, username, createdAt }) {
+ this.storyId = storyId;
+ this.title = title;
+ this.author = author;
+ this.url = url;
+ this.username = username;
+ this.createdAt = createdAt;
+ }
+
+ /** Parses hostname out of URL and returns it. */
+
+ getHostName() {
+ return new URL(this.url).host;
+ }
+}
+
+
+/******************************************************************************
+ * List of Story instances: used by UI to show story lists in DOM.
+ */
+
+class StoryList {
+ constructor(stories) {
+ this.stories = stories;
+ }
+
+ /** Generate a new StoryList. It:
+ *
+ * - calls the API
+ * - builds an array of Story instances
+ * - makes a single StoryList instance out of that
+ * - returns the StoryList instance.
+ */
+
+ static async getStories() {
+ // Note presence of `static` keyword: this indicates that getStories is
+ // **not** an instance method. Rather, it is a method that is called on the
+ // class directly. Why doesn't it make sense for getStories to be an
+ // instance method?
+
+ // query the /stories endpoint (no auth required)
+ const response = await axios({
+ url: `${BASE_URL}/stories`,
+ method: "GET",
+ });
+
+ // turn plain old story objects from API into instances of Story class
+ const stories = response.data.stories.map(story => new Story(story));
+
+ // build an instance of our own class using the new array of stories
+ return new StoryList(stories);
+ }
+
+ /** Adds story data to API, makes a Story instance, adds it to story list.
+ * - user - the current instance of User who will post the story
+ * - obj of {title, author, url}
+ *
+ * Returns the new Story instance
+ */
+
+ async addStory(user, { title, author, url }) {
+ const token = user.loginToken;
+ const response = await axios({
+ method: "POST",
+ url: `${BASE_URL}/stories`,
+ data: { token, story: { title, author, url } },
+ });
+
+ const story = new Story(response.data.story);
+ this.stories.unshift(story);
+ user.ownStories.unshift(story);
+
+ return story;
+ }
+
+ /** Delete story from API and remove from the story lists.
+ *
+ * - user: the current User instance
+ * - storyId: the ID of the story you want to remove
+ */
+
+ async removeStory(user, storyId) {
+ const token = user.loginToken;
+ await axios({
+ url: `${BASE_URL}/stories/${storyId}`,
+ method: "DELETE",
+ data: { token: user.loginToken }
+ });
+
+ // filter out the story whose ID we are removing
+ this.stories = this.stories.filter(story => story.storyId !== storyId);
+
+ // do the same thing for the user's list of stories & their favorites
+ user.ownStories = user.ownStories.filter(s => s.storyId !== storyId);
+ user.favorites = user.favorites.filter(s => s.storyId !== storyId);
+ }
+}
+
+
+/******************************************************************************
+ * User: a user in the system (only used to represent the current user)
+ */
+
+class User {
+ /** Make user instance from obj of user data and a token:
+ * - {username, name, createdAt, favorites[], ownStories[]}
+ * - token
+ */
+
+ constructor({
+ username,
+ name,
+ createdAt,
+ favorites = [],
+ ownStories = []
+ },
+ token) {
+ this.username = username;
+ this.name = name;
+ this.createdAt = createdAt;
+
+ // instantiate Story instances for the user's favorites and ownStories
+ this.favorites = favorites.map(s => new Story(s));
+ this.ownStories = ownStories.map(s => new Story(s));
+
+ // store the login token on the user so it's easy to find for API calls.
+ this.loginToken = token;
+ }
+
+ /** Register new user in API, make User instance & return it.
+ *
+ * - username: a new username
+ * - password: a new password
+ * - name: the user's full name
+ */
+
+ static async signup(username, password, name) {
+ const response = await axios({
+ url: `${BASE_URL}/signup`,
+ method: "POST",
+ data: { user: { username, password, name } },
+ });
+
+ let { user } = response.data;
+
+ return new User(
+ {
+ username: user.username,
+ name: user.name,
+ createdAt: user.createdAt,
+ favorites: user.favorites,
+ ownStories: user.stories
+ },
+ response.data.token
+ );
+ }
+
+ /** Login in user with API, make User instance & return it.
+
+ * - username: an existing user's username
+ * - password: an existing user's password
+ */
+
+ static async login(username, password) {
+ const response = await axios({
+ url: `${BASE_URL}/login`,
+ method: "POST",
+ data: { user: { username, password } },
+ });
+
+ let { user } = response.data;
+
+ return new User(
+ {
+ username: user.username,
+ name: user.name,
+ createdAt: user.createdAt,
+ favorites: user.favorites,
+ ownStories: user.stories
+ },
+ response.data.token
+ );
+ }
+
+ /** When we already have credentials (token & username) for a user,
+ * we can log them in automatically. This function does that.
+ */
+
+ static async loginViaStoredCredentials(token, username) {
+ try {
+ const response = await axios({
+ url: `${BASE_URL}/users/${username}`,
+ method: "GET",
+ params: { token },
+ });
+
+ let { user } = response.data;
+
+ return new User(
+ {
+ username: user.username,
+ name: user.name,
+ createdAt: user.createdAt,
+ favorites: user.favorites,
+ ownStories: user.stories
+ },
+ token
+ );
+ } catch (err) {
+ console.error("loginViaStoredCredentials failed", err);
+ return null;
+ }
+ }
+
+ /** Add a story to the list of user favorites and update the API
+ * - story: a Story instance to add to favorites
+ */
+
+ async addFavorite(story) {
+ this.favorites.push(story);
+ await this._addOrRemoveFavorite("add", story)
+ }
+
+ /** Remove a story to the list of user favorites and update the API
+ * - story: the Story instance to remove from favorites
+ */
+
+ async removeFavorite(story) {
+ this.favorites = this.favorites.filter(s => s.storyId !== story.storyId);
+ await this._addOrRemoveFavorite("remove", story);
+ }
+
+ /** Update API with favorite/not-favorite.
+ * - newState: "add" or "remove"
+ * - story: Story instance to make favorite / not favorite
+ * */
+
+ async _addOrRemoveFavorite(newState, story) {
+ const method = newState === "add" ? "POST" : "DELETE";
+ const token = this.loginToken;
+ await axios({
+ url: `${BASE_URL}/users/${this.username}/favorites/${story.storyId}`,
+ method: method,
+ data: { token },
+ });
+ }
+
+ /** Return true/false if given Story instance is a favorite of this user. */
+
+ isFavorite(story) {
+ return this.favorites.some(s => (s.storyId === story.storyId));
+ }
+}
diff --git a/public/Hacker News Clone/solution/js/nav.js b/public/Hacker News Clone/solution/js/nav.js
new file mode 100755
index 0000000..4ce352e
--- /dev/null
+++ b/public/Hacker News Clone/solution/js/nav.js
@@ -0,0 +1,79 @@
+"use strict";
+
+/******************************************************************************
+ * Handling navbar clicks and updating navbar
+ */
+
+/** Show main list of all stories when click site name */
+
+function navAllStories(evt) {
+ console.debug("navAllStories", evt);
+ hidePageComponents();
+ putStoriesOnPage();
+}
+
+$body.on("click", "#nav-all", navAllStories);
+
+/** Show story submit form on clicking story "submit" */
+
+function navSubmitStoryClick(evt) {
+ console.debug("navSubmitStoryClick", evt);
+ hidePageComponents();
+ $allStoriesList.show();
+ $submitForm.show();
+}
+
+$navSubmitStory.on("click", navSubmitStoryClick);
+
+/** Show favorite stories on click on "favorites" */
+
+function navFavoritesClick(evt) {
+ console.debug("navFavoritesClick", evt);
+ hidePageComponents();
+ putFavoritesListOnPage();
+}
+
+$body.on("click", "#nav-favorites", navFavoritesClick);
+
+/** Show My Stories on clicking "my stories" */
+
+function navMyStories(evt) {
+ console.debug("navMyStories", evt);
+ hidePageComponents();
+ putUserStoriesOnPage();
+ $ownStories.show();
+}
+
+$body.on("click", "#nav-my-stories", navMyStories);
+
+/** Show login/signup on click on "login" */
+
+function navLoginClick(evt) {
+ console.debug("navLoginClick", evt);
+ hidePageComponents();
+ $loginForm.show();
+ $signupForm.show();
+ $storiesContainer.hide()
+}
+
+$navLogin.on("click", navLoginClick);
+
+/** Hide everything but profile on click on "profile" */
+
+function navProfileClick(evt) {
+ console.debug("navProfileClick", evt);
+ hidePageComponents();
+ $userProfile.show();
+}
+
+$navUserProfile.on("click", navProfileClick);
+
+/** When a user first logins in, update the navbar to reflect that. */
+
+function updateNavOnLogin() {
+ console.debug("updateNavOnLogin");
+ $(".main-nav-links").css('display', 'flex');;
+ $navLogin.hide();
+ $navLogOut.show();
+ $navUserProfile.text(`${currentUser.username}`).show();
+}
diff --git a/public/Hacker News Clone/solution/js/stories.js b/public/Hacker News Clone/solution/js/stories.js
new file mode 100755
index 0000000..c9dc252
--- /dev/null
+++ b/public/Hacker News Clone/solution/js/stories.js
@@ -0,0 +1,192 @@
+"use strict";
+
+// This is the global list of the stories, an instance of StoryList
+let storyList;
+
+/** Get and show stories when site first loads. */
+
+async function getAndShowStoriesOnStart() {
+ storyList = await StoryList.getStories();
+ $storiesLoadingMsg.remove();
+
+ putStoriesOnPage();
+}
+
+/**
+ * A render method to render HTML for an individual Story instance
+ * - story: an instance of Story
+ * - showDeleteBtn: show delete button?
+ *
+ * Returns the markup for the story.
+ */
+
+function generateStoryMarkup(story, showDeleteBtn = false) {
+ // console.debug("generateStoryMarkup", story);
+
+ const hostName = story.getHostName();
+
+ // if a user is logged in, show favorite/not-favorite star
+ const showStar = Boolean(currentUser);
+
+ return $(`
+
+ `);
+}
+
+/** Make delete button HTML for story */
+
+function getDeleteBtnHTML() {
+ return `
+
+
+ `;
+}
+
+/** Make favorite/not-favorite star for story */
+
+function getStarHTML(story, user) {
+ const isFavorite = user.isFavorite(story);
+ const starType = isFavorite ? "fas" : "far";
+ return `
+
+
+ `;
+}
+
+/** Gets list of stories from server, generates their HTML, and puts on page. */
+
+function putStoriesOnPage() {
+ console.debug("putStoriesOnPage");
+
+ $allStoriesList.empty();
+
+ // loop through all of our stories and generate HTML for them
+ for (let story of storyList.stories) {
+ const $story = generateStoryMarkup(story);
+ $allStoriesList.append($story);
+ }
+
+ $allStoriesList.show();
+}
+
+/** Handle deleting a story. */
+
+async function deleteStory(evt) {
+ console.debug("deleteStory");
+
+ const $closestLi = $(evt.target).closest("li");
+ const storyId = $closestLi.attr("id");
+
+ await storyList.removeStory(currentUser, storyId);
+
+ // re-generate story list
+ await putUserStoriesOnPage();
+}
+
+$ownStories.on("click", ".trash-can", deleteStory);
+
+/** Handle submitting new story form. */
+
+async function submitNewStory(evt) {
+ console.debug("submitNewStory");
+ evt.preventDefault();
+
+ // grab all info from form
+ const title = $("#create-title").val();
+ const url = $("#create-url").val();
+ const author = $("#create-author").val();
+ const username = currentUser.username
+ const storyData = { title, url, author, username };
+
+ const story = await storyList.addStory(currentUser, storyData);
+
+ const $story = generateStoryMarkup(story);
+ $allStoriesList.prepend($story);
+
+ // hide the form and reset it
+ $submitForm.slideUp("slow");
+ $submitForm.trigger("reset");
+}
+
+$submitForm.on("submit", submitNewStory);
+
+/******************************************************************************
+ * Functionality for list of user's own stories
+ */
+
+function putUserStoriesOnPage() {
+ console.debug("putUserStoriesOnPage");
+
+ $ownStories.empty();
+
+ if (currentUser.ownStories.length === 0) {
+ $ownStories.append("
No stories added by user yet!
");
+ } else {
+ // loop through all of users stories and generate HTML for them
+ for (let story of currentUser.ownStories) {
+ let $story = generateStoryMarkup(story, true);
+ $ownStories.append($story);
+ }
+ }
+
+ $ownStories.show();
+}
+
+/******************************************************************************
+ * Functionality for favorites list and starr/un-starr a story
+ */
+
+/** Put favorites list on page. */
+
+function putFavoritesListOnPage() {
+ console.debug("putFavoritesListOnPage");
+
+ $favoritedStories.empty();
+
+ if (currentUser.favorites.length === 0) {
+ $favoritedStories.append("
No favorites added!
");
+ } else {
+ // loop through all of users favorites and generate HTML for them
+ for (let story of currentUser.favorites) {
+ const $story = generateStoryMarkup(story);
+ $favoritedStories.append($story);
+ }
+ }
+
+ $favoritedStories.show();
+}
+
+/** Handle favorite/un-favorite a story */
+
+async function toggleStoryFavorite(evt) {
+ console.debug("toggleStoryFavorite");
+
+ const $tgt = $(evt.target);
+ const $closestLi = $tgt.closest("li");
+ const storyId = $closestLi.attr("id");
+ const story = storyList.stories.find(s => s.storyId === storyId);
+
+ // see if the item is already favorited (checking by presence of star)
+ if ($tgt.hasClass("fas")) {
+ // currently a favorite: remove from user's fav list and change star
+ await currentUser.removeFavorite(story);
+ $tgt.closest("i").toggleClass("fas far");
+ } else {
+ // currently not a favorite: do the opposite
+ await currentUser.addFavorite(story);
+ $tgt.closest("i").toggleClass("fas far");
+ }
+}
+
+$storiesLists.on("click", ".star", toggleStoryFavorite);
diff --git a/public/Hacker News Clone/solution/js/user.js b/public/Hacker News Clone/solution/js/user.js
new file mode 100755
index 0000000..f03896a
--- /dev/null
+++ b/public/Hacker News Clone/solution/js/user.js
@@ -0,0 +1,132 @@
+"use strict";
+
+// global to hold the User instance of the currently-logged-in user
+let currentUser;
+
+/******************************************************************************
+ * User login/signup/login
+ */
+
+/** Handle login form submission. If login ok, sets up the user instance */
+
+async function login(evt) {
+ console.debug("login", evt);
+ evt.preventDefault();
+
+ // grab the username and password
+ const username = $("#login-username").val();
+ const password = $("#login-password").val();
+
+ // User.login retrieves user info from API and returns User instance
+ // which we'll make the globally-available, logged-in user.
+ currentUser = await User.login(username, password);
+
+ $loginForm.trigger("reset");
+
+ saveUserCredentialsInLocalStorage();
+ updateUIOnUserLogin();
+}
+
+$loginForm.on("submit", login);
+
+/** Handle signup form submission. */
+
+async function signup(evt) {
+ console.debug("signup", evt);
+ evt.preventDefault();
+
+ const name = $("#signup-name").val();
+ const username = $("#signup-username").val();
+ const password = $("#signup-password").val();
+
+ // User.signup retrieves user info from API and returns User instance
+ // which we'll make the globally-available, logged-in user.
+ currentUser = await User.signup(username, password, name);
+
+ saveUserCredentialsInLocalStorage();
+ updateUIOnUserLogin();
+
+ $signupForm.trigger("reset");
+}
+
+$signupForm.on("submit", signup);
+
+/** Handle click of logout button
+ *
+ * Remove their credentials from localStorage and refresh page
+ */
+
+function logout(evt) {
+ console.debug("logout", evt);
+ localStorage.clear();
+ location.reload();
+}
+
+$navLogOut.on("click", logout);
+
+/******************************************************************************
+ * Storing/recalling previously-logged-in-user with localStorage
+ */
+
+/** If there are user credentials in local storage, use those to log in
+ * that user. This is meant to be called on page load, just once.
+ */
+
+async function checkForRememberedUser() {
+ console.debug("checkForRememberedUser");
+ const token = localStorage.getItem("token");
+ const username = localStorage.getItem("username");
+ if (!token || !username) return false;
+
+ // try to log in with these credentials (will be null if login failed)
+ currentUser = await User.loginViaStoredCredentials(token, username);
+}
+
+/** Sync current user information to localStorage.
+ *
+ * We store the username/token in localStorage so when the page is refreshed
+ * (or the user revisits the site later), they will still be logged in.
+ */
+
+function saveUserCredentialsInLocalStorage() {
+ console.debug("saveUserCredentialsInLocalStorage");
+ if (currentUser) {
+ localStorage.setItem("token", currentUser.loginToken);
+ localStorage.setItem("username", currentUser.username);
+ }
+}
+
+/******************************************************************************
+ * General UI stuff about users & profiles
+ */
+
+/** When a user signs up or registers, we want to set up the UI for them:
+ *
+ * - show the stories list
+ * - update nav bar options for logged-in user
+ * - generate the user profile part of the page
+ */
+
+async function updateUIOnUserLogin() {
+ console.debug("updateUIOnUserLogin");
+
+ hidePageComponents();
+
+ // re-display stories (so that "favorite" stars can appear)
+ putStoriesOnPage();
+ $allStoriesList.show();
+
+ updateNavOnLogin();
+ generateUserProfile();
+ $storiesContainer.show()
+}
+
+/** Show a "user profile" part of page built from the current user's info. */
+
+function generateUserProfile() {
+ console.debug("generateUserProfile");
+
+ $("#profile-name").text(currentUser.name);
+ $("#profile-username").text(currentUser.username);
+ $("#profile-account-date").text(currentUser.createdAt.slice(0, 10));
+}
diff --git a/src/assets/nav.jsx b/src/assets/nav.jsx
index e518def..2528019 100644
--- a/src/assets/nav.jsx
+++ b/src/assets/nav.jsx
@@ -66,6 +66,7 @@ function Menu({ menuState }) {