Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply sass #90

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,811 changes: 3,782 additions & 29 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server/index.js",
"local": "./node_modules/.bin/nodemon --watch server -e js server/index.js"
"local": "./node_modules/.bin/nodemon --watch server -e js server/index.js",
"sass-watch": "sass sass:public/styles --watch --poll --no-source-map"
},
"author": "Lighthouse Labs",
"license": "ISC",
Expand Down
Binary file added public/images/stardust.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 95 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,119 @@
<html lang="en">
<head>
<!-- Meta Information -->
<!-- Disable viewport zooming -->
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0' />
<title>Tweeter - Home Page</title>

<!-- External CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" type="text/css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
type="text/css"
/>

<!-- Add Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Bungee&family=Source+Sans+Pro:ital,wght@0,300;0,600;1,300;1,600&display=swap"
rel="stylesheet"
/>

<!-- Font Awesome Icon -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"
integrity="sha512-MV7K8+y+gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK+iQrJ7lzPJQd1w=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>

<!-- App CSS -->
<link rel="stylesheet" href="/styles/layout.css" type="text/css" />
<link rel="stylesheet" href="/styles/nav.css" type="text/css" />
<link rel="stylesheet" href="/styles/header.css" type="text/css" />
<link rel="stylesheet" href="/styles/new-tweet.css" type="text/css" />
<link rel="stylesheet" href="/styles/article.css" type="text/css" />

<!-- External JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"
integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>

<!-- App JS -->
<script type="text/javascript" src="/scripts/client.js"></script>
<script
type="text/javascript"
src="/scripts/compose-char-counter.js"
></script>
<script
type="text/javascript"
src="/scripts/newButtonToggleHandler.js"
></script>
<script
type="text/javascript"
src="/scripts/errorHandler.js"
></script>
<script
type="text/javascript"
src="/scripts/formToggleHandler.js"
></script>
</head>

<body>
<!-- Top nav bar (fixed) -->
<nav>
<span>tweeter</span>
<nav class="navbar-top">
<span class="logo">tweeter</span>
<button id="new-tweet">
<span><strong>Write</strong> a new tweet</span>
<span id="arrow-icon"
><i class="fa-solid fa-angles-down arrow-icon"></i
></span>
</but>
</nav>
<!-- Header with avatar and name -->
<header class="page-header">
<div>
<img src="/images/profile-hex.png" />
</div>
<br />
<div>
<h2>Esther Choi</h2>
</div>
</header>

<!-- Page-specific (main) content here -->
<main class="container"></main>
<main class="container">
<section class="new-tweet">
<form action="/tweets" method="POST">
<label for="tweet-text">Compose Tweet</label>
<textarea
name="text"
id="tweet-text"
placeholder="What are you humming about?"
></textarea>
<div class="error-container hidden">
<div class="error-modal">
<button class="close-button icon">
<i class="fa-solid fa-close"></i>
</button>
<p class="error-message"></p>
<button class="close-button main">OK</button>
</div>
</div>
<div id="tweet-text-bottom">
<button class="button new-tweet" type="submit">Tweet</button>
<output name="counter" class="counter" for="tweet-text">140</output>
</div>
</form>
</section>
<section id="tweets-container"></section>
<button id='new-tweet-bottom' class="hide"><i class='fa-solid fa-angles-up arrow-icon'></i></button>
</main>
</body>
</html>
118 changes: 114 additions & 4 deletions public/scripts/client.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,116 @@
/*
* Client-side JS logic goes here
* jQuery is already loaded
* Reminder: Use (and do all your DOM work in) jQuery's document ready function
/**
* This function calculates the year/month/day/hour/minute/seconds passed since createdTime and returns it as a string. If the time is greater than 1, add 's' to show that it is plural.
*
* @param {number} createdTime Milliseconds since epoch time the tweeter post was created.
* @return {string} A string that displays the year/month/day/hour/minute/seconds passed since createdTime.
*/
const displayTimePassed = (createdTime) => {
const msSincePost = Date.now() - createdTime;

const seconds = Math.floor(msSincePost / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(months / 12);

if (years) {
return `${years} year${years > 1 ? "s" : ""} ago`;
}
if (months) {
return `${months} month${months > 1 ? "s" : ""} ago`;
}
if (days) {
return `${days} day${days > 1 ? "s" : ""} ago`;
}
if (hours) {
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
}
if (minutes) {
return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
}
if (seconds) {
return `${seconds} second${seconds > 1 ? "s" : ""} ago`;
}
};

/**
* This function takes in an array of tweet data objects and calls the createTweetElement function
* for each tweet data in the array. Then it appends the return value from the createTweetElement function
* to the #tweets-container section.
*
* @param {array} tweetObjects Array of objects that contain tweet data.
* @return {undefined} This function does not return any value.
*/
const renderTweets = (tweets) => {
$("#tweets-container").empty();
for (const tweet of tweets) {
const $tweet = createTweetElement(tweet);

$("#tweets-container").append($tweet);
}
};

/**
* This function takes one tweet data object and formats each value in the object in HTML format.
* The HTML format is the indiviaul article element that displays the username, avatar, handle,
* tweet content, and the time passed since user uploaded the tweet.
*
* @param {object} data A single tweet data from the array of tweet data that contains
* user information, content, and created time.
* @return {string} A HTML template literal that renders tweet data inside the article element.
*/
const createTweetElement = (data) => {
const { user, content, created_at } = data;

let $tweet = $('<article class="tweet"></article>');
const header = $(`<header>
<img src="${user.avatars}" alt="${user.name}'s avatar" />
<h3>${user.name}</h3>
<div class="handle">${user.handle}</div>
</header>`);
const paragraph = $("<p></p>");
const footer = $(`<footer>
<h6>${displayTimePassed(created_at)}</h6>
<div class="footer-icons">
<i class="fa-solid fa-flag"></i>
<i class="fa-solid fa-retweet"></i>
<i class="fa-solid fa-heart"></i>
</div>
</footer>`);
paragraph.text(content.text);
$tweet.append(header).append(paragraph).append(footer);

return $tweet;
};

const loadTweets = () => {
$.getJSON("/tweets/").then((tweetDataArr) => renderTweets(tweetDataArr));
};

$(function () {
const $form = $(".new-tweet").children("form");
$form.on("submit", function (event) {
event.preventDefault();
const $error = $(this).children("div.error-container");
const $errorMsg = $error.find(".error-message");
const $textarea = $(this).children("textarea");
const $data = $textarea.serialize();

if (!$textarea.val().trim()) {
$errorMsg.html("You cannot upload a blank tweet.");
return $error.removeClass("hidden");
}
if ($textarea.val().length > 140) {
$errorMsg.html("Exceeded the maximum character limit of 140.");
return $error.removeClass("hidden");
}

$textarea.val("");
const $counter = $(this).children("#tweet-text-bottom").children("output");
$counter.val(140);
$.post("/tweets/", $data).then(loadTweets);
});

loadTweets();
});
19 changes: 19 additions & 0 deletions public/scripts/compose-char-counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
$(function () {
let counter = 140;
$("#tweet-text").on("keyup", function () {
counter = 140 - $(this).val().replace(/\s/g, "").length;

// Although using a unqiue ID would be my personal choice,
// the assignment required us to use a combination of selectors
const $counter = $(this)
.siblings("#tweet-text-bottom")
.children(".counter");
$counter.html(counter);
if (counter < 0) {
$counter.addClass("error");
}
if (counter >= 0) {
$counter.removeClass("error");
}
});
});
32 changes: 32 additions & 0 deletions public/scripts/errorHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
$(() => {
const $closeButton = $(".new-tweet").find(".close-button");
$closeButton.on("click", function (event) {
event.preventDefault();
const $error = $(this).closest("div.error-container");
$error.addClass("hidden");
const $textarea = $(this)
.closest("div.error-container")
.siblings("textarea");
$textarea.val("");
const $counter = $(this)
.closest(".error-container")
.next("#tweet-text-bottom")
.children("output");
$counter.val(140);
$counter.removeClass("error");
});

const $modalBackdrop = $(".new-tweet").find("div.error-container");
$modalBackdrop.on("click", function (event) {
if (event.target === this) {
event.preventDefault();
const $error = $(this).closest("div.error-container");
$error.addClass("hidden");
const $textarea = $(this).siblings("textarea");
$textarea.val("");
const $counter = $(this).next("#tweet-text-bottom").children("output");
$counter.val(140);
$counter.removeClass("error");
}
});
});
16 changes: 16 additions & 0 deletions public/scripts/formToggleHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const toggleForm = () => {
const $form = $("main").children(".new-tweet").children("form");
if ($form.is(":hidden")) {
$form.slideDown("slow");
} else {
$form.slideUp("slow");
}
};

$(() => {
const $newTweetButton = $("button#new-tweet");
$newTweetButton.on("click", toggleForm);

const $newTweetButtonBottom = $("#new-tweet-bottom");
$newTweetButtonBottom.on("click", toggleForm);
});
14 changes: 14 additions & 0 deletions public/scripts/newButtonToggleHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
$(() => {
$(window).on("scroll", function () {
const $newTweetButton = $("#new-tweet");
const $newTweetButtonBottom = $("#new-tweet-bottom");

if ($(window).scrollTop() === 0) {
$newTweetButtonBottom.addClass("hide");
$newTweetButton.removeClass("hide");
} else if ($(window).scrollTop() >= 120) {
$newTweetButtonBottom.removeClass("hide");
$newTweetButton.addClass("hide");
}
});
});
56 changes: 56 additions & 0 deletions public/styles/article.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#tweets-container {
padding: 26px 0;
}

article.tweet {
background-color: #ffcb9a;
color: #2c3531;
border: 4px solid #2c3531;
margin: 1em 0;
padding: 1em;
}
article.tweet:hover {
box-shadow: 5px 5px rgba(44, 53, 49, 0.5647058824);
}
article.tweet header {
display: flex;
align-items: center;
padding: 0 10px;
}
article.tweet header h3 {
margin-left: 10px;
font-weight: 300;
}

.handle {
margin-left: auto;
font-weight: 600;
color: #49b5b7;
}

article.tweet p {
padding: 10px;
overflow-wrap: break-word;
}

article.tweet footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 10px;
border-top: 2px solid #2c3531;
}

article.tweet footer h6 {
margin: 0;
padding-left: 1.5em;
}

article.tweet footer i {
margin: 5px;
cursor: pointer;
}

article.tweet footer i:hover {
color: #49b5b7;
}
Loading