diff --git a/.github/workflows/css-linting.yml b/.github/workflows/css-linting.yml index fae3b66..48b333a 100644 --- a/.github/workflows/css-linting.yml +++ b/.github/workflows/css-linting.yml @@ -20,4 +20,4 @@ jobs: - name: Install dependencies run: sudo npm install - name: Run tests - run: sudo npm run lintCSS \ No newline at end of file + run: sudo npm run lint-css \ No newline at end of file diff --git a/.github/workflows/html-linting.yml b/.github/workflows/html-linting.yml index ab68590..c24292b 100644 --- a/.github/workflows/html-linting.yml +++ b/.github/workflows/html-linting.yml @@ -20,4 +20,4 @@ jobs: - name: Install dependencies run: sudo npm install - name: Run tests - run: sudo npm run lintHTML + run: sudo npm run lint-html diff --git a/.github/workflows/js-linting.yml b/.github/workflows/js-linting.yml index f3aafaf..b642926 100644 --- a/.github/workflows/js-linting.yml +++ b/.github/workflows/js-linting.yml @@ -22,4 +22,4 @@ jobs: - name: Install dependencies run: sudo npm install - name: Run tests - run: sudo npm run lint \ No newline at end of file + run: sudo npm run lint-js \ No newline at end of file diff --git a/.github/workflows/js-unittest.yml b/.github/workflows/js-unittest.yml index 4cb1359..918ce70 100644 --- a/.github/workflows/js-unittest.yml +++ b/.github/workflows/js-unittest.yml @@ -23,5 +23,7 @@ jobs: uses: actions/checkout@v3 - name: Install dependencies run: sudo npm install + - name: Start local http server + run: sudo npm run http-server & - name: Run tests run: sudo npm test \ No newline at end of file diff --git a/.htmlhintrc b/.htmlhintrc index edd263f..c833339 100644 --- a/.htmlhintrc +++ b/.htmlhintrc @@ -1,3 +1,4 @@ { - "attr-value-not-empty": false + "attr-value-not-empty": false, + "space-tab-mixed-disabled": "tab" } diff --git a/.stylelintrc.json b/.stylelintrc.json index 2f1eeda..c3e95e2 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,3 +1,7 @@ { - "extends": "stylelint-config-standard" + "extends": "stylelint-config-standard", + "ignore": ["inside-parens", "param", "value"], + "rules":{ + "indentation": "tab" + } } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/package.json b/package.json index bd8a69c..be9a691 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,20 @@ "type": "module", "scripts": { "test": "mocha --recursive --require mock-local-storage './{,!(node_modules)/**}/*.test.js'", - "lint": "eslint '**/*.js'", - "fix-style": "eslint --fix **/*.js", - "lintHTML": "htmlhint '**/*.html'", - "lintCSS": "stylelint '**/*.css'" + "lint-js": "eslint **/*.js", + "fix-js": "eslint --fix **/*.js", + "lint-html": "htmlhint **/*.html", + "lint-css": "stylelint **/*.css", + "fix-css": "stylelint --fix **/*.css", + "http-server": "http-server source" }, "devDependencies": { "eslint": "^8.27.0", "htmlhint": "1.1.4", + "http-server": "", "mocha": "10", "mock-local-storage": "^1.1.23", + "puppeteer": "^18.2.1", "stylelint": "14.14.1", "stylelint-config-standard": "^29.0.0" } diff --git a/source/CreatePage.html b/source/CreatePage.html index 64dfe78..67529e2 100644 --- a/source/CreatePage.html +++ b/source/CreatePage.html @@ -1,77 +1,93 @@ - - - - - Food Journal + + + + + Food Journal - - + + + + + - - - - - + + + + + + + - +
+
+ logo +

Food Journal

+ logo +
+
+ + +
+

New Entry

- -
-
- Pic: - - -
-
+ + +
+ PICTURE: + + +
+ +
+ MEAL NAME: + +
- Meal: - - -
+
+ RESTAURANT NAME: + +
+ +
+ RATING: +
+
+ + + + + +
+
+
-
- Rating: - - - - - -
+
+ COMMENTS: + +
-
- Other Info: - - +
+
+ +
- - - -
- + + + +
+ \ No newline at end of file diff --git a/source/ReviewDetails.html b/source/ReviewDetails.html index 6c1080b..9690f16 100644 --- a/source/ReviewDetails.html +++ b/source/ReviewDetails.html @@ -1,77 +1,122 @@ - - - - - Food Journal + + + + + Food Journal - - + + + + - - - - - + + + + + + + + +
+
+ logo +

Food Journal

+ logo +
+
- - -
- - -
- + + + diff --git a/source/assets/images/icons/0-star.svg b/source/assets/images/0-star.svg similarity index 100% rename from source/assets/images/icons/0-star.svg rename to source/assets/images/0-star.svg diff --git a/source/assets/images/icons/1-star.svg b/source/assets/images/1-star.svg similarity index 100% rename from source/assets/images/icons/1-star.svg rename to source/assets/images/1-star.svg diff --git a/source/assets/images/icons/2-star.svg b/source/assets/images/2-star.svg similarity index 100% rename from source/assets/images/icons/2-star.svg rename to source/assets/images/2-star.svg diff --git a/source/assets/images/icons/3-star.svg b/source/assets/images/3-star.svg similarity index 100% rename from source/assets/images/icons/3-star.svg rename to source/assets/images/3-star.svg diff --git a/source/assets/images/icons/4-star.svg b/source/assets/images/4-star.svg similarity index 100% rename from source/assets/images/icons/4-star.svg rename to source/assets/images/4-star.svg diff --git a/source/assets/images/icons/5-star.svg b/source/assets/images/5-star.svg similarity index 100% rename from source/assets/images/icons/5-star.svg rename to source/assets/images/5-star.svg diff --git a/source/assets/images/Grouppink.png b/source/assets/images/Grouppink.png new file mode 100644 index 0000000..d4f1d14 Binary files /dev/null and b/source/assets/images/Grouppink.png differ diff --git a/source/assets/images/Logo.png b/source/assets/images/Logo.png new file mode 100644 index 0000000..0f5cf77 Binary files /dev/null and b/source/assets/images/Logo.png differ diff --git a/source/assets/images/icons/default_plate.png b/source/assets/images/default_plate.png similarity index 100% rename from source/assets/images/icons/default_plate.png rename to source/assets/images/default_plate.png diff --git a/source/assets/images/delete_icon_for_interface.png b/source/assets/images/delete_icon_for_interface.png new file mode 100644 index 0000000..9d1f025 Binary files /dev/null and b/source/assets/images/delete_icon_for_interface.png differ diff --git a/source/assets/images/edit_button_for_interface.png b/source/assets/images/edit_button_for_interface.png new file mode 100644 index 0000000..4af8c8d Binary files /dev/null and b/source/assets/images/edit_button_for_interface.png differ diff --git a/source/assets/images/favicon.ico b/source/assets/images/favicon.ico new file mode 100644 index 0000000..e0dd9ea Binary files /dev/null and b/source/assets/images/favicon.ico differ diff --git a/source/assets/images/home_button_for_interface.png b/source/assets/images/home_button_for_interface.png new file mode 100644 index 0000000..7984140 Binary files /dev/null and b/source/assets/images/home_button_for_interface.png differ diff --git a/source/assets/images/icons/plate_with_chopsticks.png b/source/assets/images/plate_with_chopsticks.png similarity index 100% rename from source/assets/images/icons/plate_with_chopsticks.png rename to source/assets/images/plate_with_chopsticks.png diff --git a/source/assets/images/icons/plate_with_cutlery.png b/source/assets/images/plate_with_cutlery.png similarity index 100% rename from source/assets/images/icons/plate_with_cutlery.png rename to source/assets/images/plate_with_cutlery.png diff --git a/source/assets/scripts/CreatePage.js b/source/assets/scripts/CreatePage.js new file mode 100644 index 0000000..fab6409 --- /dev/null +++ b/source/assets/scripts/CreatePage.js @@ -0,0 +1,110 @@ +import { newReviewToStorage } from "./localStorage.js"; + +window.addEventListener("DOMContentLoaded", init); + +function init() { + // get next id + + // creates the key + initFormHandler(); + +} + +function initFormHandler() { + + //accessing form components + let tagContainer = document.getElementById("tag-container-form"); + let form = document.querySelector("form"); + + /* + * change the input source of the image between local file and URL + * depending on user's selection + */ + let select = document.getElementById("select"); + select.addEventListener("change", function() { + const input = document.getElementById("source"); + + if (select.value == "file") { + input.innerHTML = ` + Source: + + `; + } + //TODO: change to photo taking for sprint 3 + else { + input.innerHTML = ` + Source: + + `; + } + }); + + //addressing sourcing image from local file + let imgDataURL = ""; + document.getElementById("mealImg").addEventListener("change", function() { + const reader = new FileReader(); + + //store image data URL after successful image load + reader.addEventListener("load", ()=>{ + imgDataURL = reader.result; + }, false); + + //convert image file into data URL for local storage + reader.readAsDataURL(document.getElementById("mealImg").files[0]); + }); + + form.addEventListener("submit", function(e){ + /* + * User submits the form for their review. + * We create reviewCard and put in storage + */ + e.preventDefault(); + let formData = new FormData(form); + let reviewObject = {}; + for (let [key, value] of formData) { + console.log(`${key}`); + console.log(`${value}`); + if (`${key}` !== "tag-form") { + reviewObject[`${key}`] = `${value}`; + } + if (`${key}` === "mealImg" && select.value == "file") { + reviewObject["mealImg"] = imgDataURL; + } + } + if(reviewObject["rating"] != null){ + reviewObject["tags"] = []; + + let tags = document.querySelectorAll(".tag"); + for(let i = 0; i < tags.length; i ++) { + reviewObject["tags"].push(tags[i].innerHTML); + tagContainer.removeChild(tags[i]); + } + + let nextReviewId = newReviewToStorage(reviewObject); + sessionStorage.setItem("currID", JSON.stringify(nextReviewId)); + + window.location.assign("./ReviewDetails.html"); + } else{ + window.alert("NO! FILL IN STARS"); + } + + }); + + let tagAddBtn = document.getElementById("tag-add-btn"); + tagAddBtn.addEventListener("click", ()=> { + let tagField = document.getElementById("tag-form"); + if (tagField.value.length > 0) { + let tagLabel = document.createElement("label"); + tagLabel.innerHTML = tagField.value; + tagLabel.setAttribute("class","tag"); + tagLabel.addEventListener("click",()=> { + tagContainer.removeChild(tagLabel); + }); + + tagContainer.append(tagLabel); + tagField.value = ""; + + } + }); + +} diff --git a/source/assets/scripts/ReviewCard.js b/source/assets/scripts/ReviewCard.js index 07df6a6..cb60ccf 100644 --- a/source/assets/scripts/ReviewCard.js +++ b/source/assets/scripts/ReviewCard.js @@ -6,91 +6,108 @@ class ReviewCard extends HTMLElement { constructor() { super(); - let shadowEl = this.attachShadow({mode:"open"}); let articleEl = document.createElement("article"); let styleEl = document.createElement("style"); styleEl.textContent = ` - * { - font-family: sans-serif; - margin: 0; - padding: 0; - } - - a { - text-decoration: none; - } - - a:hover { - text-decoration: underline; - } - - article { - align-items: center; - border: 1px solid rgb(223, 225, 229); - border-radius: 8px; - display: grid; - grid-template-rows: 118px 56px 14px 18px 15px 36px; - height: auto; - row-gap: 5px; - padding: 0 16px 16px 16px; - width: 178px; - } - - div.rating { - align-items: center; - column-gap: 5px; - display: flex; - } - - div.rating>img { - height: auto; - display: inline-block; - object-fit: scale-down; - width: 78px; - } - - article>img { - border-top-left-radius: 8px; - border-top-right-radius: 8px; - height: 118px; - object-fit: cover; - margin-left: -16px; - width: calc(100% + 32px); - } - - label.restaurant-name { - color: black !important; - } - - label.meal-name { - display: -webkit-box; - font-size: 16px; - height: 36px; - line-height: 18px; - overflow: hidden; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - } - - label:not(.meal-name), - span, - time { - color: #70757A; - font-size: 12px; - } - `; + * { + font-family: Century Gothic; + margin: 0; + padding: 0; + overflow-wrap: anywhere; + } + + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + article { + align-items: center; + border: 2px solid rgb(31, 41, 32); + border-radius: 8px; + display: grid; + grid-template-rows: 118px 56px 14px 18px 15px 36px; + height: auto; + row-gap: 5px; + padding: 0 16px 16px 16px; + width: 178px; + margin: 8px 8px 8px 8px; + } + + div.rating { + align-items: center; + column-gap: 5px; + display: flex; + } + + div.rating>img { + height: auto; + display: inline-block; + object-fit: scale-down; + width: 78px; + } + + article>img { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + height: 119px; + object-fit: cover; + margin-left: -16px; + width: calc(100% + 32px); + } + + label.restaurant-name { + color: black !important; + } + + label.meal-name { + display: -webkit-box; + font-size: 16px; + height: 36px; + line-height: 18px; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + label:not(.meal-name), + span, + time { + color: #70757A; + font-size: 12px; + } + + .tag-container { + margin-top: 20px; + display: flex; + flex-flow: row wrap; + } + + .a-tag { + background-color:#94da97; + border-radius: 7px; + color: #94da97; + padding-right: 7px; + padding-left: 7px; + margin: 3px; + font-weight: bold; + } + `; articleEl.append(styleEl); shadowEl.append(articleEl); this.shadowEl = shadowEl; //attach event listener to each recipe-card this.addEventListener("click", (event) => { console.log(event.target); - console.log(event.target.data); + console.log(event.target.reviewId); //Option 1: sending current data to second html page using localStorage (could also just store index) - sessionStorage.setItem("current", JSON.stringify(event.target.data)); + sessionStorage.setItem("currID", JSON.stringify(event.target.data.reviewID)); window.location.assign("./ReviewDetails.html"); /* //Option 2: sending current data to second html page using string query w/ url (currently not storing value) @@ -117,7 +134,6 @@ class ReviewCard extends HTMLElement { * following format: * { * "mealImg": "string", - * "imgAlt": "string", * "mealName": "string", * "comments": "string", * "rating": number, @@ -133,12 +149,17 @@ class ReviewCard extends HTMLElement { let articleEl = this.shadowEl.querySelector("article"); // setting the article elements for the review card + this.reviewID = data["reviewID"]; //image setup let mealImg = document.createElement("img"); mealImg.setAttribute("id", "a-mealImg"); + mealImg.setAttribute("alt","Meal Photo Corrupted"); mealImg.setAttribute("src",data["mealImg"]); - mealImg.setAttribute("alt",data["imgAlt"]); + mealImg.addEventListener("error", function(e) { + mealImg.setAttribute("src", "./assets/images/plate_with_cutlery.png"); + e.onerror = null; + }); //meal name setup let mealLabel = document.createElement("label"); @@ -147,26 +168,6 @@ class ReviewCard extends HTMLElement { mealLabel.innerHTML = data["mealName"]; //restaurant name setup - /* - //review page link - //giving it functionality to save the review card's info to session storage for loading the review page - let reviewLink = document.createElement('a'); - reviewLink.setAttribute('href','./review.html') - reviewLink.innerHTML = 'review page' - reviewLink.addEventListener('click', () => { - sessionStorage.clear(); - let currReview = { - "imgSrc": data['imgSrc'], - "imgAlt": data['imgAlt'], - "mealName": data['mealName'], - "restaurant": data['restaurant'], - "comments": data['comments'], - "rating": data['rating'], - "tags": data['tags'] - } - sessionStorage.setItem('currReview', JSON.stringify(currReview)); - }); -*/ let restaurantLabel = document.createElement("label"); restaurantLabel.setAttribute("id", "a-restaurant"); restaurantLabel.setAttribute("class","restaurant-name"); @@ -183,7 +184,7 @@ class ReviewCard extends HTMLElement { ratingDiv.setAttribute("class", "rating"); let starsImg = document.createElement("img"); starsImg.setAttribute("id", "a-rating"); - starsImg.setAttribute("src", "./assets/images/icons/"+data["rating"]+"-star.svg"); + starsImg.setAttribute("src", "./assets/images/"+data["rating"]+"-star.svg"); starsImg.setAttribute("alt", data["rating"] +" stars"); starsImg.setAttribute("num", data["rating"]); ratingDiv.append(starsImg); @@ -196,15 +197,16 @@ class ReviewCard extends HTMLElement { if(data["tags"]){ for (let i = 0; i < data["tags"].length; i++) { let newTag = document.createElement("label"); - newTag.setAttribute("class","tag"); - newTag.innerHTML = data["tags"][i] + " "; + newTag.setAttribute("class","a-tag"); + newTag.innerHTML = data["tags"][i]; tagContainer.append(newTag); } } + //adding final ID to data! + articleEl.append(mealImg); articleEl.append(mealLabel); - //articleEl.append(reviewLink) articleEl.append(restaurantLabel); articleEl.append(ratingDiv); articleEl.append(tagContainer); @@ -224,7 +226,6 @@ class ReviewCard extends HTMLElement { * following format: * { * "mealImg": "string", - * "imgAlt": "string", * "mealName": "string", * "comments": "string", * "rating": number, @@ -237,11 +238,11 @@ class ReviewCard extends HTMLElement { let dataContainer = {}; // getting the article elements for the review card + dataContainer["reviewID"] = this.reviewID; //get image let mealImg = this.shadowEl.getElementById("a-mealImg"); dataContainer["mealImg"] = mealImg.getAttribute("src"); - dataContainer["imgAlt"] = mealImg.getAttribute("alt"); //get meal name let mealLabel = this.shadowEl.getElementById("a-mealName"); diff --git a/source/assets/scripts/ReviewDetails.js b/source/assets/scripts/ReviewDetails.js index 6819bb9..32a28ea 100644 --- a/source/assets/scripts/ReviewDetails.js +++ b/source/assets/scripts/ReviewDetails.js @@ -1,124 +1,194 @@ //reviewDetails.js -import {getReviewsFromStorage, saveReviewsToStorage} from "./localStorage.js"; +import {deleteReviewFromStorage, getReviewFromStorage, updateReviewToStorage} from "./localStorage.js"; // Run the init() function when the page has loaded window.addEventListener("DOMContentLoaded", init); function init(){ + setupInfo(); setupDelete(); setupUpdate(); } +function setupInfo(){ + let currID = JSON.parse(sessionStorage.getItem("currID")); + let currReview = getReviewFromStorage(currID); + + //meal image + let mealImg = document.getElementById("d-mealImg"); + mealImg.setAttribute("src",currReview["mealImg"]); + mealImg.addEventListener("error", function(e) { + mealImg.setAttribute("src", "./assets/images/plate_with_cutlery.png"); + e.onerror = null; + }); + + //meal name + let mealLabel = document.getElementById("d-mealName"); + mealLabel.innerHTML = currReview["mealName"]; + + //restaurant name + let restaurantLabel = document.getElementById("d-restaurant"); + restaurantLabel.innerHTML = currReview["restaurant"]; + + //comments + let comments = document.getElementById("d-comments"); + comments.innerText = currReview["comments"]; + + //rating + let starsImg = document.getElementById("d-rating"); + starsImg.setAttribute("src", "./assets/images/"+currReview["rating"]+"-star.svg"); + starsImg.setAttribute("alt", currReview["rating"] +" stars"); + + //tags + let tagContainer = document.getElementById("d-tags"); + if(currReview["tags"]){ + for (let i = 0; i < currReview["tags"].length; i++) { + let newTag = document.createElement("label"); + newTag.setAttribute("class","d-tag"); + newTag.innerHTML = currReview["tags"][i]; + tagContainer.append(newTag); + } + } +} + function setupDelete(){ - let deleteBtn = document.getElementById("delete"); - let reviews = getReviewsFromStorage(); - let current = JSON.parse(sessionStorage.getItem("current")); + let deleteBtn = document.getElementById("delete-btn"); + let currID = JSON.parse(sessionStorage.getItem("currID")); deleteBtn.addEventListener("click", function(){ if(window.confirm("Are you sure you want to delete this entry?")){ - //delete function - if(current){ - console.log(current); - for(let i = 0; i < reviews.length; i++){ - console.log(reviews[i]); - if(reviews[i]["mealName"] == current["mealName"] && reviews[i]["restaurant"] == current["restaurant"]){ - console.log("match found"); - reviews.splice(i,1); - saveReviewsToStorage(reviews); - sessionStorage.removeItem("current"); - window.location.assign("./index.html"); - break; - } - } - } + deleteReviewFromStorage(currID); + sessionStorage.removeItem("currID"); + window.location.assign("./index.html"); } }); } function setupUpdate(){ - let updateBtn = document.getElementById("update"); - let reviews = getReviewsFromStorage(); - let current = JSON.parse(sessionStorage.getItem("current")); - let form = document.getElementById("update-food-entry"); + let updateBtn = document.getElementById("update-btn"); + let currID = JSON.parse(sessionStorage.getItem("currID")); + let currReview = getReviewFromStorage(currID); + let form = document.getElementById("new-food-entry"); + let updateDiv = document.getElementById("update-form"); updateBtn.addEventListener("click", function(){ //update function - if(current){ - console.log(current); - form.style.display = "block"; - let tagContainer = document.getElementById("tag-container-form"); - console.log(document.querySelectorAll("#update-food-entry input")); - //Set value of each input element to current's values - document.getElementById("mealImg").defaultValue = current["mealImg"]; - document.getElementById("imgAlt").defaultValue = current["imgAlt"]; - document.getElementById("mealName").defaultValue = current["mealName"]; - document.getElementById("comments").textContent = current["comments"]; - document.getElementById("rating-" + `${current["rating"]}`).checked = true; - document.getElementById("restaurant").defaultValue = current["restaurant"]; + updateDiv.classList.remove("hidden"); - if(current["tags"]){ - for (let i = 0; i < current["tags"].length; i++) { - let newTag = document.createElement("label"); - newTag.setAttribute("class","tag"); - newTag.innerHTML = current["tags"][i] + " "; - newTag.addEventListener("click",()=> { - tagContainer.removeChild(newTag); - }); - tagContainer.append(newTag); + let tagContainer = document.getElementById("tag-container-form"); + + //Set value of each input element to current's values + document.getElementById("mealImg").defaultValue = currReview["mealImg"]; + document.getElementById("mealName").defaultValue = currReview["mealName"]; + document.getElementById("comments").textContent = currReview["comments"]; + document.getElementById("s" + `${currReview["rating"]}`).checked = true; + document.getElementById("restaurant").defaultValue = currReview["restaurant"]; + + if(currReview["tags"]){ + while (tagContainer.firstChild) { + tagContainer.removeChild(tagContainer.firstChild); + } + for (let i = 0; i < currReview["tags"].length; i++) { + let newTag = document.createElement("label"); + newTag.setAttribute("class","tag"); + newTag.innerHTML = currReview["tags"][i]; + newTag.addEventListener("click",()=> { + tagContainer.removeChild(newTag); + }); + tagContainer.append(newTag); + } + } + + /* + * change the input source of the image between local file and URL + * depending on user's selection + */ + let select = document.getElementById("select"); + select.addEventListener("change", function() { + const input = document.getElementById("source"); + + if (select.value == "file") { + input.innerHTML = ` + Source: + + `; + } + //TODO: change to photo taking for sprint 3 + else { + input.innerHTML = ` + Source: + + `; + } + }); + + //addressing sourcing image from local file + let imgDataURL = ""; + document.getElementById("mealImg").addEventListener("change", function() { + console.log("reading used"); + const reader = new FileReader(); + + //store image data URL after successful image load + reader.addEventListener("load", ()=>{ + imgDataURL = reader.result; + }, false); + + //convert image file into data URL for local storage + reader.readAsDataURL(document.getElementById("mealImg").files[0]); + }); + + + //Take formdata values as newData when submit + form.addEventListener("submit", function(){ + /* + * User submits the form for their review. + * We create reviewCard and put in storage + */ + let formData = new FormData(form); + let newData = {}; + for (let [key, value] of formData) { + console.log(`${key}`); + console.log(`${value}`); + if (`${key}` !== "tag-form") { + newData[`${key}`] = `${value}`; + } + //Account for the case where image is not updated + if (`${key}` === "mealImg" && document.getElementById("mealImg").value === "") { + newData["mealImg"] = currReview["mealImg"]; + } + else if (`${key}` === "mealImg" && select.value == "file") { + newData["mealImg"] = imgDataURL; } } - //Take formdata values as newData when submit - form.addEventListener("submit", function(){ - /* - * User submits the form for their review. - * We create reviewCard and put in storage - */ - let formData = new FormData(form); - let newData = {}; - for (let [key, value] of formData) { - console.log(`${key}`); - console.log(`${value}`); - if (`${key}` !== "tag-form") { - newData[`${key}`] = `${value}`; - } - } - newData["tags"] = []; - - let tags = document.querySelectorAll(".tag"); - for(let i = 0; i < tags.length; i ++) { - newData["tags"].push(tags[i].innerHTML); - tagContainer.removeChild(tags[i]); - } + newData["tags"] = []; + + let tags = document.querySelectorAll(".tag"); + for(let i = 0; i < tags.length; i ++) { + newData["tags"].push(tags[i].innerHTML); + tagContainer.removeChild(tags[i]); + } - for(let i = 0; i < reviews.length; i++){ - console.log(reviews[i]); - if(reviews[i]["mealName"] == current["mealName"] && reviews[i]["restaurant"] == current["restaurant"]){ - console.log("match found"); - reviews.splice(i,1,newData); - saveReviewsToStorage(reviews); - sessionStorage.setItem("current", JSON.stringify(newData)); - break; - } - } + newData["reviewID"] = currID; - form.style.display = "none"; + updateReviewToStorage(currID, newData); - }); + updateDiv.classList.add("hidden"); - let tagAddBtn = document.getElementById("tagAdd"); - tagAddBtn.addEventListener("click", ()=> { - let tagField = document.getElementById("tag-form"); - if (tagField.value.length > 0) { - let tagLabel = document.createElement("label"); - tagLabel.innerHTML = tagField.value; - tagLabel.setAttribute("class","tag"); - tagLabel.addEventListener("click",()=> { - tagContainer.removeChild(tagLabel); - }); - - tagContainer.append(tagLabel); - tagField.value = ""; - } - }); - } + }); + + let tagAddBtn = document.getElementById("tag-add-btn"); + tagAddBtn.addEventListener("click", ()=> { + let tagField = document.getElementById("tag-form"); + if (tagField.value.length > 0) { + let tagLabel = document.createElement("label"); + tagLabel.innerHTML = tagField.value; + tagLabel.setAttribute("class","tag"); + tagLabel.addEventListener("click",()=> { + tagContainer.removeChild(tagLabel); + }); + + tagContainer.append(tagLabel); + tagField.value = ""; + } + }); }); } diff --git a/source/assets/scripts/ReviewPage.js b/source/assets/scripts/ReviewPage.js deleted file mode 100644 index 89cd440..0000000 --- a/source/assets/scripts/ReviewPage.js +++ /dev/null @@ -1,13 +0,0 @@ -// Run the init() function when the page has loaded -window.addEventListener("DOMContentLoaded", init); - -function init() { - let result = sessionStorage.getItem("currReview"); - - let main = document.querySelector("main"); - - main.innerHTML = result; - let p = document.createElement("p"); - p.innerHTML = JSON.parse(result)["comments"]; - main.append(p); -} diff --git a/source/assets/scripts/appTestHelpers.js b/source/assets/scripts/appTestHelpers.js new file mode 100644 index 0000000..37e7cb4 --- /dev/null +++ b/source/assets/scripts/appTestHelpers.js @@ -0,0 +1,73 @@ +import {strict as assert} from "node:assert"; + +/** + * Fills out a create or update review form + * @param {Object} page the page object which contains the create or update form + * @param {Object} review review data to input into the form + */ +export async function setReviewForm(page, review) { + + // Set text fields + await page.$eval("#mealName", (el, value) => el.value = value, review.mealName); + await page.$eval("#comments", (el, value) => el.value = value, review.comments); + await page.$eval("#restaurant", (el, value) => el.value = value, review.restaurant); + + // Get all tag elements and click them to delete them + let tag_items = await page.$$(".tag"); + if(tag_items !== null){ + for(let i = 0; i < tag_items.length; i++){ + await tag_items[i].click(); + } + } + + // Get the button needed to add new tags + let tag_btn = await page.$("#tag-add-btn"); + for(let i = 0; i < review.tags.length; i++){ + await page.$eval("#tag-form", (el, value) => el.value = value, review.tags[i]); + await tag_btn.click(); + } + + // Select a new rating + let rating_select = await page.$(`#s${review.rating}-select`); + await rating_select.click({delay: 100}); +} + +/** + * Tests a page or shadowDOM for correct element text or src values + * @param {Object} root page or shodowDOM to test + * @param {string} prefix prefix character for element IDs + * @param {Object} expected values for each element + */ +export async function checkCorrectness(root, prefix, expected){ + // Get the review image and check src + let img = await root.$(`#${prefix}-mealImg`); + let imgSrc = await img.getProperty("src"); + // Check src + assert.strictEqual(await imgSrc.jsonValue(), expected.imgSrc); + + // Get the title, comment, and restaurant + let title = await root.$(`#${prefix}-mealName`); + let title_text = await title.getProperty("innerText"); + let comment = await root.$(`#${prefix}-comments`); + let comment_text = await comment.getProperty("innerText"); + let restaurant = await root.$(`#${prefix}-restaurant`); + let restaurant_text = await restaurant.getProperty("innerText"); + + // Check title, comment, and restaurant + assert.strictEqual(await title_text.jsonValue(), expected.mealName); + assert.strictEqual(await comment_text.jsonValue(), expected.comments); + assert.strictEqual(await restaurant_text.jsonValue(), expected.restaurant); + + // Check tags + let tags = await root.$$(`.${prefix}-tag`); + assert.strictEqual(await tags.length, expected.tags.length); + for(let i = 0; i < expected.tags.length; i++){ + let tag_text = await tags[i].getProperty("innerText"); + assert.strictEqual(await tag_text.jsonValue(), expected.tags[i]); + } + + // Check stars + let stars = await root.$(`#${prefix}-rating`); + let stars_src = await stars.getProperty("src"); + assert.strictEqual(await stars_src.jsonValue(), expected.rating); +} \ No newline at end of file diff --git a/source/assets/scripts/localStorage.js b/source/assets/scripts/localStorage.js index 48fc891..cab6447 100644 --- a/source/assets/scripts/localStorage.js +++ b/source/assets/scripts/localStorage.js @@ -1,19 +1,78 @@ /** - * @returns {Array} An array of reviews found in localStorage + * Creates a new review to storage and performs related meta tasks + * @param {Object} review to store + * @return {number} ID of the newly added review */ -export function getReviewsFromStorage() { - let result = JSON.parse(localStorage.getItem("reviews")); - if (result) { - return result; - } - return new Array(0); +export function newReviewToStorage(review){ + //grabbing the nextID, and putting our review object in storage associated with the ID + let nextReviewId = JSON.parse(localStorage.getItem("nextID")); + review["reviewID"] = nextReviewId; + + // set the review entry to the review object + localStorage.setItem(`review${nextReviewId}`, JSON.stringify(review)); + + //updating our activeIDS list + let tempIdArr = JSON.parse(localStorage.getItem("activeIDS")); + tempIdArr.push(nextReviewId); + localStorage.setItem("activeIDS", JSON.stringify(tempIdArr)); + + //increment nextID for next review creation + localStorage.setItem("nextID", JSON.stringify(nextReviewId + 1)); + + return nextReviewId; } /** - * Takes in an array of reviews, converts it to a string, and then - * saves that string to 'reviews' in localStorage - * @param {Array} reviews An array of reviews + * Gets a single review by ID from storage + * @param {string} ID of the review to get + * @returns {Object} review object corresponding to param ID */ -export function saveReviewsToStorage(reviews) { - localStorage.setItem("reviews", JSON.stringify(reviews)); +export function getReviewFromStorage(ID){ + return JSON.parse(localStorage.getItem(`review${ID}`)); } + +/** + * Updates a single review by ID to storage + * @param {string} ID of review to update + * @param {Object} review to store + */ +export function updateReviewToStorage(ID, review){ + // set the review entry with ID to the review object + localStorage.setItem(`review${ID}`, JSON.stringify(review)); +} + +/** + * Deletes a review by ID from storage + * @param {string} ID of the review to delete + */ +export function deleteReviewFromStorage(ID){ + let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); + + for (let i in activeIDS) { + if (activeIDS[i] == ID) { + activeIDS.splice(i,1); + localStorage.setItem("activeIDS", JSON.stringify(activeIDS)); + localStorage.removeItem(`review${ID}`); + return; + } + } + + console.error(`could not find review${ID} in localStorage`); +} + +// legacy function +export function getAllReviewsFromStorage() { + if (!(localStorage.getItem("activeIDS"))) { + // we wanna init the active ID array and start the nextID count + localStorage.setItem("activeIDS", JSON.stringify([])); + localStorage.setItem("nextID", JSON.stringify(0)); + } + //iterate thru activeIDS + let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); + let reviews = []; + for (let i = 0; i < activeIDS.length; i++) { + let currReview = JSON.parse(localStorage.getItem(`review${activeIDS[i]}`)); + reviews.push(currReview); + } + return reviews; +} \ No newline at end of file diff --git a/source/assets/scripts/localStorage.test.js b/source/assets/scripts/localStorage.test.js index 65574b8..b74087d 100644 --- a/source/assets/scripts/localStorage.test.js +++ b/source/assets/scripts/localStorage.test.js @@ -1,48 +1,103 @@ import {strict as assert} from "node:assert"; -import {describe, it, beforeEach} from "mocha"; -import {saveReviewsToStorage, getReviewsFromStorage} from "./localStorage.js"; - -beforeEach(() => { - localStorage.clear(); -}); +import {describe, it, before, after} from "mocha"; +import {newReviewToStorage, getReviewFromStorage, updateReviewToStorage, deleteReviewFromStorage, getAllReviewsFromStorage} from "./localStorage.js"; describe("test app localStorage interaction", () => { - it("get after init", () => { - assert.deepEqual(getReviewsFromStorage(), []); + + before(() => { + localStorage.clear(); }); - it("store one then get", () => { - let reviews = [{ + + it("test localStorage state after init", () => { + assert.deepEqual(getAllReviewsFromStorage(), []); + assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), []); + assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 0); + }); + + it("test localStorage state after adding one review", () => { + let review = { "imgSrc": "sample src", - "imgAlt": "sample alt", "mealName": "sample name", "restaurant": "sample restaurant", "rating": 5, "tags": ["tag 1", "tag 2", "tag 3"] - }]; + }; - saveReviewsToStorage(reviews); - assert.deepEqual(getReviewsFromStorage(), reviews); + newReviewToStorage(review); + + review.reviewID = 0; + + assert.deepEqual(getAllReviewsFromStorage(), [review]); + assert.deepEqual(getReviewFromStorage(0), review); + assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), [0]); + assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1); }); - it("repeated store one more and get", () => { - let reviews = []; - assert.deepEqual(getReviewsFromStorage(), reviews); + it("test localStorage state during adding 999 reviews", () => { + let reviews = getAllReviewsFromStorage(); + let ids = [0]; + + for(let i = 1; i < 1000; i++){ + ids.push(i); + let new_review = { + "imgSrc": `sample src ${i}`, + "mealName": `sample name ${i}`, + "restaurant": `sample restaurant ${i}`, + "rating": i, + "tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] + }; + + newReviewToStorage(new_review); + + new_review.reviewID = i; + reviews.push(new_review); + + assert.deepEqual(getAllReviewsFromStorage(), reviews); + assert.deepEqual(getReviewFromStorage(i), new_review); + assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); + assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), (i+1)); + } + }).timeout(5000); + + it("test localStorage state during updating 1000 reviews", () => { + let reviews = getAllReviewsFromStorage(); + let ids = JSON.parse(localStorage.getItem("activeIDS")); for(let i = 0; i < 1000; i++){ - reviews = getReviewsFromStorage(); - - reviews.push( - { - "imgSrc": `sample src ${i}`, - "imgAlt": `sample alt ${i}`, - "mealName": `sample name ${i}`, - "restaurant": `sample restaurant ${i}`, - "rating": i, - "tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] - } - ); - saveReviewsToStorage(reviews); - assert.deepEqual(getReviewsFromStorage(), reviews); + let new_review = { + "imgSrc": `updated sample src ${i}`, + "mealName": `updated sample name ${i}`, + "restaurant": `updated sample restaurant ${i}`, + "rating": i*2+i, + "tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] + }; + new_review.reviewID = i; + + reviews[i] = new_review; + + updateReviewToStorage(i, new_review); + + assert.deepEqual(getAllReviewsFromStorage(), reviews); + assert.deepEqual(getReviewFromStorage(i), new_review); + assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); + assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1000); } - }).timeout(10000); + }).timeout(5000); + + it("test localStorage state during deleting 1000 reviews", () => { + let reviews = getAllReviewsFromStorage(); + let ids = JSON.parse(localStorage.getItem("activeIDS")); + + for(let i = 999; i >= 0; i--){ + deleteReviewFromStorage(i); + ids.pop(); + reviews.pop(); + + assert.deepEqual(getAllReviewsFromStorage(), reviews); + assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); + assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1000); + } + }).timeout(5000); + + after(() => {}); }); diff --git a/source/assets/scripts/main.e2e.test.js b/source/assets/scripts/main.e2e.test.js new file mode 100644 index 0000000..4181d2f --- /dev/null +++ b/source/assets/scripts/main.e2e.test.js @@ -0,0 +1,232 @@ +import {strict as assert} from "node:assert"; +import {describe, it, before, after} from "mocha"; +import puppeteer from "puppeteer-core"; +import {exit} from "node:process"; +import {setReviewForm, checkCorrectness} from "./appTestHelpers.js"; + +describe("test App end to end", async () => { + + let browser; + let page; + + before(async () => { + let root; + try { + root = process.getuid() == 0; + } + catch (error) { + root = false; + } + + //browser = await puppeteer.launch({headless: false, slowMo: 250, args: root ? ['--no-sandbox'] : undefined}); + browser = await puppeteer.launch({args: root ? ["--no-sandbox"] : undefined}); + page = await browser.newPage(); + try{ + await page.goto("http://localhost:8080", {timeout: 2000}); + await console.log(`✔ connected to localhost webserver as ${root ? "root" : "user"}`); + } + catch (error) { + await console.log("❌ failed to connect to localhost webserver on port 8080"); + await exit(1); + } + }); + + describe("test simple properties", async () => { + it("page should have correct title", async () => { + assert.strictEqual(await page.title(), "Food Journal"); + }); + }); + + describe("test CRUD on simple inputs and default image", () => { + + describe("test create 1 new review", async () => { + it("create 1 new review", async () => { + // Click the button to create a new review + let create_btn = await page.$("#create-btn"); + await create_btn.click(); + await page.waitForNavigation(); + + // create a new review + let review = { + mealName: "sample name", + comments: "sample comment", + restaurant: "sample restaurant", + tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], + rating: 1 + }; + await setReviewForm(page, review); + + // Click the save button to save updates + let save_btn = await page.$("#save-btn"); + await save_btn.click(); + await page.waitForNavigation(); + }); + + it("check details page", async () => { + // check the details page for correctness + let expected = { + imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", + mealName: "sample name", + comments: "sample comment", + restaurant: "sample restaurant", + tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], + rating: "http://localhost:8080/assets/images/1-star.svg" + }; + await checkCorrectness(page, "d", expected); + }); + + it("check home page", async () => { + // Click the button to return to the home page + let home_btn = await page.$("#home-btn"); + home_btn.click(); + await page.waitForNavigation(); + + // Get the review card again and get its shadowRoot + let review_card = await page.$("review-card"); + let shadowRoot = await review_card.getProperty("shadowRoot"); + + let expected = { + imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", + mealName: "sample name", + comments: "sample comment", + restaurant: "sample restaurant", + tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], + rating: "http://localhost:8080/assets/images/1-star.svg" + }; + await checkCorrectness(shadowRoot, "a", expected); + }); + }); + + describe("test read 1 review after refresh", async () => { + it("refresh page", async () => { + // Reload the page + await page.reload({ waitUntil: ["networkidle0", "domcontentloaded"] }); + }); + + it("check details page", async () => { + // click review card + let review_card = await page.$("review-card"); + await review_card.click(); + await page.waitForNavigation(); + + // check the details page for correctness + let expected = { + imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", + mealName: "sample name", + comments: "sample comment", + restaurant: "sample restaurant", + tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], + rating: "http://localhost:8080/assets/images/1-star.svg" + }; + await checkCorrectness(page, "d", expected); + }); + + it("check home page", async () => { + // Click the button to return to the home page + let home_btn = await page.$("#home-btn"); + home_btn.click(); + await page.waitForNavigation(); + + // Get the review card again and get its shadowRoot + let review_card = await page.$("review-card"); + let shadowRoot = await review_card.getProperty("shadowRoot"); + + // check the details page for correctness + let expected = { + imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", + mealName: "sample name", + comments: "sample comment", + restaurant: "sample restaurant", + tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], + rating: "http://localhost:8080/assets/images/1-star.svg" + }; + await checkCorrectness(shadowRoot, "a", expected); + }); + }); + + describe("test update 1 review", async () => { + + it("update 1 review", async () => { + + // Get the only review card and click it + let review_card = await page.$("review-card"); + await review_card.click(); + await page.waitForNavigation(); + + // Click the button to show update form + let update_btn = await page.$("#update-btn"); + await update_btn.click(); + + // create a new review + let review = { + mealName: "updated name", + comments: "updated comment", + restaurant: "updated restaurant", + tags: ["tag -0", "tag -1", "tag -2", "tag -3", "tag -4", "tag -5"], + rating: 5 + }; + await setReviewForm(page, review); + + // Click the save button to save updates + let save_btn = await page.$("#save-btn"); + await save_btn.click(); + await page.waitForNavigation(); + }).timeout(10000); + + it("check details page", async () => { + // check the details page for correctness + let expected = { + imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", + mealName: "updated name", + comments: "updated comment", + restaurant: "updated restaurant", + tags: ["tag -0", "tag -1", "tag -2", "tag -3", "tag -4", "tag -5"], + rating: "http://localhost:8080/assets/images/5-star.svg" + }; + await checkCorrectness(page, "d", expected); + }); + + it("check home page", async () => { + // Click the button to return to the home page + let home_btn = await page.$("#home-btn"); + home_btn.click(); + await page.waitForNavigation(); + + // Get the review card again and get its shadowRoot + let review_card = await page.$("review-card"); + let shadowRoot = await review_card.getProperty("shadowRoot"); + + // check the details page for correctness + let expected = { + imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", + mealName: "updated name", + comments: "updated comment", + restaurant: "updated restaurant", + tags: ["tag -0", "tag -1", "tag -2", "tag -3", "tag -4", "tag -5"], + rating: "http://localhost:8080/assets/images/5-star.svg" + }; + await checkCorrectness(shadowRoot, "a", expected); + }); + + }); + + describe("test delete 1 review", async () => { + it("delete 1 review", async () => { + // Get the only review card and click it + let review_card = await page.$("review-card"); + await review_card.click(); + await page.waitForNavigation(); + + page.on("dialog", async dialog => { + console.log(dialog.message()); + await dialog.accept(); + }); + }); + }); + }); + + after(async () => { + await page.close(); + await browser.close(); + }); +}); \ No newline at end of file diff --git a/source/assets/scripts/main.js b/source/assets/scripts/main.js index 9d37c44..f012cb9 100644 --- a/source/assets/scripts/main.js +++ b/source/assets/scripts/main.js @@ -1,28 +1,29 @@ // main.js -import {getReviewsFromStorage, saveReviewsToStorage} from "./localStorage.js"; +import {getAllReviewsFromStorage} from "./localStorage.js"; // Run the init() function when the page has loaded window.addEventListener("DOMContentLoaded", init); function init() { // Get the reviews from localStorage - let reviews = getReviewsFromStorage(); + let reviews = getAllReviewsFromStorage(); // Add each reviews to the
element addReviewsToDocument(reviews); // Add the event listeners to the form elements initFormHandler(); } + /** * @param {Array} reviews An array of reviews */ function addReviewsToDocument(reviews) { - let mainEl = document.querySelector("main"); + let reviewBox = document.getElementById("review-container"); reviews.forEach(review => { let newReview = document.createElement("review-card"); newReview.data = review; //TODO: want to append it to whatever the box is in layout - mainEl.append(newReview); + reviewBox.append(newReview); }); } @@ -33,87 +34,9 @@ function addReviewsToDocument(reviews) { */ function initFormHandler() { - /* //btn to create form (could be its own function?) - let createBtn = document.getElementById("create"); + let createBtn = document.getElementById("create-btn"); createBtn.addEventListener("click", function(){ window.location.assign("./CreatePage.html"); - });*/ - - //accessing form components - let tagContainer = document.getElementById("tag-container-form"); - let form = document.querySelector("form"); - - form.addEventListener("submit", function(){ - /* - * User submits the form for their review. - * We create reviewCard and put in storage - */ - let formData = new FormData(form); - let reviewObject = {}; - for (let [key, value] of formData) { - console.log(`${key}`); - console.log(`${value}`); - if (`${key}` !== "tag-form") { - reviewObject[`${key}`] = `${value}`; - } - } - reviewObject["tags"] = []; - - let tags = document.querySelectorAll(".tag"); - for(let i = 0; i < tags.length; i ++) { - reviewObject["tags"].push(tags[i].innerHTML); - tagContainer.removeChild(tags[i]); - } - - - let newReview = document.createElement("review-card"); - newReview.data = reviewObject; - - //TODO: want to append it to whatever the box is in layout - let mainEl = document.querySelector("main"); - mainEl.append(newReview); - - let storedReviews = getReviewsFromStorage(); - storedReviews.push(reviewObject); - saveReviewsToStorage(storedReviews); - document.getElementById("new-food-entry").reset(); }); - - // DEV-MODE: for testing purposes - let clearBtn = document.querySelector(".danger"); - clearBtn.addEventListener("click", function() { - localStorage.clear(); - let mainEl = document.querySelector("main"); - while (mainEl.firstChild) { - mainEl.removeChild(mainEl.firstChild); - } - let deleteTags = document.querySelectorAll(".tag"); - for(let i = 0; i < deleteTags.length; i ++) { - tagContainer.removeChild(deleteTags[i]); - } - - //clears reviews AS WELL as resets form - document.getElementById("new-food-entry").reset(); - - - }); - - let tagAddBtn = document.getElementById("tagAdd"); - tagAddBtn.addEventListener("click", ()=> { - let tagField = document.getElementById("tag-form"); - if (tagField.value.length > 0) { - let tagLabel = document.createElement("label"); - tagLabel.innerHTML = tagField.value; - tagLabel.setAttribute("class","tag"); - tagLabel.addEventListener("click",()=> { - tagContainer.removeChild(tagLabel); - }); - - tagContainer.append(tagLabel); - tagField.value = ""; - - } - }); - } diff --git a/source/index.html b/source/index.html index 4418ea0..f4f96e4 100644 --- a/source/index.html +++ b/source/index.html @@ -1,79 +1,57 @@ - - - - - Food Journal + + + + + Food Journal - - + + + + - - - - - + + + + + + + +
+
+ logo +

Food Journal

+ logo +
+
+
+
+
+
+ +
+ CREATE +

Recent Reviews

+ + +
- - -
- -
- -
-
- Pic: - - -
-
- - Meal: - - -
- -
- Rating: - - - - - -
- -
- Other Info: - - - -
- - -
- +
+
+
+
+
+
+ diff --git a/source/review.html b/source/review.html deleted file mode 100644 index b9894ad..0000000 --- a/source/review.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - Food Journal - - - - -

Current Review:

-
-
- - \ No newline at end of file diff --git a/source/static/CoveredByYourGrace-Regular.ttf b/source/static/CoveredByYourGrace-Regular.ttf new file mode 100644 index 0000000..5ceba98 Binary files /dev/null and b/source/static/CoveredByYourGrace-Regular.ttf differ diff --git a/source/static/CreatePage.css b/source/static/CreatePage.css index c347a04..0fee1bc 100644 --- a/source/static/CreatePage.css +++ b/source/static/CreatePage.css @@ -1,83 +1,30 @@ /* CreatePage.css */ -* { - font-family: sans-serif; +@font-face { + font-family: testFont; + src: url("CoveredByYourGrace-Regular.ttf"); } body { - height: 100%; - width: 100%; + background-color: #f8f3f1; } -fieldset { - border: 2px solid rgb(214 214 214); - box-sizing: border-box; - display: block; - width: max-content; +.top-bar { + display: flex; + justify-content: center; } -form button { - display: block; - margin-top: 5px; +.top-bar > img { + position: relative; + align-self: center; + padding-left: 2.5%; + padding-right: 2.5%; } -label[for="ingredients"] p { - margin: 0; -} - -label[for="numRatings"] { - margin: 10px 0 0; -} - -label[for^="rating"] { - padding-right: 10px; -} - -label:not([for^="rating"]) { - display: block; - margin-bottom: 5px; -} - -main { - column-gap: 10px; - display: flex; - flex-wrap: wrap; - height: auto; - max-width: 660px; - row-gap: 10px; - width: 100%; -} - -.tag-container { - display: flex; - flex-flow: row wrap; -} - -.tag { - background-color: grey; - border-radius: 7px; - color: white; - padding-right: 7px; - padding-left: 7px; - margin: 3px; -} - -.tag::before { - display: inline-block; - content: "x"; - height: 15px; - width: 15px; - margin-right: 4px; - text-align: center; - color: white; - cursor: pointer; -} - -.tag:hover::before { - color: red; -} - -.danger { - background-color: rgb(254 171 171); - border-color: red; +.top-bar > h1 { + position: relative; + text-align: center; + color: #516754; + font-size: 6rem; + font-family: testFont, sans-serif; } diff --git a/source/static/Form.css b/source/static/Form.css new file mode 100644 index 0000000..3ca3c7a --- /dev/null +++ b/source/static/Form.css @@ -0,0 +1,153 @@ +@font-face { + font-family: testFont; + src: url("CoveredByYourGrace-Regular.ttf"); +} + +.journal-form h1 { + font-family: testFont, sans-serif; + text-align: center; +} + +.journal-form { + font-size: 120%; + font-family: "Century Gothic", sans-serif; + width: 50%; + margin: auto; + color: #516754; + border: 2px solid rgb(31 41 32); + border-radius: 8px; + background-color: #f7dfd5; +} + +fieldset { + border: none; +} + +input[type="text"] { + width: 100%; + box-sizing: border-box; + background-color: #f7dfd5; + border: none; + border-bottom: 1px solid rgb(0 0 0); +} + +input[type="text"]:focus { + outline: none; + border-bottom: 1px solid rgb(0 0 0); +} + +.rating { + display: flex; + flex-flow: nowrap row-reverse; +} + +.hidden, +.rating:not(:checked) > input { /* Hide radio circles while star rating */ + display: none; +} + +/* Unchecked stars */ +.rating:not(:checked) > label { + /* Make stars line up sideways and not vertically */ + float: right; + + /* Hide label text */ + width: 1em; + overflow: hidden; + white-space: nowrap; + + /* Star default color and size */ + font-size: 200%; + line-height: 1.2; + color: #b3b3cc; +} + +.rating > label:active { + position: relative; +} + +.rating:not(:checked) > label::before { + content: "★"; +} + +/* Checked star color */ +.rating > input:checked ~ label { + color: #ffbf00; +} + +.rating:not(:checked) > label:hover, +.rating:not(:checked) > label:hover ~ label { + color: orangered; +} + +.rating > input:checked + label:hover, +.rating > input:checked ~ label:hover, +.rating > input:checked + label:hover ~ label, +.rating > input:checked ~ label:hover ~ label, +.rating > label:hover ~ input:checked ~ label { + color: orangered; +} + +.tag-container { + display: flex; + flex-flow: wrap row; +} + +.tag { + background-color: grey; + border-radius: 7px; + color: white; + padding-right: 7px; + padding-left: 7px; + margin: 3px; +} + +.tag::before { + display: inline-block; + content: "x"; + height: 15px; + width: 15px; + margin-right: 4px; + text-align: center; + color: white; + cursor: pointer; +} + +.tag:hover::before { + color: red; +} + +#tag-add-btn { + background-color: #94da97; /* Green */ + border: round; + color: rgb(206 83 179); + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 20px; + cursor: pointer; + border-radius: 10%; + margin-top: 5px; +} + +#tag-add-btn:hover { + background-color: rgb(206 83 179); /* Green */ + border: round; + color: #94da97; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 20px; + cursor: pointer; + border-radius: 10%; + margin-top: 5px; +} + +.hidden { + display: none; +} + +.tag-container * { + max-width: 100%; + overflow-wrap: anywhere; +} diff --git a/source/static/OFL.txt b/source/static/OFL.txt new file mode 100644 index 0000000..06117d9 --- /dev/null +++ b/source/static/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/source/static/ReviewCard.css b/source/static/ReviewCard.css deleted file mode 100644 index 2c69f0b..0000000 --- a/source/static/ReviewCard.css +++ /dev/null @@ -1,83 +0,0 @@ -/* main.css */ - -* { - font-family: sans-serif; -} - -body { - height: 100%; - width: 100%; -} - -fieldset { - border: 2px solid rgb(214 214 214); - box-sizing: border-box; - display: block; - width: max-content; -} - -form button { - display: block; - margin-top: 5px; -} - -label[for="ingredients"] p { - margin: 0; -} - -label[for="numRatings"] { - margin: 10px 0 0; -} - -label[for^="rating"] { - padding-right: 10px; -} - -label:not([for^="rating"]) { - display: block; - margin-bottom: 5px; -} - -main { - column-gap: 10px; - display: flex; - flex-wrap: wrap; - height: auto; - max-width: 660px; - row-gap: 10px; - width: 100%; -} - -.tag-container { - display: flex; - flex-flow: row wrap; -} - -.tag { - background-color: grey; - border-radius: 7px; - color: white; - padding-right: 7px; - padding-left: 7px; - margin: 3px; -} - -.tag::before { - display: inline-block; - content: "x"; - height: 15px; - width: 15px; - margin-right: 4px; - text-align: center; - color: white; - cursor: pointer; -} - -.tag:hover::before { - color: red; -} - -.danger { - background-color: rgb(254 171 171); - border-color: red; -} diff --git a/source/static/ReviewDetails.css b/source/static/ReviewDetails.css new file mode 100644 index 0000000..4b92b4d --- /dev/null +++ b/source/static/ReviewDetails.css @@ -0,0 +1,45 @@ +/* ReviewDetails.css */ + +@font-face { + font-family: testFont; + src: url("CoveredByYourGrace-Regular.ttf"); +} + +body { + background-color: #f8f3f1; +} + +h1 { + text-align: center; +} + +.top-bar { + display: flex; + justify-content: center; +} + +.top-bar > img { + position: relative; + align-self: center; + padding-left: 2.5%; + padding-right: 2.5%; +} + +.top-bar > h1 { + position: relative; + text-align: center; + + /* color: #e4c3d2; */ + color: #516754; + font-size: 6rem; + font-family: testFont, sans-serif; +} + +.d-tag { + background-color: grey; + border-radius: 7px; + color: white; + padding-right: 7px; + padding-left: 7px; + margin: 10px; +} diff --git a/source/static/homepage.css b/source/static/homepage.css new file mode 100644 index 0000000..ff575b4 --- /dev/null +++ b/source/static/homepage.css @@ -0,0 +1,93 @@ +/* homepage.css */ + +@font-face { + font-family: testFont; + src: url("CoveredByYourGrace-Regular.ttf"); +} + +/* Color */ +body { + /* background-color: #97a5bd */ + + /* background-color: #E3E3EC; */ + background-color: #f8f3f1; +} + +.top-bar { + display: flex; + justify-content: center; +} + +.top-bar > img { + position: relative; + align-self: center; + padding-left: 2.5%; + padding-right: 2.5%; +} + +.top-bar > h1 { + position: relative; + text-align: center; + + /* color: #e4c3d2; */ + + /* color: rgb(145, 124, 175); */ + color: #516754; + font-size: 6rem; + font-family: testFont, sans-serif; +} + +.body-container { + display: flex; + max-height: 100%; +} + +.center-display { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.search-bar { + display: flex; + justify-content: center; +} + +.search-bar > form { + float: right; + padding: 6px 10px; + margin-top: 8px; + margin-right: 16px; + background: rgb(239 183 183); + font-size: 17px; + border: none; + border-radius: 12px; + cursor: pointer; +} + +#recent-reviews-text { + text-align: center; + font-size: 4rem; + color: #516754; + font-family: testFont, sans-serif; +} + +img#create-btn { + position: relative; + align-self: center; + padding-left: 2.5%; + padding-right: 2.5%; +} + +.review-container { + display: flex; + position: relative; + flex-wrap: wrap; + justify-content: center; +} + +.review-container > div { + background-color: #f1f1f1; + text-align: center; +} diff --git a/source/static/reset.css b/source/static/reset.css deleted file mode 100644 index 6677f89..0000000 --- a/source/static/reset.css +++ /dev/null @@ -1,158 +0,0 @@ -/* This is a generic CSS file that sets preliminary rules for content that should be the same across pages */ - -html, -body, -div, -span, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -abbr, -address, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -samp, -small, -strong, -sub, -sup, -var, -b, -i, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; -} - -body { - line-height: 1; -} - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} - -nav ul { - list-style: none; -} - -blockquote, -q { - quotes: none; -} - -blockquote::before, -blockquote::after, -q::before, -q::after { - content: ""; - content: none; -} - -a { - margin: 0; - padding: 0; - font-size: 100%; - vertical-align: baseline; - background: transparent; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -input, -select { - vertical-align: middle; -} - -img, -fieldset, -object { - border: none; -} - -*, -*::after, -*::before { - box-sizing: border-box; -} - -button, -label { - cursor: pointer; -} - -html, -body { - height: 100%; -} - -form { - border: solid; -} diff --git a/specs/adrs/111622-e2etesting-puppeteer.md b/specs/adrs/111622-e2etesting-puppeteer.md new file mode 100644 index 0000000..4a1d3ce --- /dev/null +++ b/specs/adrs/111622-e2etesting-puppeteer.md @@ -0,0 +1,19 @@ +# Use puppeteer for JS unit testing framework + +- Status: accept +- Deciders: Arthur Lu, Marc Reta +- Date: 11 / 16 / 22 + +## Decision Drivers + +- Need end to end testing framework which runs headlessly and quickly +- Framework should integrate well with Mocha, the existing unit testing framework +- Framework should be easy to implement end to end tests with + +## Considered Options +- puppeteer +- selenium-webdriver + +## Decision Outcome + +Chosen Option: Puppeteer for its ease of use with mocha. \ No newline at end of file