initial commit of source code
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…</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…
Reference in New Issue