From d2315e1293df57eea57a2e982d542248e0c30342 Mon Sep 17 00:00:00 2001 From: Malkenes <122474300+Malkenes@users.noreply.github.com> Date: Tue, 6 Feb 2024 06:06:44 +0100 Subject: [PATCH 01/10] Authentication (#2) * implemented authentication to signin/register * added documentation * relocated function, added documentation --- index.html | 28 +++++-- js/index.js | 10 +++ js/services/apiServices.mjs | 46 +++++++++++ js/services/authService.mjs | 149 ++++++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 js/index.js create mode 100644 js/services/apiServices.mjs create mode 100644 js/services/authService.mjs diff --git a/index.html b/index.html index 5cc93a525..647836c90 100644 --- a/index.html +++ b/index.html @@ -31,14 +31,16 @@

FriendLink

-
+
- + +
Email needs to be @noroff.no or @stud.noroff.no
- + +
Password needs to be atleast 8 characters
@@ -46,18 +48,26 @@

FriendLink

-
+ +
+ + +
Username can not contain punctuation symbols apart from (_)
+
- + +
Email needs to be @noroff.no or @stud.noroff.no
- + +
Password needs to be atleast 8 characters
- + +
Password does not match
@@ -65,6 +75,9 @@

FriendLink

+
+
Something went wrong!
+
@@ -73,5 +86,6 @@

FriendLink

+ \ No newline at end of file diff --git a/js/index.js b/js/index.js new file mode 100644 index 000000000..034442b61 --- /dev/null +++ b/js/index.js @@ -0,0 +1,10 @@ +import { initializeFormValidation } from "./services/authService.mjs"; +import { apiCall } from "./services/apiServices.mjs"; +if (localStorage["accessToken"]) { + apiCall("/social/profiles"); +} + +const forms = document.querySelectorAll(".needs-validation"); +initializeFormValidation(forms); + + diff --git a/js/services/apiServices.mjs b/js/services/apiServices.mjs new file mode 100644 index 000000000..47ae76169 --- /dev/null +++ b/js/services/apiServices.mjs @@ -0,0 +1,46 @@ +const NOROFF_API_URL = "https://v2.api.noroff.dev"; +const accessToken = localStorage.getItem("accessToken"); +/** + *Requests and retrieves an API key for a user with the provided authentication token. + * @param {string} token + * @returns {Promise} + */ +async function getApiKey(token) { + try { + const response = await fetch(`${NOROFF_API_URL}/auth/create-api-key`, { + method: "POST", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: "API KEY" + }) + }) + + const result = await response.json(); + return result.data.key; + } catch (error) { + console.log(error); + } +} +/** + *Makes an authenticated API call to a specified endpoint using the provided access token. + * @param {string} endpoint + */ +export async function apiCall(endpoint) { + const apiKey = await getApiKey(accessToken); + const options = { + headers: { + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + } + } + try { + const response = await fetch(`${NOROFF_API_URL}${endpoint}`, options) + const result = await response.json(); + console.log(result); + } catch (error) { + + } +} \ No newline at end of file diff --git a/js/services/authService.mjs b/js/services/authService.mjs new file mode 100644 index 000000000..883640c99 --- /dev/null +++ b/js/services/authService.mjs @@ -0,0 +1,149 @@ +const NOROFF_API_URL = "https://v2.api.noroff.dev"; + +/** + *Initializes form validation and submission logic for a collection of forms. + * @param {NodeList} forms + */ +export function initializeFormValidation(forms) { + Array.from(forms).forEach(form => { + const inputs = form.querySelectorAll("input"); + Array.from(inputs).forEach(input => { + input.addEventListener("focusout", () => { + if (input.value.length > 0) { + if(inputValidation(input)) { + input.classList.add("is-valid"); + } else { + input.classList.add("is-invalid"); + } + }; + }); + input.addEventListener("focusin", () => { + input.classList.remove("is-valid", "is-invalid"); + }) + }) + form.addEventListener("submit", event => { + event.preventDefault(); + + loginFormValidation(form); + }) + }) +} +/** + * Handles user authentication by sending a POST request to the specified API endpoint + * @param {object} data + * @param {string} data.email + * @param {string} data.password + */ +async function signIn(data) { + try { + const response = await fetch(`${NOROFF_API_URL}/auth/login`, { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + const authError = document.querySelector("#auth-error"); + authError.style.display = "block"; + if (response.status === 401) { + authError.textContent = "Incorrect Password"; + } + } else { + window.location.href = "/profile/index.html"; + } + + const result = await response.json(); + localStorage.setItem("accessToken", result.data.accessToken); + + } catch (error) { + console.log(error); + } +} + +/** + *Registers a new user by sending a POST request to the specified API endpoint. + * @param {Object} data + * @param {string} data.name + * @param {string} data.email + * @param {string} data.password + */ +async function registerUser(data) { + try { + const response = await fetch(`${NOROFF_API_URL}/auth/register`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(data) + }); + if (!response.ok) { + const authError = document.querySelector("#auth-error"); + authError.style.display = "block"; + } else { + const signInData = (({email, password}) => ({email, password}))(data); + signIn(signInData); + } + } catch (error) { + console.log(error); + } +} +/** + *Validates the input fields in a given form and performs user authentication or registration based on the form data. + * @param {HTMLFormElement} form + */ +function loginFormValidation(form) { + const dataObject = {} + const inputs = form.querySelectorAll("input"); + + Array.from(inputs).forEach(input => { + if(inputValidation(input)) { + const key = input.name; + dataObject[key] = input.value; + } else { + input.classList.add("is-invalid"); + } + }) + if ("email" in dataObject && "password" in dataObject) { + if ("name" in dataObject) { + registerUser(dataObject); + } else { + signIn(dataObject); + } + } +} +/** + *Validates the value of an input element based on its type. + * @param {HTMLInputElement} input + * @returns {boolean} + */ +function inputValidation(input) { + if (input.type === "text") { + return (userNameValidation(input.value)); + } + + if (input.type === "email") { + return (emailValidation(input.value)) + } + + if (input.type === "password") { + if (input.value.length >= 8) { + if (input.id ==="registerPasswordRepeat") { + const pass = document.querySelector("#registerPassword"); + return (pass.value === input.value); + } else { + return true; + } + } + } +} + +let emailValidation = (email) => { + const regEx = /\S+@(?:stud\.)?noroff\.no$/; + return regEx.test(email); +} +let userNameValidation = (username) => { + const regEx = /^[a-zA-Z0-9_]+$/; + return regEx.test(username); +} \ No newline at end of file From f7e319047e99a6cc35c72d882810ce39a9cdf239 Mon Sep 17 00:00:00 2001 From: Malkenes <122474300+Malkenes@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:24:22 +0100 Subject: [PATCH 02/10] Render content (#3) * added functionality to display posts in feed * added functionality for posting content * added ability to interact with posts --- feed/index.html | 89 +++++++++-- js/components/commentSection.mjs | 79 ++++++++++ js/components/postList.mjs | 99 ++++++++++++ js/index.js | 258 ++++++++++++++++++++++++++++++- js/pages/feed.mjs | 28 ++++ js/services/apiServices.mjs | 48 +++++- js/services/authService.mjs | 1 + js/utils/timeUtils.mjs | 27 ++++ src/scss/_variables.scss | 3 +- 9 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 js/components/commentSection.mjs create mode 100644 js/components/postList.mjs create mode 100644 js/pages/feed.mjs create mode 100644 js/utils/timeUtils.mjs diff --git a/feed/index.html b/feed/index.html index 758035963..759d2f9af 100644 --- a/feed/index.html +++ b/feed/index.html @@ -25,7 +25,7 @@ - profile +
@@ -56,13 +56,48 @@

Filters

-
-
- -
- +
+ +
+ +
+ + + +
+
+ +
+
+ +
+ + +
+ +
+ + +
+
+
+ + + +
+
+
+ +
+
- +
-
+
+
+ + \ No newline at end of file diff --git a/js/components/commentSection.mjs b/js/components/commentSection.mjs new file mode 100644 index 000000000..8e16db526 --- /dev/null +++ b/js/components/commentSection.mjs @@ -0,0 +1,79 @@ +import { timePassed } from "../utils/timeUtils.mjs"; + +export function commentSection(comments, reactions) { + const div = document.createElement("div"); + div.className = "comment-container"; + const reactionElement = createReactElements(reactions); + const commentElements = createComments(comments); + div.append(reactionElement); + div.append(commentElements); + return div; +} +function createReactElements(reactions) { + const div = document.createElement("div"); + div.classList.add("bg-primary-subtle"); + //console.log(data.reactions); + let hearth = 0; + let smile = 0; + let frown = 0; + reactions.filter((reaction) => { + if (reaction.symbol === "💗") { + hearth = reaction.count; + } + if (reaction.symbol === "😀") { + smile = reaction.count; + } + if (reaction.symbol === "🙁") { + frown = reaction.count; + } + }); + div.innerHTML = ` + + + + + `; + return div; +} + +function createComments(comments) { + const div = document.createElement("div"); + div.classList.add("bg-body", "p-3"); + div.innerHTML = ` +
+

Comments (${comments.length})

+
+
+
+
+ +
+ +
+
+
+ `; + Array.from(comments).forEach(comment => { + const com = document.createElement("div"); + com.dataset.commentId = comment.id; + com.innerHTML = ` +
+ ${comment.author.avatar.alt} +

${comment.author.name}

+
`; + const time = timePassed(comment.created); + com.append(time); + const body = document.createElement("div"); + body.textContent = comment.body; + if (comment.author.name === localStorage["name"]) { + const deleteBtn = document.createElement("button"); + deleteBtn.classList.add("btn", "btn-danger", "btn-sm", "delete-comment-btn"); + deleteBtn.textContent = "delete"; + com.append(deleteBtn); + } + com.append(body); + div.append(com); + }); + return div; +} + diff --git a/js/components/postList.mjs b/js/components/postList.mjs new file mode 100644 index 000000000..8b04c8478 --- /dev/null +++ b/js/components/postList.mjs @@ -0,0 +1,99 @@ +import { timePassed } from "../utils/timeUtils.mjs"; +import { showAll } from "../pages/feed.mjs"; + + +export function displayPost(data) { + const post = document.createElement("div"); + const header = createPostHeader(data); + const body = createPostBody(data); + post.append(header); + post.append(body); + return post; +} + +function createPostHeader(data) { + const header = document.createElement("div"); + header.classList.add("border-bottom"); + header.innerHTML = ` +
+ ${data.author.avatar.alt} +

${data.author.name}

+
`; + + const time = timePassed(data.created); + header.append(time); + if (data.author.name === localStorage["name"]) { + const div = document.createElement("div"); + div.classList.add("btn-group"); + const editBtn = document.createElement("button"); + const deleteBtn = document.createElement("button"); + editBtn.classList.add("btn", "btn-light", "btn-sm", "edit-btn"); + deleteBtn.classList.add("btn", "btn-danger", "btn-sm", "delete-btn"); + editBtn.textContent = "Edit"; + deleteBtn.textContent = "delete"; + div.append(editBtn, deleteBtn); + //edit.onclick = function() { + // editPost(data); + //} + //edit.textContent = "edit"; + //edit.dataset.id = data.id; + header.append(div); + } + return header; +} + +function createPostBody(data) { + const body = document.createElement("div"); + body.innerHTML = ` +

${data.title}

`; + + const div = document.createElement("div"); + const buttonContainer = document.createElement("div"); + div.innerHTML = data.body; + div.classList.add("content"); + div.style.maxHeight = "200px"; + div.style.overflow = "hidden"; + const button = document.createElement("button"); + button.style.display = "none"; + button.classList.add("show-more", "btn"); + button.textContent = "show more"; + button.onclick = function () { + showAll(div, button); + }; + buttonContainer.append(button); + body.append(div); + body.append(button); + if (data.media) { + const image = createMediaElement(data.media); + body.append(image); + } + if (Array.isArray(data.tags) && data.tags.length > 0) { + const tags = createTagsElement(data.tags); + body.append(tags); + } + return body; +} + +function createMediaElement(media) { + const div = document.createElement("div"); + div.classList.add("text-center", "mb-3"); + const image = document.createElement("img"); + image.classList.add("img-fluid"); + image.src = media.url; + image.alt = media.alt; + div.append(image); + return div; +} + +function createTagsElement(tags) { + const div = document.createElement("div"); + div.classList.add("d-flex", "gap-2", "mb-3"); + tags.forEach(tag => { + const container = document.createElement("div"); + container.classList.add("bg-secondary-subtle", "p-1"); + container.innerText = tag; + div.append(container); + }); + return div; +} + diff --git a/js/index.js b/js/index.js index 034442b61..83ae82fd8 100644 --- a/js/index.js +++ b/js/index.js @@ -1,10 +1,264 @@ import { initializeFormValidation } from "./services/authService.mjs"; -import { apiCall } from "./services/apiServices.mjs"; +import { apiCall, deleteApiData, postApiData, putApiData } from "./services/apiServices.mjs"; +import { displayFeed } from "./pages/feed.mjs"; if (localStorage["accessToken"]) { - apiCall("/social/profiles"); + const apiName = await apiCall("/social/profiles/" + localStorage["name"]); + const user = document.querySelectorAll(".user-profile"); + Array.from(user).forEach(img => { + img.src = apiName.data.avatar.url; + img.alt = apiName.data.avatar.alt; + }) + + const feed = document.querySelector("#feed"); + if (feed) { + let page = 1; + fetchFeed(page); + window.onscroll = function() { + if (window.innerHeight + window.scrollY >= document.body.scrollHeight) { + page += 1; + fetchFeed(page); + } + }; + } + feed.addEventListener("click", function(e) { + const postContainer = e.target.closest("[data-id]"); + const postId = postContainer.dataset.id; + if (e.target.classList.contains("react-btn")) { + const emoji = e.target.childNodes[0].nodeValue.trim(); + putApiData(`/social/posts/${postId}/react/${emoji}`); + } + if (e.target.classList.contains("toggle-comment-btn")) { + const commentContainer = e.target.closest(".comment-container"); + const collapseElement = commentContainer.querySelector(".collapse"); + collapseElement.classList.toggle("show"); + } + if (e.target.classList.contains("delete-btn")) { + deleteApiData(`/social/posts/${postId}`); + } + if (e.target.classList.contains("edit-btn")) { + editPost(postId); + } + if (e.target.classList.contains("comment-btn")) { + e.preventDefault(); + const textarea = e.target.closest(".comment-form").querySelector("textarea"); + const body = textarea.value; + postApiData(`/social/posts/${postId}/comment`,{body: body}); + } + if (e.target.classList.contains("delete-comment-btn")) { + const comment = e.target.closest("[data-comment-id]"); + const commentId = comment.dataset.commentId; + deleteApiData(`/social/posts/${postId}/comment/${commentId}`); + } + }) } +async function fetchFeed(page) { + const apiData = await apiCall("/social/posts?limit=10&_author=true&_reactions=true&_comments=true&page=" + page); + console.log(apiData); + displayFeed(apiData); + //const buass = document.querySelector(".edit-btn"); + //console.log(buass.parentNode.parentNode.parentNode); +} const forms = document.querySelectorAll(".needs-validation"); initializeFormValidation(forms); +const createPost = document.querySelector("#create-post"); +const postForm = document.querySelector("#post-form"); +const closePostForm = document.querySelector("#close-postform"); +const editForm = document.querySelector("#edit-form"); +editForm.addEventListener("submit", event => { + event.preventDefault(); + const dataPackage = {} + + const postId = document.querySelector("#myModal").dataset.id; + const title = document.querySelector("#edit-title-input").value; + const body = document.querySelector("#edit-body-input").value; + const mediaUrl = document.querySelector("#edit-media-url-input").value; + const mediaAlt = document.querySelector("#edit-media-alt-input").value; + const tags = document.querySelector("#edit-added-tags"); + const currentTags = tags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + if (title) { + dataPackage.title = title; + } + if (body) { + dataPackage.body = body; + } + if (mediaUrl) { + dataPackage.media = {} + dataPackage.media.url = mediaUrl; + if (mediaAlt) { + dataPackage.media.alt = mediaAlt; + } + } + if (tagsByName) { + dataPackage.tags = tagsByName; + } + putApiData(`/social/posts/${postId}`, dataPackage); + console.log(dataPackage); + console.log(postId); +}) + +createPost.addEventListener("click", () => { + postForm.classList.remove("d-none"); + createPost.classList.add("d-none"); +}) + +closePostForm.addEventListener("click" , () => { + postForm.classList.add("d-none"); + createPost.classList.remove("d-none"); +}) + +postForm.addEventListener("submit", event => { + event.preventDefault(); + const dataPackage = {} + const title = document.querySelector("#title-input").value; + const body = document.querySelector("#body-input").value; + const mediaUrl = document.querySelector("#media-url-input").value; + const mediaAlt = document.querySelector("#media-alt-input").value; + const tags = document.querySelector("#added-tags"); + const currentTags = tags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + if (title) { + dataPackage.title = title; + } + if (body) { + dataPackage.body = body; + } + if (mediaUrl) { + dataPackage.media = {}; + dataPackage.media.url = mediaUrl; + if (mediaAlt) { + dataPackage.media.alt = mediaAlt; + } + } + if (tagsByName) { + dataPackage.tags = tagsByName; + } + console.log(dataPackage); + postApiData("/social/posts", dataPackage); + //postForm.reset(); +}) + +let addMedia = () => { + const mediaInputs = document.querySelector(".add-media"); + const mediaIcon = document.querySelector("#media-image"); + const closeIcon = document.querySelector("#media-close"); + + if (mediaInputs.classList.contains("d-none")) { + mediaInputs.classList.remove("d-none"); + mediaIcon.classList.add("d-none"); + closeIcon.classList.remove("d-none"); + } else { + mediaInputs.classList.add("d-none"); + mediaIcon.classList.remove("d-none"); + closeIcon.classList.add("d-none"); + } +} +function addTag() { + const tagInput = document.querySelector("#tags-input"); + const tag = tagInput.value.trim().toLowerCase(); + const addedTags = document.querySelector("#added-tags"); + const currentTags = addedTags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + if (tag !== "" && !dublicateTag(tagsByName,tag)) { + const tagElement = document.createElement("span"); + tagElement.classList.add("p-1", "bg-secondary-subtle", "tag") + tagElement.textContent = tag; + tagElement.onclick = function() { + tagElement.remove(); + } + addedTags.append(tagElement); + } + tagInput.value = ""; +} + +function editTag() { + const tagInput = document.querySelector("#edit-tags-input"); + const tag = tagInput.value.trim().toLowerCase(); + const addedTags = document.querySelector("#edit-added-tags"); + const currentTags = addedTags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + if (tag !== "" && !dublicateTag(tagsByName,tag)) { + const tagElement = document.createElement("span"); + tagElement.classList.add("p-1", "bg-secondary-subtle", "tag") + tagElement.textContent = tag; + tagElement.onclick = function() { + tagElement.remove(); + } + addedTags.append(tagElement); + } + tagInput.value = ""; +} + + +let dublicateTag = (tags, tag) => { + const foundTag = tags.find((currentTag) => { + if (currentTag === tag) { + return true; + } + }) + if (foundTag) { + return true; + } +} +const addTags = document.querySelector("#add-tag"); +addTags.addEventListener("click" , addTag); + +const editTags = document.querySelector("#edit-add-tag"); +editTags.addEventListener("click" , editTag); + +const mediaToggle = document.querySelector("#media-toggle"); +mediaToggle.addEventListener("click", addMedia); +const mediaUrlTest = document.querySelector("#media-url-input"); +mediaUrlTest.addEventListener("focusout", () => { + const image = document.querySelector("#placeholder-image"); + image.src = mediaUrlTest.value; +}) + +async function editPost(id) { + const myModal = new bootstrap.Modal(document.getElementById("myModal")) + const response = await apiCall(`/social/posts/${id}`); + const data = response.data; + document.querySelector("#myModal").dataset.id = data.id; + document.querySelector("#edit-title-input").value = data.title; + document.querySelector("#edit-body-input").value = data.body; + document.querySelector("#edit-media-url-input").value = data.media.url; + document.querySelector("#edit-media-alt-input").value = data.media.alt; + const addedTags = document.querySelector("#edit-added-tags"); + const currentTags = addedTags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + + data.tags.forEach(tag => { + if (!dublicateTag(tagsByName, tag)) { + const tagElement = document.createElement("span"); + tagElement.classList.add("p-1", "bg-secondary-subtle", "tag") + tagElement.textContent = tag; + tagElement.onclick = function() { + tagElement.remove(); + } + addedTags.append(tagElement); + } + }) + myModal.toggle(); +} + +/* +const testTags = document.querySelectorAll(".tag"); +if (testTags) { + Array.from(testTags).forEach(tag => { + console.log(tag); + }) +} +*/ diff --git a/js/pages/feed.mjs b/js/pages/feed.mjs new file mode 100644 index 000000000..23c43eadc --- /dev/null +++ b/js/pages/feed.mjs @@ -0,0 +1,28 @@ +import { commentSection } from "../components/commentSection.mjs"; +import { displayPost } from "../components/postList.mjs"; + +export function displayFeed(data) { + const feed = document.querySelector("#feed"); + data.data.forEach(element => { + const post = document.createElement("div"); + post.classList.add("container", "bg-white", "p-3", "mb-3"); + post.dataset.id = element.id; + + const postContent = displayPost(element); + const postComments = commentSection(element.comments, element.reactions); + post.append(postContent); + post.append(postComments); + feed.append(post); + + const bodyText = post.querySelector(".content"); + const bodyShowMore = post.querySelector(".show-more"); + if (bodyText.clientHeight < bodyText.scrollHeight) { + bodyShowMore.style.display = "block"; + } + }); +} + +export function showAll(div, btn) { + div.style.maxHeight = "none"; + btn.style.display = "none"; +} \ No newline at end of file diff --git a/js/services/apiServices.mjs b/js/services/apiServices.mjs index 47ae76169..fb6caf275 100644 --- a/js/services/apiServices.mjs +++ b/js/services/apiServices.mjs @@ -39,8 +39,54 @@ export async function apiCall(endpoint) { try { const response = await fetch(`${NOROFF_API_URL}${endpoint}`, options) const result = await response.json(); - console.log(result); + return result; } catch (error) { } +} + +export async function postApiData(endpoint, data = {}) { + const apiKey = await getApiKey(accessToken); + const options = { + method: "post", + headers: { + "Content-Type" : "application/json", + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + }, + body: JSON.stringify(data) + } + const response = await fetch (`${NOROFF_API_URL}${endpoint}`, options) + return response.json(); +} +export async function putApiData(endpoint, data = {}) { + const apiKey = await getApiKey(accessToken); + const options = { + method: "put", + headers: { + "Content-Type" : "application/json", + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + }, + body: JSON.stringify(data) + } + const response = await fetch (`${NOROFF_API_URL}${endpoint}`, options) + return response.json(); + +} + +export async function deleteApiData(endpoint, data = {}) { + const apiKey = await getApiKey(accessToken); + const options = { + method: "delete", + headers: { + "Content-Type" : "application/json", + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + }, + body: JSON.stringify(data) + } + const response = await fetch (`${NOROFF_API_URL}${endpoint}`, options) + //return response.json(); + } \ No newline at end of file diff --git a/js/services/authService.mjs b/js/services/authService.mjs index 883640c99..10a1aff86 100644 --- a/js/services/authService.mjs +++ b/js/services/authService.mjs @@ -56,6 +56,7 @@ async function signIn(data) { const result = await response.json(); localStorage.setItem("accessToken", result.data.accessToken); + localStorage.setItem("name", result.data.name); } catch (error) { console.log(error); diff --git a/js/utils/timeUtils.mjs b/js/utils/timeUtils.mjs new file mode 100644 index 000000000..dc4271c5f --- /dev/null +++ b/js/utils/timeUtils.mjs @@ -0,0 +1,27 @@ + +export function timePassed(created) { + const div = document.createElement("p"); + div.classList.add("text-black-50"); + const date = Date.parse(created); + const timeElapsed = Date.now() - date; + const days = Math.floor(timeElapsed / (1000 * 60 * 60 * 24)); + div.textContent = `Posted ${days} days ago`; + let time = ""; + switch (true) { + case timeElapsed > 1000 * 60 * 60 * 24 * 7: + time = Math.floor(timeElapsed / (1000 * 60 * 60 * 24 * 7)) + " weeks"; + break; + case timeElapsed > 1000 * 60 * 60 * 24: + time = Math.floor(timeElapsed / (1000 * 60 * 60 * 24)) + " days"; + break; + case timeElapsed > 1000 * 60 * 60: + time = Math.floor(timeElapsed / (1000 * 60 * 60)) + " hours"; + break; + case timeElapsed > 1000 * 60: + time = Math.floor(timeElapsed / (1000 * 60)) + " minutes"; + default: + break; + } + div.textContent = `Posted ${time} ago`; + return div; +} diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss index 048b225d7..e6eb2490e 100644 --- a/src/scss/_variables.scss +++ b/src/scss/_variables.scss @@ -28,4 +28,5 @@ $spacers: ( 6: $spacer * 6.5, ); $nav-link-color: black; -$btn-font-weight: 700; \ No newline at end of file +$btn-font-weight: 700; +$badge-font-size: 0.5em; \ No newline at end of file From bc838541d0535120d2bbfef0dc199f5b1c0b02b8 Mon Sep 17 00:00:00 2001 From: Malkenes <122474300+Malkenes@users.noreply.github.com> Date: Tue, 5 Mar 2024 05:27:14 +0100 Subject: [PATCH 03/10] Profiles (#4) * profiles displaying correctly * added functionality to update user profile * added logout function --- feed/index.html | 9 +- js/components/postList.mjs | 4 +- js/index.js | 55 +++++++---- js/pages/profilePage.mjs | 162 ++++++++++++++++++++++++++++++++ profile/index.html | 183 +++++++++++++++++++++++++++++++------ src/scss/index.scss | 15 ++- 6 files changed, 374 insertions(+), 54 deletions(-) create mode 100644 js/pages/profilePage.mjs diff --git a/feed/index.html b/feed/index.html index 759d2f9af..41535d721 100644 --- a/feed/index.html +++ b/feed/index.html @@ -24,9 +24,12 @@ - - profile - +
+ + + + +
diff --git a/js/components/postList.mjs b/js/components/postList.mjs index 8b04c8478..fbe31fb8e 100644 --- a/js/components/postList.mjs +++ b/js/components/postList.mjs @@ -16,7 +16,9 @@ function createPostHeader(data) { header.classList.add("border-bottom"); header.innerHTML = `
- ${data.author.avatar.alt} + + ${data.author.avatar.alt} +

${data.author.name}

`; diff --git a/js/index.js b/js/index.js index 83ae82fd8..ab31c5045 100644 --- a/js/index.js +++ b/js/index.js @@ -1,6 +1,8 @@ import { initializeFormValidation } from "./services/authService.mjs"; import { apiCall, deleteApiData, postApiData, putApiData } from "./services/apiServices.mjs"; import { displayFeed } from "./pages/feed.mjs"; +import { displayProfile } from "./pages/profilePage.mjs"; + if (localStorage["accessToken"]) { const apiName = await apiCall("/social/profiles/" + localStorage["name"]); const user = document.querySelectorAll(".user-profile"); @@ -8,7 +10,18 @@ if (localStorage["accessToken"]) { img.src = apiName.data.avatar.url; img.alt = apiName.data.avatar.alt; }) + const userLink = document.querySelectorAll(".user-profile-link"); + Array.from(userLink).forEach(link => { + link.href = "../profile/index.html?user=" + localStorage["name"]; + }) + const queryString = document.location.search; + const params = new URLSearchParams(queryString); + const userParam = params.get("user"); + if (userParam) { + const userProfile = await apiCall("/social/profiles/" + userParam + "?_following=true&_followers=true"); + displayProfile(userProfile); + } const feed = document.querySelector("#feed"); if (feed) { let page = 1; @@ -53,11 +66,16 @@ if (localStorage["accessToken"]) { } async function fetchFeed(page) { - const apiData = await apiCall("/social/posts?limit=10&_author=true&_reactions=true&_comments=true&page=" + page); + const queryString = document.location.search; + const params = new URLSearchParams(queryString); + const user = params.get("user"); + let endpoint = "/social/posts"; + if (user) { + endpoint = `/social/profiles/${user}/posts`; + } + const apiData = await apiCall(endpoint +"?limit=10&_author=true&_reactions=true&_comments=true&page=" + page); console.log(apiData); displayFeed(apiData); - //const buass = document.querySelector(".edit-btn"); - //console.log(buass.parentNode.parentNode.parentNode); } const forms = document.querySelectorAll(".needs-validation"); initializeFormValidation(forms); @@ -100,17 +118,17 @@ editForm.addEventListener("submit", event => { console.log(dataPackage); console.log(postId); }) +if (createPost) { + createPost.addEventListener("click", () => { + postForm.classList.remove("d-none"); + createPost.classList.add("d-none"); + }) -createPost.addEventListener("click", () => { - postForm.classList.remove("d-none"); - createPost.classList.add("d-none"); -}) - -closePostForm.addEventListener("click" , () => { - postForm.classList.add("d-none"); - createPost.classList.remove("d-none"); -}) - + closePostForm.addEventListener("click" , () => { + postForm.classList.add("d-none"); + createPost.classList.remove("d-none"); + }) +} postForm.addEventListener("submit", event => { event.preventDefault(); const dataPackage = {} @@ -254,11 +272,10 @@ async function editPost(id) { myModal.toggle(); } -/* -const testTags = document.querySelectorAll(".tag"); -if (testTags) { - Array.from(testTags).forEach(tag => { - console.log(tag); +const logoutBtn = document.querySelector("#logout-btn"); +if (logoutBtn) { + logoutBtn.addEventListener("click", () => { + localStorage.clear(); + window.location.href = "../index.html"; }) } -*/ diff --git a/js/pages/profilePage.mjs b/js/pages/profilePage.mjs new file mode 100644 index 000000000..cfea36ea0 --- /dev/null +++ b/js/pages/profilePage.mjs @@ -0,0 +1,162 @@ +import { putApiData } from "../services/apiServices.mjs"; + +export function displayProfile(data) { + + console.log(data); + const avatar = document.querySelector("#user-avatar"); + avatar.src = data.data.avatar.url; + avatar.alt = data.data.avatar.alt; + + const banner = document.querySelector("#banner"); + getMeta(data.data.banner.url, (err,img) => { + if (img.naturalHeight > img.naturalWidth) { + banner.style.backgroundSize = "100% auto"; + } else { + banner.style.backgroundSize = "auto 100%"; + + } + banner.style.backgroundImage = `url(${data.data.banner.url})`; + }); + + const user = document.querySelector("h1"); + user.textContent = data.data.name; + user.style.filter = "drop-shadow(4px -4px 12px white)"; + const bio = document.querySelector("#bio"); + if (data.data.bio) { + bio.textContent = data.data.bio; + } + + const followOrEdit = document.querySelector("#followOrEdit"); + if (data.data.name === localStorage["name"]) { + followOrEdit.textContent = "Edit"; + followOrEdit.onclick = function() { + editProfile(data.data.avatar, data.data.banner); + } + const postBtn = document.querySelector("#create-posts"); + postBtn.classList.remove("d-none"); + } else { + if (isFollowing(data.data.followers)) { + followOrEdit.textContent = "Unfollow"; + } + followOrEdit.addEventListener("click", function() { + if (isFollowing(data.data.followers)) { + putApiData(`/social/profiles/${data.data.name}/unfollow`); + followOrEdit.textContent = "Follow"; + } else { + putApiData(`/social/profiles/${data.data.name}/follow`); + followOrEdit.textContent = "Unfollow"; + } + }) + } + const testFollow = document.querySelectorAll(".followers"); + displayFollow(data.data.followers, testFollow); + const testFollowing = document.querySelectorAll(".following"); + displayFollow(data.data.following, testFollowing); +} + +function displayFollow(followers, containers) { + Array.from(containers).forEach(container => { + const amount = container.querySelector(".amount"); + const followLength = followers.length; + amount.textContent = followLength; + let list = displayUsers(followers); + const modal = container.querySelector(".modal-body"); + if (modal) { + list.classList.add("row"); + modal.append(list); + } else { + if (followLength >= 3) { + list = displayUsers(followers,3); + } + list.classList.add("d-none", "d-lg-flex", "row"); + container.append(list); + } + }) +} + +function isFollowing(users) { + const following = users.find((user) => { + if (user.name === localStorage["name"]) { + return true; + } + }) + return following; +} + +function displayUsers(users, amount = users.length) { + const div = document.createElement("div"); + for (let i = 0 ; i < amount ; i++) { + const user = document.createElement("a"); + user.href = "../profile/index.html?user=" + users[i].name; + user.classList.add("d-flex","flex-column","align-items-center","col-4", "overflow-hidden"); + const userImg = document.createElement("img"); + userImg.src = users[i].avatar.url; + userImg.classList.add("user-icon"); + const userName = document.createElement("h3"); + userName.classList.add("fs-5"); + userName.textContent = users[i].name; + user.append(userImg); + user.append(userName); + div.append(user); + } + return div; +} + +function editProfile(avatar, banner) { + const editModal = new bootstrap.Modal(document.getElementById("edit-modal")) + const modal = document.querySelector("#edit-modal"); + const userImg = modal.querySelector("img"); + const userBanner = modal.querySelector(".test-bg"); + userBanner.style.backgroundImage = `url(${banner.url})`; + userBanner.style.backgroundSize = "100% auto"; + userImg.src = avatar.url; + const avatarUrl = document.querySelector("#avatar-url"); + const avatarAlt = document.querySelector("#avatar-alt"); + const bannerUrl = document.querySelector("#banner-url"); + const bannerAlt = document.querySelector("#banner-alt"); + avatarUrl.value = avatar.url; + avatarAlt.value = avatar.alt; + bannerUrl.value = banner.url; + bannerAlt.value = banner.alt; + editModal.toggle(); + avatarUrl.addEventListener("input", () => { + userImg.src = avatarUrl.value; + }); + bannerUrl.addEventListener("input", () => { + getMeta(bannerUrl.value, (err,img) => { + if (img.naturalHeight > img.naturalWidth) { + userBanner.style.backgroundSize = "100% auto"; + } else { + userBanner.style.backgroundSize = "auto 100%"; + + } + //console.log(img.naturalWidth,img.naturalHeight); + }); + userBanner.style.backgroundImage = `url(${bannerUrl.value})`; + }) + + const form = document.querySelector("#edit-profile-form"); + form.addEventListener("submit", event => { + event.preventDefault(); + const dataPackage = {} + if (bannerUrl) { + dataPackage.banner = {}; + dataPackage.banner.url = bannerUrl.value; + dataPackage.banner.alt = bannerAlt.value; + } + if (avatarUrl) { + dataPackage.avatar = {}; + dataPackage.avatar.url = avatarUrl.value; + dataPackage.avatar.alt = avatarAlt.value; + } + putApiData("/social/profiles/" + localStorage["name"], dataPackage); + editModal.hide() + }) +} + +function getMeta(url,cb) { + const img = new Image(); + img.onload = () => cb(null, img); + img.onerror = (err) => cb(err); + img.src = url; +} \ No newline at end of file diff --git a/profile/index.html b/profile/index.html index c5e05acb8..e4baab6ec 100644 --- a/profile/index.html +++ b/profile/index.html @@ -24,9 +24,12 @@ - - Profile - +
+ + + + +
@@ -34,55 +37,87 @@
-
+
-
-
+
+
+
+
-
+
+
- +
-
-
- -
- + +
+ + +
+
+ +
+
+ +
+ + +
+ +
+ + +
+
+
+ + + +
+
+
+
-
+
+
+