diff --git a/.github/workflows/js-unittest.yml b/.github/workflows/js-unittest.yml index 07438f8..4cb1359 100644 --- a/.github/workflows/js-unittest.yml +++ b/.github/workflows/js-unittest.yml @@ -1,27 +1,27 @@ -name: JS Unit Test - -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: Run tests +name: JS Unit Test + +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: Run tests run: sudo npm test \ No newline at end of file diff --git a/CreatePage.html b/CreatePage.html new file mode 100644 index 0000000..d1b5223 --- /dev/null +++ b/CreatePage.html @@ -0,0 +1,77 @@ + + + + + + + + Food Journal + + + + + + + + + + + + + + +
+
+ Pic: + + +
+
+ + Meal: + + +
+ +
+ Rating: + + + + + +
+ +
+ Other Info: + + + +
+ + +
+ + + \ No newline at end of file diff --git a/ReviewDetails.html b/ReviewDetails.html new file mode 100644 index 0000000..a2aea68 --- /dev/null +++ b/ReviewDetails.html @@ -0,0 +1,77 @@ + + + + + + + Food Journal + + + + + + + + + + + + + +
+ + +
+ + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..159a2b8 --- /dev/null +++ b/index.html @@ -0,0 +1,79 @@ + + + + + + + + Food Journal + + + + + + + + + + + + + +
+ +
+ +
+
+ Pic: + + +
+
+ + Meal: + + +
+ +
+ Rating: + + + + + +
+ +
+ Other Info: + + + +
+ + +
+ + diff --git a/package.json b/package.json index 85da852..bd8a69c 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,20 @@ -{ - "name": "food-journal", - "version": "1.0.0", - "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'" - }, - "devDependencies": { - "eslint": "^8.27.0", - "htmlhint": "1.1.4", - "mocha": "10", - "mock-local-storage": "^1.1.23", - "stylelint": "14.14.1", - "stylelint-config-standard": "^29.0.0" - } -} +{ + "name": "food-journal", + "version": "1.0.0", + "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'" + }, + "devDependencies": { + "eslint": "^8.27.0", + "htmlhint": "1.1.4", + "mocha": "10", + "mock-local-storage": "^1.1.23", + "stylelint": "14.14.1", + "stylelint-config-standard": "^29.0.0" + } +} diff --git a/review.html b/review.html new file mode 100644 index 0000000..52371f9 --- /dev/null +++ b/review.html @@ -0,0 +1,16 @@ + + + + + + + Food Journal + + + + +

Current Review:

+
+
+ + \ No newline at end of file diff --git a/source/assets/images/1_spooky-ghost-cookies.jpeg b/source/assets/images/1_spooky-ghost-cookies.jpeg new file mode 100644 index 0000000..aa5a1ed Binary files /dev/null and b/source/assets/images/1_spooky-ghost-cookies.jpeg differ diff --git a/source/assets/images/2_frightfully-easy-ghost-cookies.jpeg b/source/assets/images/2_frightfully-easy-ghost-cookies.jpeg new file mode 100644 index 0000000..38b34e8 Binary files /dev/null and b/source/assets/images/2_frightfully-easy-ghost-cookies.jpeg differ diff --git a/source/assets/images/3_ingredient-ghost-halloween-cookies.jpeg b/source/assets/images/3_ingredient-ghost-halloween-cookies.jpeg new file mode 100644 index 0000000..2bca5e0 Binary files /dev/null and b/source/assets/images/3_ingredient-ghost-halloween-cookies.jpeg differ diff --git a/source/assets/images/icons/0-star.svg b/source/assets/images/icons/0-star.svg new file mode 100644 index 0000000..6a62bca --- /dev/null +++ b/source/assets/images/icons/0-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/images/icons/1-star.svg b/source/assets/images/icons/1-star.svg new file mode 100644 index 0000000..d915bfc --- /dev/null +++ b/source/assets/images/icons/1-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/images/icons/2-star.svg b/source/assets/images/icons/2-star.svg new file mode 100644 index 0000000..349146a --- /dev/null +++ b/source/assets/images/icons/2-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/images/icons/3-star.svg b/source/assets/images/icons/3-star.svg new file mode 100644 index 0000000..1e414b3 --- /dev/null +++ b/source/assets/images/icons/3-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/images/icons/4-star.svg b/source/assets/images/icons/4-star.svg new file mode 100644 index 0000000..c8537c9 --- /dev/null +++ b/source/assets/images/icons/4-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/images/icons/5-star.svg b/source/assets/images/icons/5-star.svg new file mode 100644 index 0000000..d554b72 --- /dev/null +++ b/source/assets/images/icons/5-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/images/icons/default_plate.png b/source/assets/images/icons/default_plate.png new file mode 100644 index 0000000..dde984e Binary files /dev/null and b/source/assets/images/icons/default_plate.png differ diff --git a/source/assets/images/icons/plate_with_chopsticks.png b/source/assets/images/icons/plate_with_chopsticks.png new file mode 100644 index 0000000..023e43e Binary files /dev/null and b/source/assets/images/icons/plate_with_chopsticks.png differ diff --git a/source/assets/images/icons/plate_with_cutlery.png b/source/assets/images/icons/plate_with_cutlery.png new file mode 100644 index 0000000..6f7a938 Binary files /dev/null and b/source/assets/images/icons/plate_with_cutlery.png differ diff --git a/source/assets/scripts/ReviewCard.js b/source/assets/scripts/ReviewCard.js new file mode 100644 index 0000000..252ff26 --- /dev/null +++ b/source/assets/scripts/ReviewCard.js @@ -0,0 +1,270 @@ +// ReviewCard.js + +class ReviewCard extends HTMLElement { + // Called once when document.createElement('review-card') is called, or + // the element is written into the DOM directly as + 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; + } + `; + 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); + //Option 1: sending current data to second html page using localStorage (could also just store index) + sessionStorage.setItem('current', JSON.stringify(event.target.data)); + window.location.assign("./ReviewDetails.html"); + /* + //Option 2: sending current data to second html page using string query w/ url (currently not storing value) + let reviewFields = window.location.search.slice(1).split("&"); + for(let i = 0; i < reviewFields.length; i++) { + let kv = reviewFields[i].split("="); + let key = kv[0]; + let value = kv[1]; + console.log(key); + console.log(value); + // What you want to do with name and value... + }*/ + }); + } + + /** + * Called when the .data property is set on this element. + * + * For Example: + * let reviewCard = document.createElement('review-card'); + * reviewCard.data = { foo: 'bar' } + * + * @param {Object} data - The data to pass into the , must be of the + * following format: + * { + * "mealImg": "string", + * "imgAlt": "string", + * "mealName": "string", + * "comments": "string", + * "rating": number, + * "restaurant": "string", + * "tags": string array + * } + */ + set data(data) { + // If nothing was passed in, return + if (!data) return; + + // Select the
we added to the Shadow DOM in the constructor + let articleEl = this.shadowEl.querySelector('article'); + + // setting the article elements for the review card + + //image setup + let mealImg = document.createElement('img'); + mealImg.setAttribute('id', 'a-mealImg'); + mealImg.setAttribute('src',data['mealImg']); + mealImg.setAttribute('alt',data['imgAlt']); + + //meal name setup + let mealLabel = document.createElement('label'); + mealLabel.setAttribute('id', 'a-mealName'); + mealLabel.setAttribute('class','meal-name'); + 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'); + restaurantLabel.innerHTML = data['restaurant']; + + //comment section setup (display set to none) + let comments = document.createElement('p'); + comments.setAttribute('id', 'a-comments'); + comments.style.display = 'none'; + comments.innerText = data['comments']; + + //other info: rating + let ratingDiv = document.createElement('div'); + ratingDiv.setAttribute('class', 'rating'); + let starsImg = document.createElement('img'); + starsImg.setAttribute('id', 'a-rating'); + starsImg.setAttribute('src', './source/assets/images/icons/'+data['rating']+'-star.svg'); + starsImg.setAttribute('alt', data['rating'] +' stars'); + starsImg.setAttribute('num', data['rating']); + ratingDiv.append(starsImg); + + //added tags + let tagContainer = document.createElement('div'); + tagContainer.setAttribute('class', 'tag-container'); + tagContainer.setAttribute('id', 'a-tags'); + tagContainer.setAttribute('list', data['tags']); + 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] + " "; + tagContainer.append(newTag); + } + } + + articleEl.append(mealImg); + articleEl.append(mealLabel); + //articleEl.append(reviewLink) + articleEl.append(restaurantLabel); + articleEl.append(ratingDiv); + articleEl.append(tagContainer); + articleEl.append(comments); + + + } + + /** + * Called when getting the .data property of this element. + * + * For Example: + * let reviewCard = document.createElement('review-card'); + * reviewCard.data = { foo: 'bar' } + * + * @return {Object} data - The data from the , of the + * following format: + * { + * "mealImg": "string", + * "imgAlt": "string", + * "mealName": "string", + * "comments": "string", + * "rating": number, + * "restaurant": "string", + * "tags": string array + * } + */ + get data() { + + let dataContainer = {}; + + // getting the article elements for the review card + + //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'); + dataContainer['mealName'] = mealLabel.innerHTML; + + //get comment section + let comments = this.shadowEl.getElementById('a-comments'); + console.log(comments); + dataContainer['comments'] = comments.innerText; + + //get other info: rating + let starsImg = this.shadowEl.getElementById('a-rating'); + dataContainer['rating'] = starsImg.getAttribute('num'); + + //get restaurant name + let restaurantLabel = this.shadowEl.getElementById('a-restaurant'); + dataContainer['restaurant'] = restaurantLabel.innerHTML; + + //get tags + let tagContainer = this.shadowEl.getElementById('a-tags'); + dataContainer['tags'] = tagContainer.getAttribute('list').split(","); + + return dataContainer; + } +} +customElements.define('review-card', ReviewCard); diff --git a/source/assets/scripts/ReviewDetails.js b/source/assets/scripts/ReviewDetails.js new file mode 100644 index 0000000..a072e9f --- /dev/null +++ b/source/assets/scripts/ReviewDetails.js @@ -0,0 +1,127 @@ +//reviewDetails.js +import {getReviewsFromStorage, saveReviewsToStorage} from './localStorage.js'; + +// Run the init() function when the page has loaded +window.addEventListener('DOMContentLoaded', init); + +function init(){ + let main = document.querySelector('main'); + let reviews = getReviewsFromStorage(); + setupDelete(); + setupUpdate(); +} + +function setupDelete(){ + let deleteBtn = document.getElementById('delete'); + let reviews = getReviewsFromStorage(); + let current = JSON.parse(sessionStorage.getItem('current')); + 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; + } + }; + } + } + }); +} + +function setupUpdate(){ + let updateBtn = document.getElementById('update'); + let reviews = getReviewsFromStorage(); + let current = JSON.parse(sessionStorage.getItem('current')); + let form = document.getElementById('update-food-entry'); + 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']; + + 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); + } + } + + //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]); + } + + 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; + } + }; + + form.style.display = 'none'; + + }); + + 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/assets/scripts/ReviewPage.js b/source/assets/scripts/ReviewPage.js new file mode 100644 index 0000000..2ff8d70 --- /dev/null +++ b/source/assets/scripts/ReviewPage.js @@ -0,0 +1,13 @@ +// 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/localStorage.js b/source/assets/scripts/localStorage.js new file mode 100644 index 0000000..eab3ded --- /dev/null +++ b/source/assets/scripts/localStorage.js @@ -0,0 +1,19 @@ +/** + * @returns {Array} An array of reviews found in localStorage + */ +export function getReviewsFromStorage() { + let result = JSON.parse(localStorage.getItem('reviews')) + if (result) { + return result; + } + return new Array(0); +} + +/** + * 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 + */ +export function saveReviewsToStorage(reviews) { + localStorage.setItem('reviews', JSON.stringify(reviews)); +} diff --git a/source/assets/scripts/localStorage.test.js b/source/assets/scripts/localStorage.test.js new file mode 100644 index 0000000..9bf3c5f --- /dev/null +++ b/source/assets/scripts/localStorage.test.js @@ -0,0 +1,48 @@ +import {strict as assert} from "node:assert" +import {describe, it, beforeEach} from "mocha"; +import {saveReviewsToStorage, getReviewsFromStorage} from "./localStorage.js"; + +beforeEach(() => { + localStorage.clear(); +}); + +describe("test app localStorage interaction", () => { + it("get after init", () => { + assert.deepEqual(getReviewsFromStorage(), []); + }); + it("store one then get", () => { + let reviews = [{ + "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); + }); + it("repeated store one more and get", () => { + let reviews = []; + + assert.deepEqual(getReviewsFromStorage(), reviews); + + 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); + } + }); +}); diff --git a/source/assets/scripts/main.js b/source/assets/scripts/main.js new file mode 100644 index 0000000..e7f81f2 --- /dev/null +++ b/source/assets/scripts/main.js @@ -0,0 +1,118 @@ +// main.js +import {getReviewsFromStorage, saveReviewsToStorage} 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(); + // 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'); + 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); + }); + +} + +/** + * Adds the necessary event handlers to
and the clear storage + *