From 7381a7033231e6454a37fd64b1f3de4e8d59355f Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Wed, 11 Nov 2020 20:07:12 -0500
Subject: Kind of flailing with the UI; lots of API bugfixes though.

---
 iridescence/src/App.vue                            |  4 +-
 iridescence/src/api/dichroism.js                   | 18 ++++--
 iridescence/src/components/Navbar.vue              | 13 +---
 iridescence/src/components/admin/NewProduct.vue    | 12 ++--
 .../src/components/admin/ProductEditCard.vue       | 73 +++++++++++-----------
 .../src/components/admin/ProductEditList.vue       |  2 +-
 iridescence/src/models/product.js                  | 62 ++++--------------
 iridescence/src/models/product_diff.js             | 43 +++++++++++++
 iridescence/src/store/index.js                     | 33 +++++++---
 iridescence/src/views/Admin.vue                    | 11 +++-
 10 files changed, 153 insertions(+), 118 deletions(-)
 create mode 100644 iridescence/src/models/product_diff.js

(limited to 'iridescence/src')

diff --git a/iridescence/src/App.vue b/iridescence/src/App.vue
index a586f83..debc599 100644
--- a/iridescence/src/App.vue
+++ b/iridescence/src/App.vue
@@ -29,5 +29,7 @@ export default {
 </script>
 
 <style lang="scss">
-@use "../node_modules/bulma/bulma.sass";
+@charset "utf-8";
+$modal-background-background-color: hsla(0, 0%, 4%, 0.2);
+@import "../node_modules/bulma/bulma.sass";
 </style>
diff --git a/iridescence/src/api/dichroism.js b/iridescence/src/api/dichroism.js
index 610941c..c2cc93c 100644
--- a/iridescence/src/api/dichroism.js
+++ b/iridescence/src/api/dichroism.js
@@ -18,7 +18,7 @@ export default class Dichroism {
       const photos = await this._sendRequest("photos", options);
       return photos.map(p => new PhotoSet(p));
     } catch (err) {
-      console.error(err.message);
+      console.error("Dichroism: " + err.message);
       return null;
     }
   }
@@ -28,7 +28,7 @@ export default class Dichroism {
       const products = await this._sendRequest("products", null);
       return products.map(p => new Product(p));
     } catch (err) {
-      console.error(err.message);
+      console.error("Dichroism: " + err.message);
       return [];
     }
   }
@@ -36,14 +36,17 @@ export default class Dichroism {
   async updateProduct(fieldDiff) {
     const options = {
       method: "PATCH",
-      body: fieldDiff
+      headers: {
+        "Content-Type": "application/json"
+      },
+      body: JSON.stringify(fieldDiff)
     };
 
     try {
       const product = await this._sendRequest("products", options);
       return new Product(product);
     } catch (err) {
-      console.error(err.message);
+      console.error("Dichroism: " + err.message);
       return null;
     }
   }
@@ -51,14 +54,17 @@ export default class Dichroism {
   async createProduct(newProduct) {
     const options = {
       method: "POST",
-      body: newProduct
+      headers: {
+        "Content-Type": "application/json"
+      },
+      body: JSON.stringify(newProduct)
     };
 
     try {
       const product = await this._sendRequest("products", options);
       return new Product(product);
     } catch (err) {
-      console.error(err.message);
+      console.error("Dichroism: " + err.message);
       return null;
     }
   }
diff --git a/iridescence/src/components/Navbar.vue b/iridescence/src/components/Navbar.vue
index 0a66fa8..78627e9 100644
--- a/iridescence/src/components/Navbar.vue
+++ b/iridescence/src/components/Navbar.vue
@@ -36,14 +36,9 @@
         >
           <div
             class="navbar-end"
-            v-if="routeName == 'Administration'"
-            key="newProduct"
+            v-if="routeName != 'Administration'"
+            key="cartCheckout"
           >
-            <div class="navbar-item">
-              <NewProduct></NewProduct>
-            </div>
-          </div>
-          <div class="navbar-end" v-else key="cartCheckout">
             <div
               class="navbar-item has-dropdown is-active"
               v-for="category in categories.keys()"
@@ -74,13 +69,11 @@
 
 <script>
 import CartCheckout from "@/components/CartCheckout.vue";
-import NewProduct from "@/components/admin/NewProduct.vue";
 
 export default {
   name: "Navbar",
   components: {
-    CartCheckout,
-    NewProduct
+    CartCheckout
   },
   data() {
     return {
diff --git a/iridescence/src/components/admin/NewProduct.vue b/iridescence/src/components/admin/NewProduct.vue
index 511ae9c..48eb165 100644
--- a/iridescence/src/components/admin/NewProduct.vue
+++ b/iridescence/src/components/admin/NewProduct.vue
@@ -1,6 +1,6 @@
 <template>
   <div id="addNewProduct">
-    <button class="button is-primary" @click="toggleModal">
+    <button class="button is-primary is-medium" @click="toggleModal">
       + Add New
     </button>
     <transition
@@ -17,9 +17,7 @@
             <button class="delete" @click="toggleModal"></button>
           </header>
           <section class="modal-card-body">
-            <ProductEditCard
-              v-bind:current-product="newProduct"
-            ></ProductEditCard>
+            <ProductEditCard v-bind:parent-product="template"></ProductEditCard>
           </section>
           <footer class="modal-card-foot"></footer>
         </div>
@@ -29,8 +27,8 @@
 </template>
 
 <script>
-import Product from "../../models/product";
-import ProductEditCard from "./ProductEditCard";
+import Product from "@/models/product";
+import ProductEditCard from "@/components/admin/ProductEditCard";
 
 export default {
   name: "NewProduct",
@@ -40,7 +38,7 @@ export default {
   data: function() {
     return {
       modalEnabled: false,
-      newProduct: new Product(),
+      template: new Product({}),
       addAnother: false
     };
   },
diff --git a/iridescence/src/components/admin/ProductEditCard.vue b/iridescence/src/components/admin/ProductEditCard.vue
index 350f8df..a8117ce 100644
--- a/iridescence/src/components/admin/ProductEditCard.vue
+++ b/iridescence/src/components/admin/ProductEditCard.vue
@@ -2,7 +2,7 @@
   <div id="productEditCard">
     <div class="card">
       <div class="card-header">
-        <p class="card-header-title" v-if="currentProduct.id > 0">
+        <p class="card-header-title" v-if="currentProduct.id">
           {{ currentProduct.id }}: {{ currentProduct.name }}
         </p>
       </div>
@@ -74,7 +74,7 @@
                 type="file"
                 name="image"
                 accept=".jpg,.jpeg,.JPG,.JPEG"
-                @change="previewFiles"
+                @change="changePhotoSet"
               />
               <span class="file-cta">
                 <span class="file-label has-text-centered">
@@ -82,7 +82,7 @@
                 </span>
               </span>
               <span class="file-name">
-                {{ newProduct.photo_set }}
+                {{ filename }}
               </span>
             </label>
           </div>
@@ -107,7 +107,12 @@
         enter-active-class="animate__animated animate__fadeIn"
         leave-active-class="animate__animated animate__fadeOut"
       >
-        <div class="card-footer" v-if="isDifferent && isValid">
+        <div
+          class="card-footer"
+          v-if="
+            newProduct.isValidPost() || newProduct.isValidPatch(currentProduct)
+          "
+        >
           <div class="card-footer-item">
             <button class="button is-primary is-fullwidth" @click="saveProduct">
               Save
@@ -120,7 +125,8 @@
 </template>
 
 <script>
-import Product from "../../models/product";
+import Product from "@/models/product";
+import ProductDiff from "@/models/product_diff";
 
 const dollarRe = /^\$?(\d+)\.(\d{2})/gm;
 
@@ -128,28 +134,18 @@ export default {
   name: "ProductEditCard",
   data: function() {
     return {
-      newProduct: new Product(this.currentProduct)
+      currentProduct: new Product(this.parentProduct),
+      newProduct: new ProductDiff(this.currentProduct),
+      filename: ""
     };
   },
   props: {
-    currentProduct: {
+    parentProduct: {
       type: Product,
       required: true
     }
   },
-  watch: {
-    currentProduct() {
-      // TODO: necessary?
-      this.newProduct = new Product(this.currentProduct);
-    }
-  },
   computed: {
-    isDifferent() {
-      return this.newProduct.isDifferent(this.currentProduct);
-    },
-    isValid() {
-      return this.newProduct.isValid();
-    },
     newProductQuantity: {
       get: function() {
         return this.newProduct.quantity;
@@ -165,7 +161,7 @@ export default {
         return (this.newProduct.cents / 100).toFixed(2);
       },
       set: function(val) {
-        let groups = dollarRe.exec(val);
+        const groups = dollarRe.exec(val);
         if (groups && groups[1] && groups[2]) {
           this.newProduct.cents = 100 * groups[1] + 1 * groups[2];
         }
@@ -174,12 +170,24 @@ export default {
   },
   methods: {
     saveProduct() {
-      if (this.newProduct.id == 0) {
-        // new product
-        this.$store.dispatch("createProduct", this.newProduct);
-      } else {
+      if (this.newProduct.id) {
         // update existing
-        this.$store.dispatch("updateProduct", this.newProduct);
+        const updatedProduct = this.$store.dispatch(
+          "updateProduct",
+          this.newProduct
+        );
+        if (updatedProduct) {
+          this.currentProduct = updatedProduct;
+        }
+      } else {
+        // new product
+        const newProduct = this.$store.dispatch(
+          "createProduct",
+          this.newProduct
+        );
+        if (newProduct) {
+          this.currentProduct = newProduct;
+        }
       }
     },
     incrementQuantity(amount) {
@@ -187,21 +195,16 @@ export default {
         this.newProduct.quantity += amount;
       }
     },
-    async previewFiles(event) {
-      let file = event.target.files[0];
+    changePhotoSet(event) {
+      const file = event.target.files[0];
       if (!file) {
         return;
       }
 
-      const fd = new FormData();
-      fd.append(file.name, file);
-
-      const response = await fetch("http://localhost:8000/photos", {
-        method: "POST",
-        body: fd
+      this.$store.dispatch("createPhotoSet", file).then(r => {
+        this.filename = file.name;
+        this.newProduct.photo_set = r[0].id;
       });
-
-      console.log(response);
     }
   }
 };
diff --git a/iridescence/src/components/admin/ProductEditList.vue b/iridescence/src/components/admin/ProductEditList.vue
index 556f1c3..24a276e 100644
--- a/iridescence/src/components/admin/ProductEditList.vue
+++ b/iridescence/src/components/admin/ProductEditList.vue
@@ -6,7 +6,7 @@
         v-for="product in products"
         :key="product.id"
       >
-        <ProductEditCard v-bind:current-product="product"></ProductEditCard>
+        <ProductEditCard v-bind:parent-product="product"></ProductEditCard>
       </div>
     </div>
   </div>
diff --git a/iridescence/src/models/product.js b/iridescence/src/models/product.js
index fb7fd68..c408b79 100644
--- a/iridescence/src/models/product.js
+++ b/iridescence/src/models/product.js
@@ -1,54 +1,16 @@
 export default class Product {
-  id = 0;
-  name = "";
-  description = "";
-  cents = 0;
-  quantity = 0;
-  featured = false;
-  photo_base = "";
-  photo_fullsize = "";
-  photo_thumbnail = "";
-  category = "";
-
-  constructor(from) {
-    if (from) {
-      this.id = from.id;
-      this.name = from.name;
-      this.description = from.description;
-      this.cents = from.cents;
-      this.quantity = from.quantity;
-      this.featured = from.featured;
-      this.photo_base = from.photo_base;
-      this.photo_fullsize = from.photo_fullsize;
-      this.photo_thumbnail = from.photo_thumbnail;
-      this.category = from.category;
+  constructor(json) {
+    if (json) {
+      this.id = json.id ? json.id : null;
+      this.name = json.name ? json.name : null;
+      this.description = json.description ? json.description : null;
+      this.cents = json.cents ? json.cents : null;
+      this.quantity = json.quantity ? json.quantity : null;
+      this.featured = json.featured ? json.featured : false;
+      this.category = json.category ? json.category : null;
+      this.photo_base = json.photo_base ? json.photo_base : null;
+      this.photo_thumbnail = json.photo_thumbnail ? json.photo_thumbnail : null;
+      this.photo_fullsize = json.photo_fullsize ? json.photo_fullsize : null;
     }
   }
-
-  isDifferent(product) {
-    return (
-      this.id != product.id ||
-      this.name != product.name ||
-      this.quantity != product.quantity ||
-      this.cents != product.cents ||
-      this.photo_base != product.photo_base ||
-      this.photo_thumbnail != product.photo_thumbnail ||
-      this.photo_fullsize != product.photo_fullsize ||
-      this.description != product.description ||
-      this.featured != product.featured ||
-      this.category != product.category
-    );
-  }
-
-  isValid() {
-    return (
-      this.cents > 0 &&
-      this.name != "" &&
-      this.photo_thumbnail != "" &&
-      this.photo_base != "" &&
-      this.photo_fullsize != "" &&
-      this.category != "" &&
-      this.description != ""
-    );
-  }
 }
diff --git a/iridescence/src/models/product_diff.js b/iridescence/src/models/product_diff.js
new file mode 100644
index 0000000..a683102
--- /dev/null
+++ b/iridescence/src/models/product_diff.js
@@ -0,0 +1,43 @@
+export default class ProductDiff {
+  constructor(product) {
+    if (product) {
+      this.id = product.id ? product.id : 0;
+      this.name = product.name ? product.name : null;
+      this.description = product.description ? product.description : null;
+      this.cents = product.cents ? product.cents : null;
+      this.quantity = product.quantity ? product.quantity : null;
+      this.featured =
+        typeof product.featured === "boolean" ? product.featured : null;
+      this.category_path = product.category ? product.category : null;
+      this.photo_set = null;
+    }
+  }
+
+  isValidPost() {
+    return (
+      !this.id &&
+      this.name &&
+      this.description &&
+      this.cents &&
+      !this.quantity.isNaN &&
+      this.photo_set &&
+      this.category_path &&
+      this.description &&
+      typeof this.featured === "boolean"
+    );
+  }
+
+  isValidPatch(product) {
+    return (
+      this.id &&
+      (this.photo_set ||
+        (this.name && this.name != product.name) ||
+        (this.cents && this.cents != product.cents) ||
+        (this.category_path && this.category_path != product.category) ||
+        (this.quantity && this.quantity != product.quantity) ||
+        (this.description && this.description != product.description) ||
+        (typeof this.featured === "boolean" &&
+          this.featured != product.featured))
+    );
+  }
+}
diff --git a/iridescence/src/store/index.js b/iridescence/src/store/index.js
index fc77f79..f98fd87 100644
--- a/iridescence/src/store/index.js
+++ b/iridescence/src/store/index.js
@@ -29,6 +29,9 @@ export default new Vuex.Store({
     }
   },
   mutations: {
+    toggleBusy(state) {
+      state.busy = !state.busy;
+    },
     searchTerm(state, term) {
       if (term) {
         state.searchTerm = term;
@@ -40,9 +43,13 @@ export default new Vuex.Store({
       }
     },
     replaceProduct(state, product) {
+      if (!product || !product.id) {
+        return;
+      }
+
       let index = state.products.findIndex(p => p.id == product.id);
 
-      if (product && index >= 0) {
+      if (index) {
         state.products[index] = product;
       }
     },
@@ -53,17 +60,29 @@ export default new Vuex.Store({
     }
   },
   actions: {
-    async refreshProducts(context) {
+    async refreshProducts({ commit }) {
+      commit("toggleBusy");
       const products = await dichroism.getProducts();
-      context.commit("setProducts", products);
+      commit("setProducts", products);
+      commit("toggleBusy");
     },
-    async updateProduct(context, product) {
+    async updateProduct({ commit }, product) {
+      commit("toggleBusy");
       const updatedProduct = await dichroism.updateProduct(product);
-      context.dispatch("replaceProduct", updatedProduct);
+      commit("replaceProduct", updatedProduct);
+      commit("toggleBusy");
     },
-    async createProduct(context, product) {
+    async createProduct({ commit }, product) {
+      commit("toggleBusy");
       const newProduct = await dichroism.createProduct(product);
-      context.dispatch("addProduct", newProduct);
+      commit("addProduct", newProduct);
+      commit("toggleBusy");
+    },
+    async createPhotoSet({ commit }, file) {
+      commit("toggleBusy");
+      const photoSet = await dichroism.createPhoto(file);
+      commit("toggleBusy");
+      return photoSet;
     }
   },
   modules: {}
diff --git a/iridescence/src/views/Admin.vue b/iridescence/src/views/Admin.vue
index ec02e07..5274740 100644
--- a/iridescence/src/views/Admin.vue
+++ b/iridescence/src/views/Admin.vue
@@ -2,7 +2,14 @@
   <div id="admin">
     <div class="container">
       <section class="section">
-        <ProductSearch></ProductSearch>
+        <div class="columns">
+          <div class="column is-narrow">
+            <NewProduct></NewProduct>
+          </div>
+          <div class="column">
+            <ProductSearch></ProductSearch>
+          </div>
+        </div>
         <ProductEditList></ProductEditList>
       </section>
     </div>
@@ -10,6 +17,7 @@
 </template>
 
 <script>
+import NewProduct from "@/components/admin/NewProduct.vue";
 import ProductSearch from "@/components/ProductSearch.vue";
 import ProductEditList from "@/components/admin/ProductEditList.vue";
 
@@ -17,6 +25,7 @@ export default {
   name: "Admin",
   components: {
     ProductEditList,
+    NewProduct,
     ProductSearch
   }
 };
-- 
cgit v1.2.3