initial commit of source code

main
anela 2 years ago
commit 1f80e8b35f

@ -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;
}

@ -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;
}
}

@ -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;
}

@ -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;
}

@ -0,0 +1,106 @@
<!DOCTYPE html>
<!-- Hack or Snooze
This is the only HTML page; the applications manipulates this DOM
during use.
Primary authors:
- Michael Hueter: initial creation, 2018
- Elie Schoppik: refactoring using OO, 2019
- Joel Burton: refactored and componentized, 2020
- You!
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hack or Snooze</title>
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.3.1/css/all.css"
integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU"
crossorigin="anonymous">
<link rel="stylesheet" href="css/site.css">
<link rel="stylesheet" href="css/user.css">
<link rel="stylesheet" href="css/stories.css">
<link rel="stylesheet" href="css/nav.css">
</head>
<body>
<!-- top navigation bar -->
<nav>
<div class="navbar-brand">
<a class="nav-link" href="#" id="nav-all">Hack or Snooze</a>
</div>
<div class="nav-right">
<a class="nav-link" href="#" id="nav-login">login/signup</a>
<a class="nav-link" href="#" id="nav-user-profile"></a>
<a class="hidden" id="nav-logout" href="#"><small>(logout)</small></a>
</div>
</nav>
<!-- area for stories (all stories, user stories, favorites) -->
<section class="stories-container container">
<!-- loading message (removed by JS after stories loaded) -->
<div id="stories-loading-msg">Loading&hellip;</div>
<!-- List of all stories -->
<ol id="all-stories-list" class="stories-list"></ol>
</section>
<!-- Login and signup forms -->
<section class="account-forms-container container">
<!-- Login form -->
<form id="login-form" class="account-form hidden">
<h4>Login</h4>
<div class="login-input">
<label for="login-username">username</label>
<input id="login-username" autocomplete="current-username">
</div>
<div class="login-input">
<label for="login-password">password</label>
<input id="login-password" type="password" autocomplete="current-password">
</div>
<button type="submit">login</button>
<hr>
</form>
<!-- Signup form -->
<form id="signup-form" class="account-form hidden">
<h4>Create Account</h4>
<div class="login-input">
<label for="signup-name">name</label>
<input id="signup-name" autocapitalize="words">
</div>
<div class="login-input">
<label for="signup-username">username</label>
<input id="signup-username" autocomplete="new-username">
</div>
<div class="login-input">
<label for="signup-password">password</label>
<input id="signup-password" autocomplete="new-password" type="password">
</div>
<button type="submit">create account</button>
</form>
</section>
<!-- Library JS & our JS -->
<script src="https://unpkg.com/jquery"></script>
<script src="https://unpkg.com/axios/dist/axios.js"></script>
<script src="js/models.js"></script>
<script src="js/main.js"></script>
<script src="js/nav.js"></script>
<script src="js/user.js"></script>
<script src="js/stories.js"></script>
</body>
</html>

@ -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);

@ -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;
}
}
}

@ -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();
}

@ -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 $(`
<li id="${story.storyId}">
<a href="${story.url}" target="a_blank" class="story-link">
${story.title}
</a>
<small class="story-hostname">(${hostName})</small>
<small class="story-author">by ${story.author}</small>
<small class="story-user">posted by ${story.username}</small>
</li>
`);
}
/** 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();
}

@ -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();
}
Loading…
Cancel
Save