diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml deleted file mode 100644 index 8003d23..0000000 --- a/.github/workflows/prettier.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Prettier - -on: - pull_request: - branches: - - main - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - # Single deploy job since we're just deploying - test: - runs-on: ubuntu-latest - steps: - - name: Install apt updates - run: sudo apt -y update; sudo apt -y upgrade; - - name: Install prerequisites - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Checkout - 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 lint-prettier \ No newline at end of file diff --git a/admin/cipipeline/phase2.drawio.png b/admin/cipipeline/phase2.drawio.png new file mode 100644 index 0000000..625f966 Binary files /dev/null and b/admin/cipipeline/phase2.drawio.png differ diff --git a/admin/cipipeline/phase2.md b/admin/cipipeline/phase2.md new file mode 100644 index 0000000..5075e7c --- /dev/null +++ b/admin/cipipeline/phase2.md @@ -0,0 +1,47 @@ +# CI/CD Phase 2 + +## Overall Pipeline Architecture + +Rather than create one large pipeline with many steps which increases complexity, we decided to create many small independent pipelines which work in parallel to conduct code quality checking. Using this strategy, if any one pipeline has issues, we can still continue development without delay, and the quality of code is likely to remain high. + +## Overview of Pipeline Features + +We've identified 5 major features which we definitely want to implement in the CI/CD pipeline. + +- Deployment +- Unit Testing +- Linting +- End To End Validation +- Manual Validation + +We also identified some features which are nice to have: +- Automatic documentation publishing +- Minification +- HTML Validation and accessibility scoring + +We created this diagram to demonstrate our strategy of multiple simple pipelines. + +![Pipeline Diagram](phase2.drawio.png) + +## Finished Features and Implementation + +So far the features listed below have been completed to some degree: + +- Deployment + - Implemented: action triggered on any push to main, uses the github pages action to publish the app + - Implemented: uses JSDoc to generate documentation on the same site at [docs](https://cse110-fa22-group29.github.io/cse110-fa22-group29/docs/) + - ToDo: Add minification step between trigger and github pages action +- Unit Testing + - Implemented: action triggers on PR to main, uses mocha to perform unit testing on core components +- End to end testing + - Implemented: action triggers on PR to main, uses mocha and puppeteer to perform end to end testing +- Linting (JS) + - Implemented: action triggers on PR to main, uses eslint to perform style enforcement on all JS components +- Linting (HTML) + - Implemented: action triggers on PR to main, uses HTMLhint to perform style enforcement on all HTML components +- Linting (CSS) + - Implemented: action triggers on PR to main, uses Stylelint to perform style enforcement on all CSS components +- Linting (general) + - Implemented: action triggers on PR to main, uses Prettier to perform style checking on all file types + +## Planned Features and Timeline diff --git a/admin/cipipeline/phase2.mp4 b/admin/cipipeline/phase2.mp4 new file mode 100644 index 0000000..2387d61 Binary files /dev/null and b/admin/cipipeline/phase2.mp4 differ diff --git a/admin/meetings/112922-Sprint3Opener.md b/admin/meetings/112922-Sprint3Opener.md new file mode 100644 index 0000000..dde1ac3 --- /dev/null +++ b/admin/meetings/112922-Sprint3Opener.md @@ -0,0 +1,35 @@ +# Meeting Minutes (11/29/2022) +## Team 29: Hackers1995 +## Meeting Topic: Sprint 3 Debut Meeting + +## Attendance +1. Rhea Bhutada +2. George Dubinin +3. Gavyn Ezell +4. Henry Feng +5. Kara Hoagland +6. Marc Reta (remote) +7. Sanjit Joseph +8. Daniel Hernandez +9. Arthur Lu (remote) + +## Meeting Details +- When: 11/29/2022 at 5:00PM +- Where: Design and Innovation Building + +## Agenda: +- ### Old/Unresolved Business + - Resolve pretty print linting PR + - Resolve documentation not being merged to main PR +- ### New Business + - Create ADR for image storage + - Review sorting defaults (recent or top rated) + - Adding lists of reviewIDs corresponding to reviews which share specific star ratings + - Create function for retrieving top 20 reviews organized by decreasing star ratings + - Implement search for for flitering tags + - Frontend checked out new branch for alternate home page designs +- ### Next Meeting's Business + - Creation of team status video + +## End Time +- 11/20/2022 at 3:00PM \ No newline at end of file diff --git a/admin/meetings/113022-Sprint3Cont.md b/admin/meetings/113022-Sprint3Cont.md new file mode 100644 index 0000000..5689e85 --- /dev/null +++ b/admin/meetings/113022-Sprint3Cont.md @@ -0,0 +1,30 @@ +# Meeting Minutes (11/30/2022) +## Team 29: Hackers1995 +## Meeting Topic: Sprint 3 Continued + +## Attendance +1. Rhea Bhutada +2. George Dubinin +4. Henry Feng +5. Kara Hoagland +7. Sanjit Joseph +9. Arthur Lu + +## Meeting Details +- When: 11/30/2022 at 2:00PM +- Where: Design and Innovation Building + +## Agenda: +- ### Old/Unresolved Business + - Fix empty page for no-tag search + - Catch testing up with what we implemented yesterday +- ### New Business + - Cache the site for local first (high priority) + - Implement editing form "in place" (optional for this sprint) + - Change icon for "add review" entry + - Overcoming UI test challenges +- ### Next Meeting's Business + - Creation of team status video + +## End Time +- 11/30/2022 at 2:00PM \ No newline at end of file diff --git a/admin/meetings/1212022-check10.md b/admin/meetings/1212022-check10.md new file mode 100644 index 0000000..80a9937 --- /dev/null +++ b/admin/meetings/1212022-check10.md @@ -0,0 +1,22 @@ +# Meeting Minutes (12/1/2022) +## Team 29: Hackers1995 +## Meeting Topic: Weekly TA Catchup with Gagan +We are meeting with Gagan to discuss status video and general updates on project. + +## Attendance +1. Rhea Bhutada +2. George Dubinin +3. Gagan Gopalaiah + +## Meeting Details +- When: 12/1/2022 at 12:00PM +- Where: Zoom + +## Discussion Points by Gagan +- Don't code up anything after Sunday. Reserve the time for bug fixes +- Final Interview is a 4-5 minute interview about general course specific topics +- Live demo of the app for Gagan +- Gagan asks us to evaluate instructional assistants + +## End Time +- 12/1/2022 at 12:20PM \ No newline at end of file diff --git a/package.json b/package.json index 66b994d..13fd016 100644 --- a/package.json +++ b/package.json @@ -24,4 +24,4 @@ "stylelint": "14.14.1", "stylelint-config-standard": "^29.0.0" } -} +} \ No newline at end of file diff --git a/source/ReviewDetails.html b/source/ReviewDetails.html index d58bf69..ed1323e 100644 --- a/source/ReviewDetails.html +++ b/source/ReviewDetails.html @@ -7,7 +7,7 @@ Food Journal - + diff --git a/source/assets/images/Grouppink.png b/source/assets/images/Grouppink.png deleted file mode 100644 index d4f1d14..0000000 Binary files a/source/assets/images/Grouppink.png and /dev/null differ diff --git a/source/assets/images/create_button.png b/source/assets/images/create_button.png new file mode 100644 index 0000000..3a96529 Binary files /dev/null and b/source/assets/images/create_button.png differ diff --git a/source/assets/images/plate_with_chopsticks.png b/source/assets/images/plate_with_chopsticks.png deleted file mode 100644 index 023e43e..0000000 Binary files a/source/assets/images/plate_with_chopsticks.png and /dev/null differ diff --git a/source/assets/images/plate_with_cutlery.png b/source/assets/images/plate_with_cutlery.png deleted file mode 100644 index 6f7a938..0000000 Binary files a/source/assets/images/plate_with_cutlery.png and /dev/null differ diff --git a/source/assets/images/search_button.png b/source/assets/images/search_button.png new file mode 100644 index 0000000..66a1a85 Binary files /dev/null and b/source/assets/images/search_button.png differ diff --git a/source/assets/scripts/CreatePage.js b/source/assets/scripts/CreatePage.js index 526a766..83bb776 100644 --- a/source/assets/scripts/CreatePage.js +++ b/source/assets/scripts/CreatePage.js @@ -159,10 +159,10 @@ function initFormHandler() { let tagLabel = document.createElement("label"); tagLabel.innerHTML = tagField.value; tagLabel.setAttribute("class","tag"); - tagSet.add(tagField.value.toLowerCase()); + tagSet.add(tagSetVal); tagLabel.addEventListener("click",()=> { tagContainer.removeChild(tagLabel); - tagSet.delete(tagField.value.toLowerCase()); + tagSet.delete(tagSetVal); }); tagContainer.append(tagLabel); diff --git a/source/assets/scripts/ReviewCard.js b/source/assets/scripts/ReviewCard.js index ad339bc..91a7a70 100644 --- a/source/assets/scripts/ReviewCard.js +++ b/source/assets/scripts/ReviewCard.js @@ -17,6 +17,7 @@ class ReviewCard extends HTMLElement { margin: 0; padding: 0; overflow-wrap: anywhere; + cursor: pointer; } a { @@ -149,7 +150,7 @@ class ReviewCard extends HTMLElement { // Image setup let mealImg = document.createElement("img"); - mealImg.setAttribute("id", "a-mealImg"); + mealImg.setAttribute("id", "a-meal-img"); mealImg.setAttribute("alt","Meal Photo Corrupted"); mealImg.setAttribute("src",data["mealImg"]); mealImg.addEventListener("error", function(e) { @@ -157,11 +158,11 @@ class ReviewCard extends HTMLElement { e.onerror = null; }); - //meal name setup + // Meal name setup let meallabelDiv = document.createElement("div"); meallabelDiv.setAttribute("class", "meal-name-div"); let mealLabel = document.createElement("label"); - mealLabel.setAttribute("id", "a-mealName"); + mealLabel.setAttribute("id", "a-meal-name"); mealLabel.setAttribute("class","meal-name"); mealLabel.innerHTML = data["mealName"]; meallabelDiv.append(mealLabel); @@ -188,7 +189,7 @@ class ReviewCard extends HTMLElement { starsImg.setAttribute("num", data["rating"]); ratingDiv.append(starsImg); - //added tags + // Tags setup let tagContainerDiv = document.createElement("div"); tagContainerDiv.setAttribute("class", "tag-container-div"); let tagContainer = document.createElement("div"); @@ -244,12 +245,12 @@ class ReviewCard extends HTMLElement { // Getting the article elements for the review card dataContainer["reviewID"] = this.reviewID; - // Get image - let mealImg = this.shadowEl.getElementById("a-mealImg"); + //get image + let mealImg = this.shadowEl.getElementById("a-meal-img"); dataContainer["mealImg"] = mealImg.getAttribute("src"); - // Get meal name - let mealLabel = this.shadowEl.getElementById("a-mealName"); + //get meal name + let mealLabel = this.shadowEl.getElementById("a-meal-name"); dataContainer["mealName"] = mealLabel.innerHTML; // Get comment section diff --git a/source/assets/scripts/ReviewDetails.js b/source/assets/scripts/ReviewDetails.js index 5c056c1..ceaea94 100644 --- a/source/assets/scripts/ReviewDetails.js +++ b/source/assets/scripts/ReviewDetails.js @@ -58,7 +58,7 @@ function setupInfo(){ } /** - * Sets up delete button to delete review from storage and switch to homepage + * Sets up delete button to delete reveiw from storage and switch to homepage. */ function setupDelete(){ let deleteBtn = document.getElementById("delete-btn"); @@ -73,7 +73,7 @@ function setupDelete(){ } /** - * Sets up update button to reveal form and update info in storage and the current page + * Sets up update button to reveal form and update info in storage and the current page. */ function setupUpdate(){ let updateBtn = document.getElementById("update-btn"); @@ -102,8 +102,10 @@ function setupUpdate(){ while (tagContainer.firstChild) { tagContainer.removeChild(tagContainer.firstChild); } + + let tagSetVal; for (let i = 0; i < currReview["tags"].length; i++) { - let tagSetVal = currReview["tags"][i].toLowerCase() + tagSetVal = currReview["tags"][i].toLowerCase(); tagSet.add(tagSetVal); let newTag = document.createElement("label"); newTag.setAttribute("class","tag"); @@ -242,7 +244,7 @@ function setupUpdate(){ }); - //adding tag to form functionality + // Adding tag to form functionality let tagAddBtn = document.getElementById("tag-add-btn"); tagAddBtn.addEventListener("click", ()=> { let tagField = document.getElementById("tag-form"); diff --git a/source/assets/scripts/appTestHelpers.js b/source/assets/scripts/appTestHelpers.js index 37e7cb4..aa4b1db 100644 --- a/source/assets/scripts/appTestHelpers.js +++ b/source/assets/scripts/appTestHelpers.js @@ -40,13 +40,13 @@ export async function setReviewForm(page, review) { */ export async function checkCorrectness(root, prefix, expected){ // Get the review image and check src - let img = await root.$(`#${prefix}-mealImg`); + let img = await root.$(`#${prefix}-meal-img`); 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 = await root.$(`#${prefix}-meal-name`); let title_text = await title.getProperty("innerText"); let comment = await root.$(`#${prefix}-comments`); let comment_text = await comment.getProperty("innerText"); diff --git a/source/assets/scripts/localStorage.js b/source/assets/scripts/localStorage.js index d0278e3..093e711 100644 --- a/source/assets/scripts/localStorage.js +++ b/source/assets/scripts/localStorage.js @@ -13,6 +13,14 @@ export function newReviewToStorage(review){ // adding to the tag keys addTagsToStorage(nextReviewId, review["tags"]); + + //adding to the star storage + let starArr = JSON.parse(localStorage.getItem(`star${review["rating"]}`)); + if(!starArr){ + starArr = []; + } + starArr.push(nextReviewId); + localStorage.setItem(`star${review["rating"]}`, JSON.stringify(starArr)); //updating our activeIDS list let tempIdArr = JSON.parse(localStorage.getItem("activeIDS")); @@ -41,6 +49,70 @@ export function getReviewFromStorage(ID){ */ export function updateReviewToStorage(ID, review){ let oldReview = JSON.parse(localStorage.getItem(`review${ID}`)); + let starArr = JSON.parse(localStorage.getItem(`star${review["rating"]}`)); + + //activeID update recency + let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); + for (let i in activeIDS){ + if(activeIDS[i] == ID){ + activeIDS.splice(i,1); + activeIDS.push(ID); + break; + } + } + localStorage.setItem("activeIDS", JSON.stringify(activeIDS)); + + //star local storage update + if(oldReview["rating"] !== review["rating"]){ + //first delete from previous rating array in storage + let oldStarArr = JSON.parse(localStorage.getItem(`star${oldReview["rating"]}`)); + for (let i in oldStarArr) { + if (oldStarArr[i] == ID) { + //removing from corresponding rating array and updating local Storage + oldStarArr.splice(i,1); + break; + } + } + if(oldStarArr.length != 0){ + localStorage.setItem(`star${oldReview["rating"]}`, JSON.stringify(oldStarArr)); + } else { + localStorage.removeItem(`star${oldReview["rating"]}`); + } + //then add ID to array corresponding to new review rating + let newStarArr = starArr; + if(!newStarArr){ + newStarArr = []; + } + newStarArr.push(ID); + localStorage.setItem(`star${review["rating"]}`, JSON.stringify(newStarArr)); + } else if(starArr.length !== 1) { + //stars update recency if unchanged + for (let i in starArr){ + if(starArr[i] == ID) { + starArr.splice(i,1); + starArr.push(ID); + break; + } + } + localStorage.setItem(`star${review["rating"]}`, JSON.stringify(starArr)); + } + + //specifically the unchanged tags update recency + let repeatedTags = review["tags"].filter(x => oldReview["tags"].includes(x)); + let tagArr = []; + for (let i in repeatedTags){ + tagArr = JSON.parse(localStorage.getItem(`!${repeatedTags[i]}`)); + if(tagArr.length == 1){ + for (let j in tagArr){ + if(tagArr[j] == ID){ + tagArr.splice(j,1); + tagArr.push(ID); + break; + } + } + localStorage.setItem(`!${repeatedTags[i]}`, JSON.stringify(tagArr)); + } + } //Get diff of tags and update storage let deletedTags = oldReview["tags"].filter(x => !review["tags"].includes(x)); @@ -57,12 +129,29 @@ export function updateReviewToStorage(ID, review){ * @param {string} ID of the review to delete */ export function deleteReviewFromStorage(ID){ + //removing id number from activeIDS and star{rating} let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); - + let reviewRating = JSON.parse(localStorage.getItem(`review${ID}`))["rating"]; + let starArr = JSON.parse(localStorage.getItem(`star${reviewRating}`)); + + for (let i in starArr) { + if (starArr[i] == ID) { + //removing from corresponding rating array and updating local Storage + starArr.splice(i,1); + break; + } + } + if(starArr.length != 0){ + localStorage.setItem(`star${reviewRating}`, JSON.stringify(starArr)); + } else { + localStorage.removeItem(`star${reviewRating}`); + } + for (let i in activeIDS) { if (activeIDS[i] == ID) { activeIDS.splice(i,1); localStorage.setItem("activeIDS", JSON.stringify(activeIDS)); + let currReview = JSON.parse(localStorage.getItem(`review${ID}`)); deleteTagsFromStorage(ID, currReview["tags"]); localStorage.removeItem(`review${ID}`); @@ -115,24 +204,9 @@ function addTagsToStorage(ID, addedTags) { } /** - * Returns the top n reviews by ID. If there are less than n reviews, returns the most possible. - * @param {number} n number of reviews to return - * @returns {Object} list of n reviews that are the top rated + * Test Helper Function to get all reviews from local storage + * @returns {Object} all active reviews from local storage */ -export function getTopReviewsFromStorage(n) { - -} - -/** - * Returns all reviews which contain the same tag specified. - * @param {string} tag to filter by - * @returns {Object} list of reviews that all contain the specified tag - */ -export function getReviewsByTag(tag) { - -} - -// legacy function export function getAllReviewsFromStorage() { if (!(localStorage.getItem("activeIDS"))) { // we wanna init the active ID array and start the nextID count @@ -147,4 +221,47 @@ export function getAllReviewsFromStorage() { reviews.push(currReview); } return reviews; +} + +/** + * Get all IDs of active reviews (order: most recent) + * @returns {number[]} list of all active IDs by recency + */ +export function getIDsFromStorage() { + 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)); + } + let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); + return activeIDS.reverse(); +} + +/** + * Returns all review IDs which contain the same tag specified (order: most recent) + * @param {string} tag to filter by + * @returns {number[]} list of IDs of reviews that all contain the specified tag by recency + */ +export function getIDsByTag(tag) { + let tagArr = JSON.parse(localStorage.getItem("!" + tag.toLowerCase())); + if(!tagArr){ + tagArr = []; + } + return tagArr.reverse(); +} + +/** + * Returns the top rated review IDs in order. + * @returns {number[]} list of IDs of reviews in order of top rating (most recent if equal rating) + */ +export function getTopIDsFromStorage() { + let resultArr = []; + for(let i = 5; i > 0; i--){ + let starArr = JSON.parse(localStorage.getItem(`star${i}`)); + if(!starArr){ + continue; + } + resultArr = resultArr.concat(starArr.reverse()); + } + return resultArr; } \ No newline at end of file diff --git a/source/assets/scripts/localStorage.test.js b/source/assets/scripts/localStorage.test.js index 3330ba0..a365c59 100644 --- a/source/assets/scripts/localStorage.test.js +++ b/source/assets/scripts/localStorage.test.js @@ -1,6 +1,6 @@ import {strict as assert} from "node:assert"; import {describe, it, before, after} from "mocha"; -import {newReviewToStorage, getReviewFromStorage, updateReviewToStorage, deleteReviewFromStorage, getAllReviewsFromStorage, getTopReviewsFromStorage, getReviewsByTag} from "./localStorage.js"; +import {newReviewToStorage, getReviewFromStorage, updateReviewToStorage, deleteReviewFromStorage, getAllReviewsFromStorage, getIDsByTag, getTopIDsFromStorage} from "./localStorage.js"; describe("test CRUD localStorage interaction", () => { @@ -58,26 +58,27 @@ describe("test CRUD localStorage interaction", () => { }).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++){ + let old_review = getReviewFromStorage(i); + let id = old_review.reviewID; + 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}`] + "imgSrc": `updated sample src ${id}`, + "mealName": `updated sample name ${id}`, + "restaurant": `updated sample restaurant ${id}`, + "reviewID": id, + "rating": (id % 5) + 1, + "tags": [`tag ${3*id}`, `tag ${3*id + 1}`, `tag ${3*id + 2}`] }; - new_review.reviewID = i; - reviews[i] = new_review; + updateReviewToStorage(id, new_review); - updateReviewToStorage(i, new_review); + let all_reviews = getAllReviewsFromStorage(); + let active_ids = JSON.parse(localStorage.getItem("activeIDS")); - assert.deepEqual(getAllReviewsFromStorage(), reviews); + assert.deepEqual(all_reviews[999], new_review); + assert.strictEqual(active_ids[999], id); assert.deepEqual(getReviewFromStorage(i), new_review); - assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1000); } }).timeout(5000); @@ -108,6 +109,7 @@ describe("test sort/filter localStorage interaction", () => { before(() => { localStorage.clear(); + getAllReviewsFromStorage(); }); it("add sample data for sort and filter", () => { @@ -116,7 +118,7 @@ describe("test sort/filter localStorage interaction", () => { "imgSrc": `sample src ${i}`, "mealName": `sample name ${i}`, "restaurant": `sample restaurant ${i}`, - "rating": i, + "rating": (i % 5) + 1, "tags": [`tag ${i%3}`, `tag ${i < 50}`, "tag x"] }; @@ -124,134 +126,145 @@ describe("test sort/filter localStorage interaction", () => { } }); - it("test getTopReviewsFromStorage end behavior after create", () =>{ - for(let i = 0; i <= 100; i++){ - let top_reviews = getTopReviewsFromStorage(i); - for(let j = 0; j < i; j++){ - assert.strictEqual(top_reviews[j].rating, 99 - j); - assert.strictEqual(top_reviews[j].reviewID, 99 - j); - } + it("test getTopIDsFromStorage end behavior after create", () =>{ + let top_reviews = getTopIDsFromStorage(); + let prev = Infinity; + for(let i = 0; i < top_reviews.length; i++){ + let review = getReviewFromStorage(top_reviews[i]); + assert.strictEqual(review.rating <= prev, true); } }); - it("test getReviewsByTag end behavior after create", () => { + it("test getIDsByTag end behavior after create", () => { let specific_tagged_reviews = []; - specific_tagged_reviews = getReviewsByTag("tag 0"); + specific_tagged_reviews = getIDsByTag("tag 0"); assert.strictEqual(specific_tagged_reviews.length, 34); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 0"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 3, 0); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 0"), true); + assert.strictEqual(review.reviewID % 3, 0); } - specific_tagged_reviews = getReviewsByTag("tag 1"); + specific_tagged_reviews = getIDsByTag("tag 1"); assert.strictEqual(specific_tagged_reviews.length, 33); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 1"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 3, 1); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 1"), true); + assert.strictEqual(review.reviewID % 3, 1); } - specific_tagged_reviews = getReviewsByTag("tag 2"); + specific_tagged_reviews = getIDsByTag("tag 2"); assert.strictEqual(specific_tagged_reviews.length, 33); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 2"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 3, 2); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 2"), true); + assert.strictEqual(review.reviewID % 3, 2); } - specific_tagged_reviews = getReviewsByTag("tag true"); + specific_tagged_reviews = getIDsByTag("tag true"); assert.strictEqual(specific_tagged_reviews.length, 50); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag true"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID < 50, true); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag true"), true); + assert.strictEqual(review.reviewID < 50, true); } - specific_tagged_reviews = getReviewsByTag("tag false"); + specific_tagged_reviews = getIDsByTag("tag false"); assert.strictEqual(specific_tagged_reviews.length, 50); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag false"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID >= 50, true); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag false"), true); + assert.strictEqual(review.reviewID >= 50, true); } - specific_tagged_reviews = getReviewsByTag("tag x"); + specific_tagged_reviews = getIDsByTag("tag x"); assert.strictEqual(specific_tagged_reviews.length, 100); - specific_tagged_reviews = getReviewsByTag("tag y"); + specific_tagged_reviews = getIDsByTag("tag y"); assert.deepEqual(specific_tagged_reviews, []); }); it("update sample data for sort and filter", () => { for(let i = 0; i < 100; i++){ + let old_review = getReviewFromStorage(i); let new_review = { "imgSrc": `sample src ${i}`, "mealName": `sample name ${i}`, "restaurant": `sample restaurant ${i}`, - "rating": 99-i, - "tags": [`tag ${i%4}`, `tag ${i < 37}`, "tag y"] + "reviewID": old_review.reviewID, + "rating": (i % 5) + 1, + "tags": [`tag ${i % 4}`, `tag ${i < 37}`, "tag y"] }; - updateReviewToStorage(i, new_review); + updateReviewToStorage(old_review.reviewID, new_review); } }); - it("test getTopReviewsFromStorage end behavior after create", () =>{ - for(let i = 0; i <= 100; i++){ - let top_reviews = getTopReviewsFromStorage(i); - for(let j = 0; j < i; j++){ - assert.strictEqual(top_reviews[j].rating, 99 - j); - assert.strictEqual(top_reviews[j].reviewID, j); - } + it("test getTopIDsFromStorage end behavior after create", () =>{ + let top_reviews = getTopIDsFromStorage(); + let prev = Infinity; + for(let i = 0; i < top_reviews.length; i++){ + let review = getReviewFromStorage(top_reviews[i]); + assert.strictEqual(review.rating <= prev, true); } }); - it("test getReviewsByTag end behavior after update", () => { + it("test getIDsByTag end behavior after update", () => { let specific_tagged_reviews = []; - specific_tagged_reviews = getReviewsByTag("tag 0"); + specific_tagged_reviews = getIDsByTag("tag 0"); assert.strictEqual(specific_tagged_reviews.length, 25); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 0"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 4, 0); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 0"), true); + assert.strictEqual(review.reviewID % 4, 0); } - specific_tagged_reviews = getReviewsByTag("tag 1"); + specific_tagged_reviews = getIDsByTag("tag 1"); assert.strictEqual(specific_tagged_reviews.length, 25); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 1"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 4, 1); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 1"), true); + assert.strictEqual(review.reviewID % 4, 1); } - specific_tagged_reviews = getReviewsByTag("tag 2"); + specific_tagged_reviews = getIDsByTag("tag 2"); assert.strictEqual(specific_tagged_reviews.length, 25); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 2"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 4, 2); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 2"), true); + assert.strictEqual(review.reviewID % 4, 2); } - specific_tagged_reviews = getReviewsByTag("tag 3"); + specific_tagged_reviews = getIDsByTag("tag 3"); assert.strictEqual(specific_tagged_reviews.length, 25); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag 3"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID % 4, 3); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag 3"), true); + assert.strictEqual(review.reviewID % 4, 3); } - specific_tagged_reviews = getReviewsByTag("tag true"); + specific_tagged_reviews = getIDsByTag("tag true"); assert.strictEqual(specific_tagged_reviews.length, 37); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag true"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID < 37, true); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag true"), true); + assert.strictEqual(review.reviewID < 37, true); } - specific_tagged_reviews = getReviewsByTag("tag false"); + specific_tagged_reviews = getIDsByTag("tag false"); assert.strictEqual(specific_tagged_reviews.length, 63); for(let i = 0; i < specific_tagged_reviews.length; i++){ - assert.strictEqual(specific_tagged_reviews[i].tags.includes("tag false"), true); - assert.strictEqual(specific_tagged_reviews[i].reviewID >= 37, true); + let review = getReviewFromStorage(specific_tagged_reviews[i]); + assert.strictEqual(review.tags.includes("tag false"), true); + assert.strictEqual(review.reviewID >= 37, true); } - specific_tagged_reviews = getReviewsByTag("tag x"); + specific_tagged_reviews = getIDsByTag("tag x"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag y"); + specific_tagged_reviews = getIDsByTag("tag y"); assert.strictEqual(specific_tagged_reviews.length, 100); }); @@ -261,38 +274,38 @@ describe("test sort/filter localStorage interaction", () => { } }); - it("test getTopReviewsFromStorage end behavior after delete", () =>{ + it("test getTopIDsFromStorage end behavior after delete", () =>{ for(let i = 0; i <= 100; i++){ - let top_reviews = getTopReviewsFromStorage(i); + let top_reviews = getTopIDsFromStorage(i); assert.deepEqual(top_reviews, []); } }); - it("test getReviewsByTag end behavior after delete", () => { + it("test getIDsByTag end behavior after delete", () => { let specific_tagged_reviews = []; - specific_tagged_reviews = getReviewsByTag("tag 0"); + specific_tagged_reviews = getIDsByTag("tag 0"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag 1"); + specific_tagged_reviews = getIDsByTag("tag 1"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag 2"); + specific_tagged_reviews = getIDsByTag("tag 2"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag 3"); + specific_tagged_reviews = getIDsByTag("tag 3"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag true"); + specific_tagged_reviews = getIDsByTag("tag true"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag false"); + specific_tagged_reviews = getIDsByTag("tag false"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag x"); + specific_tagged_reviews = getIDsByTag("tag x"); assert.deepEqual(specific_tagged_reviews, []); - specific_tagged_reviews = getReviewsByTag("tag y"); + specific_tagged_reviews = getIDsByTag("tag y"); assert.deepEqual(specific_tagged_reviews, []); }); diff --git a/source/assets/scripts/main.js b/source/assets/scripts/main.js index f012cb9..7f57f1b 100644 --- a/source/assets/scripts/main.js +++ b/source/assets/scripts/main.js @@ -1,15 +1,13 @@ // main.js -import {getAllReviewsFromStorage} from "./localStorage.js"; +import {getIDsByTag, getIDsFromStorage, getReviewFromStorage, getTopIDsFromStorage} 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 = getAllReviewsFromStorage(); - // Add each reviews to the
element - addReviewsToDocument(reviews); - // Add the event listeners to the form elements + //initial population of review container + sortAndFilter(false, null); + //Add the event listeners to dropdown and search bar initFormHandler(); } @@ -22,21 +20,141 @@ function addReviewsToDocument(reviews) { reviews.forEach(review => { let newReview = document.createElement("review-card"); newReview.data = review; - //TODO: want to append it to whatever the box is in layout reviewBox.append(newReview); }); } /** - * Adds the necessary event handlers to
and the clear storage - * -
- -
-
- -
- CREATE -

Recent Reviews

- - -
+ + + + + + -
+ +
+
+ logo +

Food Journal

+ logo +
+
+
+
+
+
+ + -
+ +
+ CREATE +

Recent Reviews

+
+ +
-
- - +
+
+ +
+ + + \ No newline at end of file diff --git a/source/static/Form.css b/source/static/Form.css index 584826a..283cc0d 100644 --- a/source/static/Form.css +++ b/source/static/Form.css @@ -17,6 +17,7 @@ border: 2px solid rgb(31 41 32); border-radius: 8px; background-color: #f7dfd5; + word-break: break-all; } #d-meal-img { diff --git a/source/static/homepage.css b/source/static/homepage.css index ff575b4..3487761 100644 --- a/source/static/homepage.css +++ b/source/static/homepage.css @@ -52,18 +52,34 @@ body { .search-bar { display: flex; justify-content: center; + font-family: "Century Gothic", sans-serif; + font-weight: bold; + color: #516754; +} + +#sort { + margin-right: 1em; } .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; +} + +#search-btn { + position: relative; + align-self: center; + width: 30px; + height: 30px; } #recent-reviews-text { @@ -78,6 +94,17 @@ img#create-btn { align-self: center; padding-left: 2.5%; padding-right: 2.5%; + cursor: pointer; + width: 10%; + height: 10%; +} + +img#create-btn-invis { + opacity: 0; + padding-left: 2.5%; + padding-right: 2.5%; + width: 10%; + height: 10%; } .review-container { diff --git a/source/sw.js b/source/sw.js new file mode 100644 index 0000000..a082357 --- /dev/null +++ b/source/sw.js @@ -0,0 +1,63 @@ +const CACHE_NAME = "food-journal-v1"; +const ASSETS = [ + "index.html", + "ReviewDetails.html", + "CreatePage.html", + "static/CoveredByYourGrace-Regular.ttf", + "static/CreatePage.css", + "static/Form.css", + "static/homepage.css", + "static/ReviewDetails.css", + "assets/images/0-star.svg", + "assets/images/1-star.svg", + "assets/images/2-star.svg", + "assets/images/3-star.svg", + "assets/images/4-star.svg", + "assets/images/5-star.svg", + "assets/images/create_button.png", + "assets/images/default_plate.png", + "assets/images/delete_icon_for_interface.png", + "assets/images/edit_button_for_interface.png", + "assets/images/home_button_for_interface.png", + "assets/images/favicon.ico", + "assets/images/Logo.png", + "assets/images/search_button.png", + "assets/scripts/CreatePage.js", + "assets/scripts/localStorage.js", + "assets/scripts/main.js", + "assets/scripts/ReviewCard.js", + "assets/scripts/ReviewDetails.js" +]; + +/** + * Adds the install listener where the app assets are added to the cache + */ +self.addEventListener("install", async () => { + // open the cace + const cache = await caches.open(CACHE_NAME); + // add all elements in ASSETS to the cache, these are all the files requried for the app to run + await cache.addAll(ASSETS); +}); + +/** + * Adds an event listener on fetch events to serve cached resources while offline + * Uses a network first structure to prioritize fetching from network in case of app updates. + * If there are important updates, we want the user to get those if possible. + */ +self.addEventListener("fetch", (event) => { + // add a response to the fetch event + event.respondWith(caches.open(CACHE_NAME).then((cache) => { + // try to return a network fetch response + return fetch(event.request).then((fetchedResponse) => { + // if there is a response, add it to the cache + cache.put(event.request, fetchedResponse.clone()); + // return the network response + return fetchedResponse; + }).catch(() => { + // If there is not a network response, return the cached response + // The ignoreVary option is used here to fix an issue where the service worker + // would not serve certain requests unless the page was refreshed at least once + return cache.match(event.request, {ignoreVary: true, ignoreSearch: true}); + }); + })); +}); diff --git a/specs/adrs/112922-review-storage.md b/specs/adrs/112922-review-storage.md new file mode 100644 index 0000000..9965b60 --- /dev/null +++ b/specs/adrs/112922-review-storage.md @@ -0,0 +1,16 @@ +# Backend Storage Structure +- Status: Accept +- Deciders: Rhea Bhutada, Kara Hoagland, Gavyn Ezell, George Dubinin, Henry Feng +- Date: 11/29/2022 + +## Decision Drivers +- Needed more efficient way of storing reviews that are created, for more efficient testing, updating, accessing, and deleting. + +## Considered Options +- localStorage + +## Decision Outcome +Using local storage to maintain the "local first" requirement. +Moved away from array of objects for storing reviews, reviews are stored individually as keys in localStorage, under the "review{id}" format. Each key +corresponds to object containing review data. We also have an array stored in local storage, named "activeIDs" which keeps track of id numbers that are attached +to created reviews. diff --git a/specs/adrs/112922-reviewpage-session.md b/specs/adrs/112922-reviewpage-session.md new file mode 100644 index 0000000..98ab568 --- /dev/null +++ b/specs/adrs/112922-reviewpage-session.md @@ -0,0 +1,15 @@ +# Opening Specific Reviews +- Status: Accept +- Deciders: Rhea Bhutada, Kara Hoagland, Gavyn Ezell, George Dubinin, Henry Feng +- Date: 11/29/2022 + +## Decision Drivers +- When opening up a review, browser needs to know what review ID to use for loading the review page data + +## Considered Options +- sessionStorage + +## Decision Outcome +Review cards have event listeners that will add their associated review ID number to session storage so +when the review loads, the browser will use the id stored to pull exact data corresponding to the review. + diff --git a/specs/adrs/112922-tag-review-collision-fix.md b/specs/adrs/112922-tag-review-collision-fix.md new file mode 100644 index 0000000..8841ed8 --- /dev/null +++ b/specs/adrs/112922-tag-review-collision-fix.md @@ -0,0 +1,13 @@ +# Organizing Review Under Tags +- Status: Accept +- Deciders: Rhea Bhutada, Kara Hoagland, Gavyn Ezell, George Dubinin, Henry Feng +- Date: 11/29/2022 + +## Decision Drivers +- Needed to keep track of reviews under certain given tags for filtering feature. + +## Considered Options +- localStorage + +## Decision Outcome +For every tag create a key under that tag name in localStorage. They will store an array of IDs that correspond to reviews that contain that tag. diff --git a/specs/adrs/120122-serviceworker-netfirst.md b/specs/adrs/120122-serviceworker-netfirst.md new file mode 100644 index 0000000..d60844f --- /dev/null +++ b/specs/adrs/120122-serviceworker-netfirst.md @@ -0,0 +1,19 @@ +# Use a network first cache second for service worker architecture + +- Status: in consideration +- Deciders: Arthur Lu, Kara Hoagland, Rhea Bhutada, George Dubinin +- Date: 12 / 01 / 22 + +## Decision Drivers + +- Need to balance the need for user ease of use and local first priority +- Users should expect to update their app easily when they have network, but may not be expected to know how to perform a hard refresh +- Local first priority means we should avoid unnecessary network activity when possible + +## Considered Options +- Network first cache second +- Cache first network second + +## Decision Outcome + +Chosen Option: Network first for automatic app updating. \ No newline at end of file