diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..6fc8feddf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +bundle.js \ No newline at end of file diff --git a/chitterApi.js b/chitterApi.js new file mode 100644 index 000000000..ec929f8b7 --- /dev/null +++ b/chitterApi.js @@ -0,0 +1,62 @@ +class ChitterApi { + fetchPeeps(callback) { + fetch("https://chitter-backend-api-v2.herokuapp.com/peeps") + .then((response) => response.json()) + .then(callback); + } + + createUser(newUsername, newPassword, callback) { + fetch("https://chitter-backend-api-v2.herokuapp.com/users", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: { handle: newUsername, password: newPassword }, + }), + }) + .then((response) => response.json()) + .then(callback); + } + + loginUser(username, password, callback) { + fetch("https://chitter-backend-api-v2.herokuapp.com/sessions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + session: { handle: username, password: password }, + }), + }) + .then((response) => response.json()) + .then(callback); + } + + postPeep(sessionKey, userId, peepBody, callback) { + fetch("https://chitter-backend-api-v2.herokuapp.com/peeps", { + method: "POST", + headers: { + Authorization: `Token token=${sessionKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + peep: { + user_id: userId, + body: peepBody, + }, + }), + }) + .then((response) => response.json()) + .then(callback); + } + + deletePeep(peepId, sessionKey, callback) { + fetch(`https://chitter-backend-api-v2.herokuapp.com/peeps/${peepId}`, { + method: "DELETE", + headers: { Authorization: `Token token=${sessionKey}` }, + }).then(callback); + } +} + +module.exports = ChitterApi; diff --git a/chitterApi.test.js b/chitterApi.test.js new file mode 100644 index 000000000..4209d6f1e --- /dev/null +++ b/chitterApi.test.js @@ -0,0 +1,88 @@ +const ChitterApi = require("./chitterApi"); + +require("jest-fetch-mock").enableMocks(); + +let api, + testPeepsArray, + testUserSessionResponse, + testPostPeepResponse, + testDeletePeepResponse; + +describe("ChitterApi class", () => { + beforeEach(() => { + api = new ChitterApi(); + testPeepsArray = require("./testPeepsArray"); + testUserSessionResponse = require("./testUserSessionResponse"); + testPostPeepResponse = require("./testPostPeepResponse"); + testDeletePeepResponse = require("./testDeletePeepResponse"); + fetch.resetMocks(); + }); + + it("gets peeps from chitter", () => { + expect.assertions(4); + fetch.mockResponseOnce(JSON.stringify(testPeepsArray)); + api.fetchPeeps((data) => { + expect(data.length).toBe(2); + expect(data[0].id).toBe(1494); + expect(data[1].id).toBe(1461); + expect(fetch.mock.calls[0][0]).toEqual( + "https://chitter-backend-api-v2.herokuapp.com/peeps" + ); + }); + }); + + it("creates a new user", () => { + expect.assertions(3); + + fetch.mockResponseOnce(JSON.stringify(testUserSessionResponse)); + api.createUser("kay", "mypassword", (data) => { + expect(data.handle).toBe("kay"); + expect(fetch.mock.calls[0][0]).toEqual( + "https://chitter-backend-api-v2.herokuapp.com/users" + ); + expect(fetch.mock.calls[0][1].method).toBe("POST"); + }); + }); + + it("logs in a user", () => { + expect.assertions(4); + fetch.mockResponseOnce( + JSON.stringify({ + user_id: 1, + session_key: "a_valid_session_key", + }) + ); + api.loginUser("kay", "mypassword", (data) => { + expect(data.user_id).toBe(1); + expect(data.session_key).toBe("a_valid_session_key"); + expect(fetch.mock.calls[0][0]).toEqual( + "https://chitter-backend-api-v2.herokuapp.com/sessions" + ); + expect(fetch.mock.calls[0][1].method).toBe("POST"); + }); + }); + + it("posts a Peep", () => { + expect.assertions(3); + fetch.mockResponseOnce(JSON.stringify(testPostPeepResponse)); + api.postPeep("sessionKey", 1, "my first peep :)", (data) => { + expect(data).toEqual(testPostPeepResponse); + expect(fetch.mock.calls[0][0]).toEqual( + "https://chitter-backend-api-v2.herokuapp.com/peeps" + ); + expect(fetch.mock.calls[0][1].method).toBe("POST"); + }); + }); + + it("deletes a Peep", () => { + expect.assertions(3); + fetch.mockResponseOnce({}, { status: 204 }); + api.deletePeep(1494, "session key", (response) => { + expect(fetch.mock.calls[0][0]).toEqual( + "https://chitter-backend-api-v2.herokuapp.com/peeps/1494" + ); + expect(fetch.mock.calls[0][1].method).toBe("DELETE"); + expect(response.status).toBe(204); + }); + }); +}); diff --git a/chitterModel.js b/chitterModel.js new file mode 100644 index 000000000..92bb01db2 --- /dev/null +++ b/chitterModel.js @@ -0,0 +1,41 @@ +class ChitterModel { + constructor() { + this.peepsArray = []; + this.userId = null; + this.sessionKey = null; + } + + setPeeps(peepsArray) { + this.peepsArray = peepsArray; + } + + loadPeeps() { + return this.peepsArray; + } + + setUserId(userId) { + this.userId = userId; + } + + loadUserId() { + return this.userId; + } + + resetUserId() { + this.userId = null; + } + + setSessionKey(sessionKey) { + this.sessionKey = sessionKey; + } + + loadSessionKey() { + return this.sessionKey; + } + + resetSessionKey() { + this.sessionKey = null; + } +} + +module.exports = ChitterModel; diff --git a/chitterModel.test.js b/chitterModel.test.js new file mode 100644 index 000000000..6c56a49aa --- /dev/null +++ b/chitterModel.test.js @@ -0,0 +1,25 @@ +const ChitterModel = require("./chitterModel"); + +let model, peepsArray; + +describe("ChitterModel class", () => { + beforeEach(() => { + model = new ChitterModel(); + testPeepsArray = require("./testPeepsArray"); + }); + + it("sets and loads Peeps", () => { + model.setPeeps(peepsArray); + expect(model.loadPeeps()).toBe(peepsArray); + }); + + it("sets the user ID", () => { + model.setUserId(1); + expect(model.loadUserId()).toBe(1); + }); + + it("sets the session key", () => { + model.setSessionKey("session key"); + expect(model.loadSessionKey()).toBe("session key"); + }); +}); diff --git a/chitterView.js b/chitterView.js new file mode 100644 index 000000000..230c84001 --- /dev/null +++ b/chitterView.js @@ -0,0 +1,141 @@ +class ChitterView { + constructor(model, api, displaySinglePeep) { + this.model = model; + this.api = api; + this.displaySinglePeep = displaySinglePeep; + + this.newUsernameInputEl = document.querySelector("input#create-username"); + this.newPasswordInputEl = document.querySelector("input#create-password"); + + // create user button listener + document + .querySelector("#create-user-button") + .addEventListener("click", () => { + this.removeUserCreatedMessages(); + this.api.createUser( + this.newUsernameInputEl.value, + this.newPasswordInputEl.value, + (data) => { + this.handleTakenLogic(data); + this.newUsernameInputEl.value = ""; + this.newPasswordInputEl.value = ""; + } + ); + }); + + this.loginUsernameEl = document.querySelector("#login-username"); + this.loginPasswordEl = document.querySelector("#login-password"); + + // login button listener + document.querySelector("#login-button").addEventListener("click", () => { + this.api.loginUser( + this.loginUsernameEl.value, + this.loginPasswordEl.value, + (data) => { + this.model.setUserId(data.user_id); + this.model.setSessionKey(data.session_key); + this.loginUsernameEl.value = ""; + this.loginPasswordEl.value = ""; + setTimeout(this.displayPeepsFromApi(), 500); + } + ); + }); + + // logout button listener + document.querySelector("#logout-button").addEventListener("click", () => { + this.model.resetSessionKey(); + this.model.resetUserId(); + setTimeout(this.displayPeepsFromApi(), 500); + }); + + this.postPeepBodyEl = document.querySelector("#post-peep-body"); + + // post peep button listener + document + .querySelector("#post-peep-button") + .addEventListener("click", () => { + this.api.postPeep( + this.model.loadSessionKey(), + this.model.loadUserId(), + this.postPeepBodyEl.value, + () => { + this.createPeepSuccessMesage(); + this.postPeepBodyEl.value = ""; + setTimeout(this.displayPeepsFromApi(), 500); + } + ); + }); + } + + addDeleteButtonListeners() { + document.querySelectorAll(".delete-peep-button").forEach((element) => { + const peepId = element.id.split("-")[3]; + element.addEventListener("click", () => { + this.api.deletePeep(peepId, this.model.loadSessionKey(), (data) => { + setTimeout(this.displayPeepsFromApi(), 500); + }); + }); + }); + } + + createPeepSuccessMesage() { + const postPeepSuccessMessageEl = document.createElement("p"); + postPeepSuccessMessageEl.id = "post-peep-success-message"; + postPeepSuccessMessageEl.textContent = "Peep posted successfully!"; + document + .querySelector("div#post-peep-container") + .append(postPeepSuccessMessageEl); + } + + removeUserCreatedMessages() { + if (document.querySelector("#handle-taken-message") !== null) + document.querySelector("#handle-taken-message").remove(); + if (document.querySelector("#new-user-created-message") !== null) + document.querySelector("#new-user-created-message").remove(); + } + + handleTakenLogic(data) { + if (data.handle[0] === "has already been taken") { + this.handleTaken(); + } else { + this.userCreated(); + } + } + + handleTaken() { + const handleTakenMessageEl = document.createElement("p"); + handleTakenMessageEl.id = "handle-taken-message"; + handleTakenMessageEl.textContent = "This handle has been taken"; + this.createUserContainerEl.append(handleTakenMessageEl); + } + + userCreated() { + const newUserCreatedMessageEl = document.createElement("p"); + newUserCreatedMessageEl.textContent = "You have created a new account!"; + newUserCreatedMessageEl.id = "new-user-created-message"; + this.createUserContainerEl.append(newUserCreatedMessageEl); + } + + displayPeeps() { + if (document.querySelectorAll("div.peep").length > 0) { + document + .querySelectorAll("div.peep") + .forEach((element) => element.remove()); + } + this.model + .loadPeeps() + .forEach((peep) => + this.displaySinglePeep.display(peep, this.model.loadUserId()) + ); + this.addDeleteButtonListeners(); + } + + displayPeepsFromApi() { + this.api.fetchPeeps((peeps) => { + this.model.setPeeps(peeps); + this.displayPeeps(); + }); + } +} + +module.exports = ChitterView; diff --git a/chitterView.test.js b/chitterView.test.js new file mode 100644 index 000000000..133ab127d --- /dev/null +++ b/chitterView.test.js @@ -0,0 +1,214 @@ +/** + * @jest-environment jsdom + */ + +const fs = require("fs"); +const { default: JSDOMEnvironment } = require("jest-environment-jsdom"); +const ChitterModel = require("./chitterModel"); +const ChitterView = require("./chitterView"); +const DisplaySinglePeep = require("./displaySinglePeep"); +const testPostPeepResponse = require("./testPostPeepResponse"); +let view, + model, + mockApi, + displaySinglePeep, + testPeepsArray, + testUserSessionResponse, + fakeCallbackResponse; + +describe("ChitterView", () => { + beforeEach(() => { + document.body.innerHTML = fs.readFileSync("./index.html"); + model = new ChitterModel(); + testPeepsArray = require("./testPeepsArray"); + testUserSessionResponse = require("./testUserSessionResponse"); + fakeCallbackResponse = jest.fn(); + mockApi = { + fetchPeeps: jest.fn((callback) => callback(fakeCallbackResponse())), + createUser: jest.fn((username, password, callback) => + callback(fakeCallbackResponse()) + ), + loginUser: jest.fn((username, password, callback) => + callback(fakeCallbackResponse()) + ), + postPeep: jest.fn((sessionKey, userId, peepBody, callback) => + callback(fakeCallbackResponse()) + ), + deletePeep: jest.fn((peepId, sessionKey, callback) => + callback(fakeCallbackResponse()) + ), + }; + + displaySinglePeep = new DisplaySinglePeep(); + view = new ChitterView(model, mockApi, displaySinglePeep); + }); + + it("displays all Peeps", () => { + fakeCallbackResponse.mockReturnValueOnce(testPeepsArray); + view.displayPeepsFromApi(); + expect(mockApi.fetchPeeps).toHaveBeenCalled(); + const peepDivEls = document.querySelectorAll("div.peep"); + expect(peepDivEls.length).toBe(2); + expect(peepDivEls[0].querySelector(".peep-body").textContent).toBe( + "First peep" + ); + expect(peepDivEls[0].querySelector(".peep-user-handle").textContent).toBe( + "jony144" + ); + expect( + peepDivEls[0].querySelector(".peep-datetime-created").textContent + ).toBe("12:33 Sat Aug 20 2022"); + expect(peepDivEls[0].querySelector(".peep-likes-count").textContent).toBe( + "1 like" + ); + expect(peepDivEls[1].querySelector(".peep-body").textContent).toBe( + "i'm tiz" + ); + expect(peepDivEls[1].querySelector(".peep-user-handle").textContent).toBe( + "tiz" + ); + expect( + peepDivEls[1].querySelector(".peep-datetime-created").textContent + ).toBe("13:04 Sun Aug 07 2022"); + expect(peepDivEls[1].querySelector(".peep-likes-count").textContent).toBe( + "0 likes" + ); + }); + + it("doesn't add new peeps with each successive call of displayPeeps", () => { + fakeCallbackResponse.mockReturnValue(testPeepsArray); + view.displayPeepsFromApi(); + expect(mockApi.fetchPeeps).toHaveBeenCalled(); + view.displayPeepsFromApi(); + const peepDivEls = document.querySelectorAll("div.peep"); + expect(peepDivEls.length).toBe(2); + }); + + it("creates a new user", () => { + fakeCallbackResponse.mockReturnValueOnce(testUserSessionResponse); + document.querySelector("input#create-username").value = "username"; + document.querySelector("input#create-password").value = "password"; + document.querySelector("#create-user-button").click(); + expect(mockApi.createUser).toHaveBeenCalled(); + expect(mockApi.createUser.mock.calls[0][0]).toBe("username"); + expect(mockApi.createUser.mock.calls[0][1]).toBe("password"); + expect( + document.querySelector("#new-user-created-message").textContent + ).toBe("You have created a new account!"); + expect(document.querySelector("input#create-username").value).toBe(""); + expect(document.querySelector("input#create-password").value).toBe(""); + }); + + it("displays a new user error", () => { + fakeCallbackResponse.mockReturnValueOnce({ + handle: ["has already been taken"], + }); + document.querySelector("input#create-username").value = "username"; + document.querySelector("input#create-password").value = "password"; + document.querySelector("#create-user-button").click(); + expect(mockApi.createUser).toHaveBeenCalled(); + expect(mockApi.createUser.mock.calls[0][0]).toBe("username"); + expect(mockApi.createUser.mock.calls[0][1]).toBe("password"); + expect(document.querySelector("#handle-taken-message").textContent).toBe( + "This handle has been taken" + ); + expect(document.querySelector("input#create-username").value).toBe(""); + expect(document.querySelector("input#create-password").value).toBe(""); + }); + + it("logs in a user", () => { + fakeCallbackResponse + .mockReturnValueOnce({ + user_id: 1, + session_key: "a_valid_session_key", + }) + .mockReturnValueOnce(testPeepsArray); + document.querySelector("input#login-username").value = "username"; + document.querySelector("input#login-password").value = "password"; + document.querySelector("#login-button").click(); + expect(mockApi.loginUser).toHaveBeenCalled(); + expect(mockApi.loginUser.mock.calls[0][0]).toBe("username"); + expect(mockApi.loginUser.mock.calls[0][1]).toBe("password"); + expect(document.querySelector("input#login-username").value).toBe(""); + expect(document.querySelector("input#login-password").value).toBe(""); + expect(model.loadUserId()).toBe(1); + expect(model.loadSessionKey()).toBe("a_valid_session_key"); + expect(mockApi.fetchPeeps).toHaveBeenCalled(); + }); + + it("logs a user out", () => { + fakeCallbackResponse.mockReturnValue(testPeepsArray).mockReturnValueOnce({ + user_id: 1, + session_key: "a_valid_session_key", + }); + document.querySelector("input#login-username").value = "username"; + document.querySelector("input#login-password").value = "password"; + document.querySelector("#login-button").click(); + expect(mockApi.fetchPeeps).toHaveBeenCalled(); + document.querySelector("#logout-button").click(); + expect(model.loadUserId()).toBe(null); + expect(model.loadSessionKey()).toBe(null); + expect(mockApi.fetchPeeps).toHaveBeenCalled(); + }); + + it("posts a Peep", () => { + fakeCallbackResponse + .mockReturnValueOnce({ + user_id: 1, + session_key: "a_valid_session_key", + }) + .mockReturnValueOnce(testPeepsArray); + document.querySelector("input#login-username").value = "kay"; + document.querySelector("input#login-password").value = "mypassword"; + document.querySelector("#login-button").click(); + fakeCallbackResponse + .mockReturnValueOnce(testPostPeepResponse) + .mockReturnValueOnce([testPostPeepResponse].concat(testPeepsArray)); + document.querySelector("#post-peep-body").value = "my first peep :)"; + document.querySelector("#post-peep-button").click(); + expect(mockApi.postPeep).toHaveBeenCalled(); + expect(mockApi.postPeep.mock.calls[0][0]).toBe("a_valid_session_key"); + expect(mockApi.postPeep.mock.calls[0][1]).toBe(1); + expect(mockApi.postPeep.mock.calls[0][2]).toBe("my first peep :)"); + expect(document.querySelector("#post-peep-body").value).toBe(""); + document.querySelector("#post-peep-success-message").textContent = + "Peep posted successfully!"; + + expect(mockApi.fetchPeeps).toHaveBeenCalled(); + const newPeepEls = document.querySelectorAll("div.peep"); + expect(newPeepEls.length).toBe(3); + expect(newPeepEls[0].querySelector(".peep-body").textContent).toBe( + "my first peep :)" + ); + expect(newPeepEls[0].querySelector(".peep-user-handle").textContent).toBe( + "kay" + ); + expect( + newPeepEls[0].querySelector(".peep-datetime-created").textContent + ).toBe("14:21 Sat Jun 23 2018"); + expect(newPeepEls[0].querySelector(".peep-likes-count").textContent).toBe( + "0 likes" + ); + }); + + it("deletes a peep", () => { + fakeCallbackResponse + .mockReturnValueOnce({ + user_id: 1124, + session_key: "a_valid_session_key", + }) + .mockReturnValue(testPeepsArray); + document.querySelector("input#login-username").value = "jony144"; + document.querySelector("input#login-password").value = "mypassword"; + document.querySelector("#login-button").click(); + expect(document.querySelectorAll(".delete-peep-button").length).toBe(1); + document + .querySelectorAll("div.peep")[0] + .querySelector(".delete-peep-button") + .click(); + expect(mockApi.deletePeep).toHaveBeenCalled(); + expect(mockApi.deletePeep.mock.calls[0][0]).toBe("1494"); + expect(mockApi.deletePeep.mock.calls[0][1]).toBe("a_valid_session_key"); + expect(mockApi.fetchPeeps).toHaveBeenCalledTimes(2); + }); +}); diff --git a/displaySinglePeep.js b/displaySinglePeep.js new file mode 100644 index 000000000..3bde2e2c5 --- /dev/null +++ b/displaySinglePeep.js @@ -0,0 +1,59 @@ +class DisplaySinglePeep { + display(peep, userId) { + const peepsContainerEl = document.querySelector("#display-peeps-container"); + const peepDivEl = document.createElement("div"); + peepDivEl.className = "peep"; + peepDivEl.append(this.createPeepBody(peep)); + peepDivEl.append(this.createPeepUserHandle(peep)); + peepDivEl.append(this.createPeepDatetimeCreated(peep)); + peepDivEl.append(this.createPeepLikesCount(peep)); + if (userId === peep.user.id) { + peepDivEl.append(this.createDeletePeepButton(peep)); + } + peepsContainerEl.append(peepDivEl); + } + + createPeepBody(peep) { + const peepBodyEl = document.createElement("p"); + peepBodyEl.className = "peep-body"; + peepBodyEl.textContent = peep.body; + return peepBodyEl; + } + + createPeepUserHandle(peep) { + const peepUserHandleEl = document.createElement("p"); + peepUserHandleEl.className = "peep-user-handle"; + peepUserHandleEl.textContent = peep.user.handle; + return peepUserHandleEl; + } + + createPeepDatetimeCreated(peep) { + const peepDatetimeCreatedEl = document.createElement("p"); + peepDatetimeCreatedEl.className = "peep-datetime-created"; + const peepCreatedString = new Date(peep.created_at); + const peepTimeString = peepCreatedString.toTimeString().substring(0, 5); + const peepDateString = peepCreatedString.toDateString(); + peepDatetimeCreatedEl.textContent = `${peepTimeString} ${peepDateString}`; + return peepDatetimeCreatedEl; + } + + createPeepLikesCount(peep) { + const peepLikesCountEl = document.createElement("p"); + peepLikesCountEl.className = "peep-likes-count"; + const likesCount = peep.likes.length; + peepLikesCountEl.textContent = `${likesCount} ${ + likesCount === 1 ? "like" : "likes" + }`; + return peepLikesCountEl; + } + + createDeletePeepButton(peep) { + const deletePeepButtonEl = document.createElement("button"); + deletePeepButtonEl.className = "delete-peep-button"; + deletePeepButtonEl.id = `delete-button-id-${peep.id}`; + deletePeepButtonEl.textContent = "delete peep"; + return deletePeepButtonEl; + } +} + +module.exports = DisplaySinglePeep; diff --git a/index.html b/index.html new file mode 100644 index 000000000..9e7e78af9 --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ + + +
+ + + +