This commit is contained in:
look-its-ashton 2022-11-29 19:30:43 -08:00
commit 7cac0bc0a8
20 changed files with 290 additions and 103 deletions

View File

@ -29,8 +29,18 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
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 run js-doc
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact

29
.github/workflows/prettier.yml vendored Normal file
View File

@ -0,0 +1,29 @@
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

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
**/.devcontainer/*
**/node_modules/*
**/package-lock.json
**/*.vscode/*

View File

@ -1,3 +0,0 @@
{
"liveServer.settings.port": 5501
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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

BIN
admin/cipipeline/phase2.mp4 Normal file

Binary file not shown.

View File

@ -9,16 +9,21 @@
"lint-html": "htmlhint **/*.html",
"lint-css": "stylelint **/*.css",
"fix-css": "stylelint --fix **/*.css",
"http-server": "http-server source"
"http-server": "http-server source",
"js-doc": "jsdoc -d source/docs/ -r source/",
"lint-prettier": "prettier --check .",
"fix-prettier": "prettier --write ."
},
"devDependencies": {
"eslint": "^8.27.0",
"htmlhint": "1.1.4",
"http-server": "",
"jsdoc": "^4.0.0",
"mocha": "10",
"mock-local-storage": "^1.1.23",
"puppeteer": "^18.2.1",
"stylelint": "14.14.1",
"stylelint-config-standard": "^29.0.0"
"stylelint-config-standard": "^29.0.0",
"prettier": "2.8.0"
}
}

View File

@ -10,19 +10,18 @@
<!--Add Favicon-->
<link rel="icon" type="image/x-icon" href="./assets/images/favicon.ico">
<!-- Recipe Card Custom Element -->
<!-- Review Card Custom Element -->
<script src="assets/scripts/ReviewCard.js" type="module"></script>
<!-- Main Stylesheets & Scripts -->
<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config-->
<!-- <link rel="stylesheet" href="/static/reset.css" /> -->
<!-- Create Page Stylesheets & Scripts -->
<link rel="stylesheet" href="./static/CreatePage.css" />
<link rel="stylesheet" href="./static/Form.css" />
<script src="./assets/scripts/CreatePage.js" type="module"></script>
</head>
<header>
<!-- Setting up logo and site name at the top of the website -->
<div class="top-bar">
<img src ="./assets/images/Logo.png" alt="logo" />
<h1> Food Journal </h1>
@ -85,6 +84,8 @@
</fieldset>
<button type="submit" id="save-btn" value="Submit">Save</button>
<!-- Button that allows user to go back to the homepage -->
<input type="button" value="Cancel" id="home-btn" onclick="window.location.assign('./index.html')">
</form>
</div>

View File

@ -9,7 +9,7 @@
<!--Add Favicon-->
<link rel="icon" type="image/x-icon" href="./assets/images/icons/favicon.ico">
<!-- Recipe Card Custom Element -->
<!-- Review Card Custom Element -->
<script src="assets/scripts/ReviewCard.js" type="module"></script>
<!-- Main Stylesheets & Scripts -->

View File

@ -2,35 +2,38 @@ import { newReviewToStorage } from "./localStorage.js";
window.addEventListener("DOMContentLoaded", init);
/**
* Delegates the functionality for creating review cards.
*/
function init() {
// get next id
// creates the key
initFormHandler();
}
/**
* Creates a form and associates a new ID with the new review card.
*/
function initFormHandler() {
//accessing form components
// Accesses 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
*/
// Event listener for reading form data
let select = document.getElementById("select");
select.addEventListener("change", function() {
const input = document.getElementById("source");
// Select a photo with HTML file selector
if (select.value == "file") {
input.innerHTML = `
Source:
<input type="file" accept="image/*" id="mealImg" name="mealImg">
`;
}
//TODO: change to photo taking for sprint 3
// Upload text URL input
else {
input.innerHTML = `
Source:
@ -39,28 +42,28 @@ function initFormHandler() {
}
});
//addressing sourcing image from local file
// Addresses 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
// Store image data URL after successful image load
reader.addEventListener("load", ()=>{
imgDataURL = reader.result;
}, false);
//convert image file into data URL for local storage
// 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
*/
// Create reviewObject and put in storage
e.preventDefault();
let formData = new FormData(form);
let reviewObject = {};
// Adds data to the reviewObject from form data
for (let [key, value] of formData) {
console.log(`${key}`);
console.log(`${value}`);
@ -71,36 +74,51 @@ function initFormHandler() {
reviewObject["mealImg"] = imgDataURL;
}
}
// Makes sure that ratings is filled
if(reviewObject["rating"] != null){
//Adds rags separately as an array
reviewObject["tags"] = [];
// Grabs tags
let tags = document.querySelectorAll(".tag");
for(let i = 0; i < tags.length; i ++) {
reviewObject["tags"].push(tags[i].innerHTML);
tagContainer.removeChild(tags[i]);
}
// Assigns the new review with a new ID
let nextReviewId = newReviewToStorage(reviewObject);
sessionStorage.setItem("currID", JSON.stringify(nextReviewId));
// Redirects to a page that shows the newly created review
window.location.assign("./ReviewDetails.html");
} else{
}
// Does not let user proceed if rating is not complete
else{
window.alert("NO! FILL IN STARS");
}
});
// Event listener for tag functionality
let tagAddBtn = document.getElementById("tag-add-btn");
tagAddBtn.addEventListener("click", ()=> {
let tagField = document.getElementById("tag-form");
// If there is a tag, it'll display the tag
if (tagField.value.length > 0) {
let tagLabel = document.createElement("label");
tagLabel.innerHTML = tagField.value;
tagLabel.setAttribute("class","tag");
// Allows for user to delete the tag
tagLabel.addEventListener("click",()=> {
tagContainer.removeChild(tagLabel);
});
// Adds the tag
tagContainer.append(tagLabel);
tagField.value = "";

View File

@ -102,24 +102,15 @@ class ReviewCard extends HTMLElement {
articleEl.append(styleEl);
shadowEl.append(articleEl);
this.shadowEl = shadowEl;
//attach event listener to each recipe-card
// Attach event listener to each review-card
this.addEventListener("click", (event) => {
console.log(event.target);
console.log(event.target.reviewId);
//Option 1: sending current data to second html page using localStorage (could also just store index)
// Saves the ID for corresponding review on new page (for data retrieval)
sessionStorage.setItem("currID", JSON.stringify(event.target.data.reviewID));
// Goes to the new page for the review
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...
}*/
});
}
@ -133,11 +124,12 @@ class ReviewCard extends HTMLElement {
* @param {Object} data - The data to pass into the <review-card>, must be of the
* following format:
* {
* "mealImg": "string",
* "mealName": "string",
* "comments": "string",
* "mealImg": string,
* "mealName": string,
* "comments": string,
* "rating": number,
* "restaurant": "string",
* "restaurant": string,
* "reviewID": number,
* "tags": string array
* }
*/
@ -148,10 +140,10 @@ class ReviewCard extends HTMLElement {
// Select the <article> we added to the Shadow DOM in the constructor
let articleEl = this.shadowEl.querySelector("article");
// setting the article elements for the review card
// Setting the article elements for the review card
this.reviewID = data["reviewID"];
//image setup
// Image setup
let mealImg = document.createElement("img");
mealImg.setAttribute("id", "a-mealImg");
mealImg.setAttribute("alt","Meal Photo Corrupted");
@ -161,25 +153,25 @@ class ReviewCard extends HTMLElement {
e.onerror = null;
});
//meal name setup
// 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
// Restaurant name setup
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)
// 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
// Rating setup
let ratingDiv = document.createElement("div");
ratingDiv.setAttribute("class", "rating");
let starsImg = document.createElement("img");
@ -189,11 +181,13 @@ class ReviewCard extends HTMLElement {
starsImg.setAttribute("num", data["rating"]);
ratingDiv.append(starsImg);
//added tags
// Tags setup
let tagContainer = document.createElement("div");
tagContainer.setAttribute("class", "tag-container");
tagContainer.setAttribute("id", "a-tags");
tagContainer.setAttribute("list", data["tags"]);
// Checks if user gave tags, if so added to review card
if(data["tags"]){
for (let i = 0; i < data["tags"].length; i++) {
let newTag = document.createElement("label");
@ -203,8 +197,7 @@ class ReviewCard extends HTMLElement {
}
}
//adding final ID to data!
// Setting all the data to the review card
articleEl.append(mealImg);
articleEl.append(mealLabel);
articleEl.append(restaurantLabel);
@ -225,11 +218,12 @@ class ReviewCard extends HTMLElement {
* @return {Object} data - The data from the <review-card>, of the
* following format:
* {
* "mealImg": "string",
* "mealName": "string",
* "comments": "string",
* "mealImg": string,
* "mealName": string,
* "comments": string,
* "rating": number,
* "restaurant": "string",
* "restaurant": string,
* "reviewID": number,
* "tags": string array
* }
*/
@ -237,31 +231,31 @@ class ReviewCard extends HTMLElement {
let dataContainer = {};
// getting the article elements for the review card
// Getting the article elements for the review card
dataContainer["reviewID"] = this.reviewID;
//get image
// Get image
let mealImg = this.shadowEl.getElementById("a-mealImg");
dataContainer["mealImg"] = mealImg.getAttribute("src");
//get meal name
// Get meal name
let mealLabel = this.shadowEl.getElementById("a-mealName");
dataContainer["mealName"] = mealLabel.innerHTML;
//get comment section
// Get comment section
let comments = this.shadowEl.getElementById("a-comments");
console.log(comments);
dataContainer["comments"] = comments.innerText;
//get other info: rating
// Get rating
let starsImg = this.shadowEl.getElementById("a-rating");
dataContainer["rating"] = starsImg.getAttribute("num");
//get restaurant name
//Get restaurant name
let restaurantLabel = this.shadowEl.getElementById("a-restaurant");
dataContainer["restaurant"] = restaurantLabel.innerHTML;
//get tags
// Get tags
let tagContainer = this.shadowEl.getElementById("a-tags");
dataContainer["tags"] = tagContainer.getAttribute("list").split(",");

View File

@ -4,6 +4,9 @@ import {deleteReviewFromStorage, getReviewFromStorage, updateReviewToStorage} fr
// Run the init() function when the page has loaded
window.addEventListener("DOMContentLoaded", init);
/**
* Populates the relevant data to the details from local storage review.
*/
function init(){
setupInfo();
setupDelete();
@ -51,6 +54,9 @@ function setupInfo(){
}
}
/**
* Sets up delete button to delete reveiw from storage and switch to homepage.
*/
function setupDelete(){
let deleteBtn = document.getElementById("delete-btn");
let currID = JSON.parse(sessionStorage.getItem("currID"));
@ -63,6 +69,9 @@ function setupDelete(){
});
}
/**
* Sets up update button to reveal form and update info in storage and the current page.
*/
function setupUpdate(){
let updateBtn = document.getElementById("update-btn");
let currID = JSON.parse(sessionStorage.getItem("currID"));
@ -139,19 +148,18 @@ function setupUpdate(){
//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
*/
//We create reviewCard datea, replace it in in storage, and update tags
let formData = new FormData(form);
let newData = {};
// Iterate through formData an add to 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
// Account for the case where image is not updated
if (`${key}` === "mealImg" && document.getElementById("mealImg").value === "") {
newData["mealImg"] = currReview["mealImg"];
}
@ -175,6 +183,7 @@ function setupUpdate(){
});
// Adding tag to form functionality
let tagAddBtn = document.getElementById("tag-add-btn");
tagAddBtn.addEventListener("click", ()=> {
let tagField = document.getElementById("tag-form");

View File

@ -10,12 +10,10 @@
<!--Add Favicon-->
<link rel="icon" type="image/x-icon" href="./assets/images/favicon.ico">
<!-- Recipe Card Custom Element -->
<!-- Review Card Custom Element -->
<script src="assets/scripts/ReviewCard.js" type="module"></script>
<!-- Main Stylesheets & Scripts -->
<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config-->
<!-- <link rel="stylesheet" href="/static/reset.css" /> -->
<!-- Homepage Stylesheets & Scripts -->
<link rel="stylesheet" href="./static/homepage.css" />
<script src="assets/scripts/main.js" type="module"></script>
</head>

View File

@ -0,0 +1,17 @@
# Use JSDoc for JS documentation
- Status: accept
- Deciders: Arthur Lu, Marc Reta
- Date: 11 / 29 / 22
## Decision Drivers
- Need simple way to publish documentation for code
- Already documentating infile using JSDoc style
## Considered Options
- JSDoc
## Decision Outcome
Chosen Option: JSDoc. Will run by generating docs in /source/docs/ before publishing /source/ so users can enter the URI /docs/ to see documentation.

View File

@ -0,0 +1,17 @@
# Use Prettier for generic style enforcement
- Status: accept
- Deciders: Arthur Lu, Marc Reta
- Date: 11 / 29 / 22
## Decision Drivers
- Other linters (HTML, CSS, JS) are sometimes too permissive
- Need to enforce style on other files like markdown, json
## Considered Options
- Prettier
## Decision Outcome
Chosen Option: Prettier

View File

@ -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.

View File

@ -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.

View File

@ -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.