commit 1f80e8b35fccac097cbfa0241b6c3ba062bb0b86 Author: anela Date: Thu Jun 2 12:27:45 2022 -0700 initial commit of source code diff --git a/css/nav.css b/css/nav.css new file mode 100644 index 0000000..30911e7 --- /dev/null +++ b/css/nav.css @@ -0,0 +1,23 @@ +/* Navigation bar */ + +nav { + display: flex; + background-color: #ff6600; + align-items: center; + padding: 5px 20px; + border-radius: 3px 3px 0 0; +} + +.navbar-brand { + font-weight: bold; +} + +.nav-link { + font-size: 0.85rem; + margin: 0 3px; +} + +.nav-right { + margin-left: auto; + text-align: right; +} diff --git a/css/site.css b/css/site.css new file mode 100644 index 0000000..a676bb5 --- /dev/null +++ b/css/site.css @@ -0,0 +1,123 @@ +/* General typography */ + +body { + font-family: Arimo, sans-serif; + margin: 8px 7.5vw; +} + +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; +} + +.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/css/stories.css b/css/stories.css new file mode 100644 index 0000000..728d40e --- /dev/null +++ b/css/stories.css @@ -0,0 +1,39 @@ +/* Lists of stories */ + +.stories-list { + margin: 20px 5px; +} + +.stories-list > li { + color: gray; + font-size: 0.8rem; + margin: 10px 0; +} + +#stories-loading-msg { + font-weight: bold; + font-size: 150%; + margin: 20px 30px; +} + + +/* Individual stories */ + +.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; +} + +.story-user { + display: block; +} diff --git a/css/user.css b/css/user.css new file mode 100644 index 0000000..f69c701 --- /dev/null +++ b/css/user.css @@ -0,0 +1,14 @@ +/* Login and signup forms */ + +.account-form button { + width: 4rem; + margin-left: 80px; +} + +#signup-form button { + width: 8rem; +} + +.account-forms-container { + padding-left: 20px; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..227dd9f --- /dev/null +++ b/index.html @@ -0,0 +1,106 @@ + + + + + + + + + + + Hack or Snooze + + + + + + + + + + + + + +
+ + +
Loading…
+ + +
    + +
    + + +
    + + + + + + +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..ce71836 --- /dev/null +++ b/js/main.js @@ -0,0 +1,50 @@ +"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 $loginForm = $("#login-form"); +const $signupForm = $("#signup-form"); + +const $navLogin = $("#nav-login"); +const $navUserProfile = $("#nav-user-profile"); +const $navLogOut = $("#nav-logout"); + +/** 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 = [ + $allStoriesList, + $loginForm, + $signupForm, + ]; + 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/js/models.js b/js/models.js new file mode 100644 index 0000000..04e7d04 --- /dev/null +++ b/js/models.js @@ -0,0 +1,196 @@ +"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: + * - {title, author, url, username, storyId, 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() { + // UNIMPLEMENTED: complete this function! + return "hostname.com"; + } +} + + +/****************************************************************************** + * 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, newStory */) { + // UNIMPLEMENTED: complete this function! + } +} + + +/****************************************************************************** + * 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 } }, + }); + + const { 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 } }, + }); + + const { 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 }, + }); + + const { 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; + } + } +} diff --git a/js/nav.js b/js/nav.js new file mode 100644 index 0000000..c5196d1 --- /dev/null +++ b/js/nav.js @@ -0,0 +1,38 @@ +"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); + evt.preventDefault(); + hidePageComponents(); + putStoriesOnPage(); +} + +$body.on("click", "#nav-all", navAllStories); + +/** Show login/signup on click on "login" */ + +function navLoginClick(evt) { + console.debug("navLoginClick", evt); + evt.preventDefault(); + hidePageComponents(); + $loginForm.show(); + $signupForm.show(); +} + +$navLogin.on("click", navLoginClick); + +/** When a user first logins in, update the navbar to reflect that. */ + +function updateNavOnLogin() { + console.debug("updateNavOnLogin"); + $(".main-nav-links").show(); + $navLogin.hide(); + $navLogOut.show(); + $navUserProfile.text(`${currentUser.username}`).show(); +} diff --git a/js/stories.js b/js/stories.js new file mode 100644 index 0000000..1a652d4 --- /dev/null +++ b/js/stories.js @@ -0,0 +1,52 @@ +"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 + * + * Returns the markup for the story. + */ + +function generateStoryMarkup(story) { + // console.debug("generateStoryMarkup", story); + + const hostName = story.getHostName(); + return $(` +
  1. + + ${story.title} + + (${hostName}) + + posted by ${story.username} +
  2. + `); +} + +/** 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(); +} diff --git a/js/user.js b/js/user.js new file mode 100644 index 0000000..ccf6b78 --- /dev/null +++ b/js/user.js @@ -0,0 +1,116 @@ +"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 + */ + +/** 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(); +}