diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..25c8fdba --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 95e87fdf..0642631e 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,42 @@ -`#html` `#css` `#js` `#dom` `#JSON` `#HTTP` `#API` `#Bootstrap` `#master-in-software-development` +# News Blog with API +Our blog adopts a design inspired by well-known newspapers and news websites, such as The Guardian, The Washington Post, The New York Times. Our goal for the design focused on simplicity and functionality for the user. -# Blog with API +It was important for us to be able to access different endpoints for the {JSON}Placeholder API. Although we did not incorporate POST and DELETE functions for the blog posts, the code is scalable and this implementation is planned for future versions. -

- Version -

+![img of our blog header](./assets/header_img.png) -> In this pill you will put into practice the knowledge learned about making HTTP requests to create a blog consuming the information from a third-party API. You will also learn how to use Bootstrap Framework for the layout. -## Index +## Planning -- [Requirements](#requirements) -- [Repository](#repository) -- [Technologies used](#technologies-used) -- [Project delivery](#project-delivery) -- [Resources](#resources) +We began the project by outlining the project, using tools like Trello for workflow, as well as Figma and Miró for basic design plans. -## Requirements - -- You must use semantic HTML5 elements for all the contents of the application -- You must use JSON server library to create your own local repository -- You must use fecth to do the requests -- You have to use Bootstrap Framework for the Layout and the styles - - -## Repository +Day 1 of the project was dedicated toward planning the functionality and design of the app. -First of all you must fork this project into your GitHub account. +Day 2 began by setting up a local JSON server for testing, then moving on to basic HTML structure and Javascript functionality. -To create a fork on GitHub is as easy as clicking the “fork” button on the repository page. +On Day 3, we focused on accessing the JSON data and displaying successfully on the main page, testing several methods, finally settling on *fetch*, using a mix of standard and async functions. -Fork on GitHub +On Day 4, the design and visual aspect was improved by further integrating Bootstrap 5 classes into the HTML and Javascript. Then time was spent refactoring main functions. -## Technologies used -\* HTML +![img of planned functions with miro number 1](./assets/miro_img01.png) -\* CSS +![img of planned functions with miro number 2](./assets/miro_img02.png) -\* JS +![img of planned functions with miro number 3](./assets/miro_img03.png) -\* Bootstrap - -\* HTTP Requests - -\* JSON - -\* API +## Requirements -## Project delivery +-Use {JSON}Placeholder API with endpoints for users, comments, and posts. +-Use fetch method for HTTP requests. +-Use Bootstrap to add style to the website. -To deliver this project you must follow the steps indicated in the document: +## Attribution -- [Submitting a solution](https://www.notion.so/Submitting-a-solution-524dab1a71dd4b96903f26385e24cdb6) +Illustration on main page from Undraw (https://undraw.co/illustrations) +Icon on header by Icons8 (https://icons8.com) -## Resources +## The Team -- [JSON server](https://github.com/typicode/json-server) -- [Official Bootstrap](https://getbootstrap.com/) \ No newline at end of file +Alejandro Gaerste Steger (https://github.com/Gaerste/) +Blake Johnson (https://github.com/blakejohns5) \ No newline at end of file diff --git a/assets/header_img.png b/assets/header_img.png new file mode 100644 index 00000000..06fca57b Binary files /dev/null and b/assets/header_img.png differ diff --git a/assets/icons8-bugle-50.png b/assets/icons8-bugle-50.png new file mode 100644 index 00000000..c34bfb78 Binary files /dev/null and b/assets/icons8-bugle-50.png differ diff --git a/assets/miro_img01.png b/assets/miro_img01.png new file mode 100644 index 00000000..e711c20f Binary files /dev/null and b/assets/miro_img01.png differ diff --git a/assets/miro_img02.png b/assets/miro_img02.png new file mode 100644 index 00000000..c4f261cc Binary files /dev/null and b/assets/miro_img02.png differ diff --git a/assets/miro_img03.png b/assets/miro_img03.png new file mode 100644 index 00000000..55217488 Binary files /dev/null and b/assets/miro_img03.png differ diff --git a/assets/undraw_news_re_6uub_tall_fit.svg b/assets/undraw_news_re_6uub_tall_fit.svg new file mode 100644 index 00000000..4c1f29ca --- /dev/null +++ b/assets/undraw_news_re_6uub_tall_fit.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 00000000..e437cb04 --- /dev/null +++ b/index.html @@ -0,0 +1,70 @@ + + + + + + + + + + The Dispatch + + + + + +
+
+ +
+ + + + + +
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 00000000..e9d17b0d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "blog-with-api", + "version": "1.0.0", + "description": "`#html` `#css` `#js` `#dom` `#JSON` `#HTTP` `#API` `#Bootstrap` `#master-in-software-development`", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "server": "json-server --watch ./src/data/db.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Gaerste/blog-with-api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Gaerste/blog-with-api/issues" + }, + "homepage": "https://github.com/Gaerste/blog-with-api#readme", + "dependencies": { + "json-server": "^0.17.0" + } +} diff --git a/src/css/style.css b/src/css/style.css new file mode 100644 index 00000000..5fe13363 --- /dev/null +++ b/src/css/style.css @@ -0,0 +1,72 @@ +@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;0,6..72,800;1,6..72,300;1,6..72,400;1,6..72,500;1,6..72,600;1,6..72,700;1,6..72,800&display=swap'); +/* fonts */ +@font-face { + font-family: "old london"; + src: url("/src/fonts/old_london/OldLondon.ttf"); +} +/* header src */ +.src__header{ + transition: all 0.85s cubic-bezier(0.68, -0.55, 0.265, 1.55); + content: ''; + width: 50%; + height: 100%; + background: black; + height: 4vh; +} +/* btn src */ +.btn{ + transition: all 0.85s cubic-bezier(0.68, -0.55, 0.265, 1.55); + content: ''; + width: 50%; + height: 100%; + background: black; + height: 4vh; + width: 6vw; + text-align: center; +} +/* main title */ +.blog__header--title{ + text-decoration: none; + margin: 0 2rem; + font-family: 'old london'; + font-size: 3.5rem; +} +/* main background-image */ +.main__content{ + background-image: url("/assets/undraw_news_re_6uub_tall_fit.svg"); + background-attachment: fixed; + background-repeat: no-repeat; + background-size: 53vh; + background-position: left; + overflow: hidden; + background-color: var(--bg-custom); +} +.blog__header{ + background-color: var(--bg-header-custom); +} +/* color of the website */ +:root { + --bg-custom: #f2ddc3; + --bg-header-custom: #ebba7e; +} + +/* nav logo */ +.navbar__logo-img { + transform: rotate(-45deg); + margin: 0 2rem; +} + +/* post style */ +.post__container { + border-bottom: solid .5rem var(--bg-header-custom); + border-radius: 1rem 1rem 0 0; + font-family: 'Newsreader', serif; +} +/* modal font */ +.modal { + font-family: 'Newsreader', serif; +} +/* color modal */ +.modal__dialog { + background-color: hsla(0, 0%, 100%, .25); +} diff --git a/data/comments.json b/src/data/comments.json similarity index 100% rename from data/comments.json rename to src/data/comments.json diff --git a/data/db.json b/src/data/db.json similarity index 100% rename from data/db.json rename to src/data/db.json diff --git a/data/posts.json b/src/data/posts.json similarity index 100% rename from data/posts.json rename to src/data/posts.json diff --git a/data/users.json b/src/data/users.json similarity index 100% rename from data/users.json rename to src/data/users.json diff --git a/src/fonts/old_london/OldLondon.ttf b/src/fonts/old_london/OldLondon.ttf new file mode 100644 index 00000000..f24a3401 Binary files /dev/null and b/src/fonts/old_london/OldLondon.ttf differ diff --git a/src/fonts/old_london/OldLondonAlternate.ttf b/src/fonts/old_london/OldLondonAlternate.ttf new file mode 100644 index 00000000..6010ffdd Binary files /dev/null and b/src/fonts/old_london/OldLondonAlternate.ttf differ diff --git a/src/fonts/old_london/Olondon_.otf b/src/fonts/old_london/Olondon_.otf new file mode 100644 index 00000000..9a68b206 Binary files /dev/null and b/src/fonts/old_london/Olondon_.otf differ diff --git a/src/fonts/old_london/Olondona.otf b/src/fonts/old_london/Olondona.otf new file mode 100644 index 00000000..56161c85 Binary files /dev/null and b/src/fonts/old_london/Olondona.otf differ diff --git a/src/js/info-modal.js b/src/js/info-modal.js new file mode 100644 index 00000000..0dc0cfc3 --- /dev/null +++ b/src/js/info-modal.js @@ -0,0 +1,96 @@ +import { getComments } from "./main.js"; + +//modal display in the main content +function openPost() { + const modalOpen = new bootstrap.Modal(document.getElementById("modal")); + const commentsBtn = document.getElementById("commentsContentBtn"); + modalOpen.show(); + removeComments(); + commentsBtn.addEventListener("click", loadComments); +} + +//show the content of body and title +function showTitleBody(event) { + const postId = event.target.getAttribute("data-post-id"); + const modalTitle = document.getElementById("modalTitle"); + const modalBody = document.getElementById("modalText"); + const postData = fetch("https://jsonplaceholder.typicode.com/posts/"); + try { + postData + .then((response) => { + return response.json(); + }) + .then((data) => { + let obj = data.find((item) => item.id == postId); // whe could make typeof!! + modalTitle.textContent = obj.title; + modalBody.textContent = obj.body; + }); + } catch (error) { + alert("Error Data"); + } + setCommentsId(postId); +} + +// Show the email and user in post modal +function showUserEmail(event) { + const userId = event.target.getAttribute("data-user-id"); + const modalUser = document.getElementById("modalUsername"); + const modalEmail = document.getElementById("modalEmail"); + const userData = fetch("https://jsonplaceholder.typicode.com/users"); + try { + userData + .then((response) => { + return response.json(); + }) + .then((data) => { + let obj = data.find((item) => item.id == userId); // whe could make typeof!! + modalUser.textContent = obj.username; + modalEmail.textContent = obj.email; + }); + } catch (error) { + alert("Error Data"); + } +} +// Gives comments section a data attribute, to match with the id from the comments json data. +function setCommentsId(postId) { + const commentsContent = document.getElementById("commentsContentBody"); + commentsContent.setAttribute("data-post-id", `${postId}`); +} + +// Removes comments before displaying more comments +function removeComments() { + const commentsBody = document.getElementById("commentsContentBody"); + while (commentsBody.firstElementChild) { + commentsBody.removeChild(commentsBody.lastElementChild); + } +} + +// Loads comments into the bottom of the modal, adds comments formatting +async function loadComments() { + const commentsContent = document.getElementById("commentsContentBody"); + const commentsTitle = document.createElement("h4"); + commentsTitle.textContent = "Comments:"; + commentsTitle.classList.add("fw-bold"); + + const modalPostId = parseInt(commentsContent.getAttribute("data-post-id")); // read the attribute for which comments + const commentsData = await getComments(); + const commentsSet = commentsData.filter((comment) => { + return comment.postId == modalPostId; + }); + commentsContent.append(commentsTitle); + commentsSet.map((comment) => { + const commentName = document.createElement("h5"); + const email = document.createElement("p"); + const body = document.createElement("p"); + commentName.textContent = comment.name; + + email.textContent = comment.email; + body.textContent = comment.body; + body.classList.add("border-bottom", "pb-3"); + + commentsContent.append(commentName, email, body); + }); +} + +//EXPORT +export { openPost, showTitleBody, showUserEmail }; diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 00000000..662f6568 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,85 @@ +import { openPost, showTitleBody, showUserEmail } from "./info-modal.js"; +import { getSearchResults, displaySearchResults } from "./search.js"; + +// Variable and listener for searchbar functions +const searchBtn = document.getElementById('headerSearchBtn'); +searchBtn.addEventListener('click', function () { + getSearchResults() + displaySearchResults(); +}); + +// Fetch posts from api for posts, return as .json data, and pass to displayPosts function. +async function getPostData() { + try { + const response = await fetch("https://jsonplaceholder.typicode.com/posts/"); + const postData = response.json(); + return postData; + } catch(error) { + alert('Error Data'); + } +} + +// Call function to define data for window onload +async function manageData () { + const data = await getPostData(); + displayPosts(data) +} + +window.onload = manageData; + +// Get Comments from json server +async function getComments () { + try { + const response = await fetch('https://jsonplaceholder.typicode.com/comments'); + const commentsData = await response.json(); + return commentsData; + } catch(error) { + alert('Error Data'); + } +} + +// Shows the blog posts with title and body on the main page +function displayPosts(data) { + const dataContainer = document.getElementById("postDisplay"); + + data.map((post) => { + const postContainer = document.createElement("div"); + const blogTitle = document.createElement("h4"); + const blogPost = document.createElement("p"); + + postContainer.classList.add( + "post__container", + "shadow-sm", + "mx-1", + "col-sm-12", + "col-md-6", + "col-xxl-3", + "p-3", + "mb-5", + "bg-body", + "container-xxl" + ); + postContainer.setAttribute("data-post-id", `${post.id}`); + postContainer.setAttribute("data-user-id", `${post.userId}`); + + blogTitle.classList.add("post__title",); + blogTitle.setAttribute("data-post-id", `${post.id}`); + blogTitle.setAttribute("data-user-id", `${post.userId}`); + + blogPost.classList.add("post__blog--post", ); + blogPost.setAttribute("data-post-id", `${post.id}`); + blogPost.setAttribute("data-user-id", `${post.userId}`); + blogTitle.textContent = post.title; + blogPost.textContent = post.body; + + postContainer.append(blogTitle, blogPost); + dataContainer.append(postContainer); + postContainer.addEventListener("click", (e) => { + showTitleBody(e); + openPost(); + showUserEmail(e); + }); + }); +} + +export { getPostData, getComments, displayPosts }; \ No newline at end of file diff --git a/src/js/search.js b/src/js/search.js new file mode 100644 index 00000000..0052306b --- /dev/null +++ b/src/js/search.js @@ -0,0 +1,32 @@ +import { getPostData, displayPosts } from './main.js' + +// Define string for search results from user entry in search bar +// Filter posts from json that have a title which includes search string and reurn results +async function getSearchResults () { + const searchString = document.getElementById('headerSearch').value; + const posts = await getPostData(); + const searchResults = posts.filter((post) => { + return post.title.includes(searchString); + }) + return searchResults; + } + +// Takes posts from the search results, if they exist, removes currently viewed posts and passes search results to display posts. +async function displaySearchResults () { + const searchResults = await getSearchResults(); + if (searchResults.length > 0) { + removePosts(); + displayPosts(searchResults); + } +} + +// Remove currently viewed posts on main page. +function removePosts () { + const dataContainer = document.getElementById("postDisplay"); + while (dataContainer.firstElementChild) { + dataContainer.removeChild(dataContainer.lastElementChild); + } +} + + + export { getSearchResults, displaySearchResults, removePosts} \ No newline at end of file