mirror of
				https://github.com/KevinMidboe/planetposen.git
				synced 2025-10-29 17:50:32 +00:00 
			
		
		
		
	Merge pull request #14 from KevinMidboe/feat/editProduct
Feat/edit product
This commit is contained in:
		| @@ -7,7 +7,9 @@ | ||||
|       </div> | ||||
|       <div v-if="products && products.length" class="products"> | ||||
|         <add-card /> | ||||
|         <product-card v-for="product in products" :product="product" /> | ||||
|         <product-card v-for="product in products" | ||||
|                       :product="product" | ||||
|                       :to="'/edit/' + product.urlSlug" :key="Math.random()" /> | ||||
|       </div> | ||||
|  | ||||
|       <span v-else> | ||||
|   | ||||
| @@ -1,64 +1,15 @@ | ||||
| <template> | ||||
|   <main role="main" v-if="product"> | ||||
|     <div class="banner padding-bottom--lg" :class="'bg-' + color"> | ||||
|       <div class="banner-content top-show-sm col-wrap max-width"> | ||||
|         <h1>{{ product.name }}</h1> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <section class="product padding-top--lg"> | ||||
|       <div class="max-width col-wrap"> | ||||
|         <div class="col-6 image-preview"> | ||||
|           <img :src="product.image" /> | ||||
|         </div> | ||||
|         <div class="col-6 details"> | ||||
|           <div class="pricing"> | ||||
|             <span class="amount">24.99</span> | ||||
|             <span class="currency">kr</span> | ||||
|           </div> | ||||
|  | ||||
|           <p v-if="product.description">{{ product.description }}</p> | ||||
|           <p v-else class="description">Notatbøkene fra All Pine Press er trykket i Oslo og papir fra Hellefoss Paper AS i Hokksund. (Notatbøkene er uten linjer)</p> | ||||
|  | ||||
|           <div class="stock" v-if="inStock"> | ||||
|             <i class="icon icon--checkmark-circle"></i> | ||||
|             På lager | ||||
|           </div> | ||||
|           <div class="stock soldOut" v-else> | ||||
|             <i class="icon icon--close-circle"></i> | ||||
|             Utsolgt | ||||
|           </div> | ||||
|  | ||||
|           <div class="actions"> | ||||
|           <!-- Variation picker --> | ||||
|  | ||||
|           <!-- Amount picker --> | ||||
|           <!-- Buy button --> | ||||
|             <Button color="black" :small="true" @click="addToCart" :scaleRotate="true">Add to cart</Button> | ||||
|           </div> | ||||
|  | ||||
|           <div class="meta"> | ||||
|             <span class="categories"> | ||||
|               Kategorier: | ||||
|             </span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
|   </main> | ||||
|   <product :product="product" /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Button from '@/components/ui/Button'; | ||||
| import store from '@/store' | ||||
| import Product from '@/components/ui/product'; | ||||
|  | ||||
| export default { | ||||
|   components: { Button }, | ||||
|   components: { Product }, | ||||
|   data() { | ||||
|     return { | ||||
|       color: Math.random() > 0.5 ? 'yellow' : 'blue', | ||||
|       inStock: Math.random() > 0.5, | ||||
|       product: null | ||||
|       product: null, | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
| @@ -74,11 +25,6 @@ export default { | ||||
|       .then(resp => resp.json()) | ||||
|       .then(product => this.product = product); | ||||
|   }, | ||||
|   methods: { | ||||
|     addToCart() { | ||||
|       store.dispatch('cartModule/addItemToCart', { ...this.product }); | ||||
|     } | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     document.querySelector('body').style.backgroundColor = '#19171A'; | ||||
|   } | ||||
| @@ -86,59 +32,4 @@ export default { | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import '/frontend//styles/spacing'; | ||||
| @import '/frontend//styles/variables'; | ||||
|  | ||||
| main { | ||||
|   color: var(--color-background); | ||||
| } | ||||
|  | ||||
| section.product { | ||||
|   overflow: auto; | ||||
|  | ||||
|   @include mobile { | ||||
|     .col-6 { | ||||
|       width: 100%; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .image-preview img { | ||||
|   border-radius: 6px; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .details { | ||||
|   margin-top: 1rem; | ||||
|   font-size: 1.3rem; | ||||
|  | ||||
|   > div, > p { | ||||
|     margin-bottom: 2.25rem; | ||||
|   } | ||||
|  | ||||
|   .stock { | ||||
|     padding: 1rem; | ||||
|     border: 4px solid var(--color-green); | ||||
|     width: max-content; | ||||
|  | ||||
|     .icon { | ||||
|       position: relative; | ||||
|       top: 2px; | ||||
|       left: -4px; | ||||
|     } | ||||
|  | ||||
|     &.soldOut { | ||||
|       border-color: var(--color-pink); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .pricing { | ||||
|     font-size: 2rem; | ||||
|   } | ||||
| } | ||||
|  | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										122
									
								
								frontend/components/editProduct.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								frontend/components/editProduct.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <main v-if="!hide"> | ||||
|       <div class="header"> | ||||
|       </div> | ||||
|       <div class="edit max-width col-wrap"> | ||||
|         <h2>Edit mode</h2> | ||||
|         <div class="edit-product" v-if="product"> | ||||
|           <h4>Product</h4> | ||||
|           <input v-model="product.color" placeholder="color" /> | ||||
|           <input v-model="product.name" placeholder="name" /> | ||||
|           <input v-model="product.description" placeholder="description" /> | ||||
|           <input v-model="product.image" placeholder="image" /> | ||||
|           <input v-model="product.slug" placeholder="slug (default: empty)" /> | ||||
|         </div> | ||||
|  | ||||
|         <div v-if="variation"> | ||||
|           <h4>Variations</h4> | ||||
|           <input v-for="key in Object.keys(variation)" v-model="variation[key]" :placeholder="key" /> | ||||
|         </div> | ||||
|  | ||||
|         <Button :small="true" @click="hide = !hide">Preview</Button> | ||||
|         <Button :small="true" color="green">Submit new bag</Button> | ||||
|       </div> | ||||
|  | ||||
|       <product :product="product" /> | ||||
|     </main> | ||||
|     <main v-else> | ||||
|       <Button @click="hide = !hide" class="pos-abs" :small="true">Close preview</Button> | ||||
|       <product :product="product" /> | ||||
|     </main> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Product from '@/components/ui/product'; | ||||
| import Button from '@/components/ui/Button'; | ||||
|  | ||||
| export default { | ||||
|   components: { Product, Button }, | ||||
|   data() { | ||||
|     return { | ||||
|       hide: false, | ||||
|       product: undefined, | ||||
|       variation: undefined | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     document.querySelector('body').style.backgroundColor = 'white'; | ||||
|  | ||||
|     const slug = this.$route.params.slug;  | ||||
|     if (slug) { | ||||
|       this.getAndSetProductBySlug(this.$route.params.slug); | ||||
|     } else { | ||||
|       this.getProductSchema() | ||||
|       this.getVariationSchema() | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getProductSchema() { | ||||
|       let url = '/api/product/schema'; | ||||
|       if (window.location.href.includes('localhost')) | ||||
|         url = 'http://localhost:30010'.concat(url) | ||||
|  | ||||
|       fetch(url) | ||||
|         .then(resp => resp.json()) | ||||
|         .then(product => this.product = product); | ||||
|     }, | ||||
|     getVariationSchema() { | ||||
|       let url = '/api/variation/schema'; | ||||
|       if (window.location.href.includes('localhost')) | ||||
|         url = 'http://localhost:30010'.concat(url) | ||||
|  | ||||
|       fetch(url) | ||||
|         .then(resp => resp.json()) | ||||
|         .then(variation => this.variation = variation); | ||||
|     }, | ||||
|     getAndSetProductBySlug(slug) { | ||||
|       let url = `/api/product/${ slug }`; | ||||
|       if (window.location.href.includes('localhost')) | ||||
|         url = 'http://localhost:30010'.concat(url) | ||||
|  | ||||
|       fetch(url) | ||||
|         .then(resp => resp.json()) | ||||
|         .then(product => this.product = product); | ||||
|     } | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     document.querySelector('body').style.backgroundColor = '#19171A'; | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .header { | ||||
|   background-color: var(--color-background); | ||||
|   width: 100%; | ||||
|   height: 8rem; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| .edit > div:not(:first-of-type) { | ||||
|   margin-top: 0.75rem; | ||||
| } | ||||
|  | ||||
| input { | ||||
|   border-color: black; | ||||
|   padding: 0.5rem; | ||||
|   font-size: 1rem; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   margin-top: 0.5rem; | ||||
| }  | ||||
|  | ||||
| .edit { | ||||
|   color: var(--color-background); | ||||
|   margin: 2rem auto; | ||||
| } | ||||
| </style> | ||||
| @@ -58,7 +58,11 @@ export default { | ||||
|       }) | ||||
|     }, | ||||
|     makeIntent() { | ||||
|       fetch('/api/stripe/create-payment-intent', { | ||||
|       let url = '/api/stripe/create-payment-intent'; | ||||
|       if (window.location.href.includes('localhost')) | ||||
|         url = 'http://localhost:30010'.concat(url) | ||||
|  | ||||
|       fetch(url, { | ||||
|         method: 'POST', | ||||
|         headers: { | ||||
|           'Content-Type': 'application/json' | ||||
|   | ||||
| @@ -40,7 +40,11 @@ export default { | ||||
|     product: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }  | ||||
|     }, | ||||
|     to: { | ||||
|       type: String, | ||||
|       required: false | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
| @@ -64,7 +68,7 @@ export default { | ||||
|       store.dispatch('cartModule/addItemToCart', { ...this.product }); | ||||
|     }, | ||||
|     viewProduct() { | ||||
|       this.$router.push('/shop/' + this.product.urlSlug) | ||||
|       this.$router.push(this.to ? this.to : '/shop/' + this.product.urlSlug) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										177
									
								
								frontend/components/ui/product.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								frontend/components/ui/product.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| <template> | ||||
|   <main role="main" v-if="product"> | ||||
|     <div class="banner padding-bottom--lg" :class="'bg-' + color"> | ||||
|       <div class="banner-content top-show-sm col-wrap max-width"> | ||||
|         <h1>{{ product.name }}</h1> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <section class="product padding-top--lg"> | ||||
|       <div class="max-width col-wrap"> | ||||
|         <div class="col-6 image-preview"> | ||||
|           <img v-if="product.image" :src="product.image" /> | ||||
|           <div v-else role="image" aria-label="no image" class="image-placeholder" ref="placeholder"> | ||||
|             <h3>(no image)</h3> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="col-6 details"> | ||||
|           <div class="pricing"> | ||||
|             <span :class="{ sale: selectedVariation.discount }">24.99 kr</span> | ||||
|             <span v-if="selectedVariation.discount">19.99 kr</span> | ||||
|           </div> | ||||
|  | ||||
|           <p v-if="product.description">{{ product.description }}</p> | ||||
|           <p v-else class="description">Notatbøkene fra All Pine Press er trykket i Oslo og papir fra Hellefoss Paper AS i Hokksund. (Notatbøkene er uten linjer)</p> | ||||
|  | ||||
|           <div class="stock" v-if="inStock"> | ||||
|             <i class="icon icon--checkmark-circle"></i> | ||||
|             På lager | ||||
|           </div> | ||||
|           <div class="stock soldOut" v-else> | ||||
|             <i class="icon icon--close-circle"></i> | ||||
|             Utsolgt | ||||
|           </div> | ||||
|  | ||||
|           <div class="actions"> | ||||
|           <!-- Variation picker --> | ||||
|  | ||||
|           <!-- Amount picker --> | ||||
|           <!-- Buy button --> | ||||
|             <Button color="black" :small="true" @click="addToCart" :scaleRotate="true">Add to cart</Button> | ||||
|           </div> | ||||
|  | ||||
|           <div class="meta"> | ||||
|             <span class="categories"> | ||||
|               Kategorier: | ||||
|             </span> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </section> | ||||
|   </main> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Button from '@/components/ui/Button'; | ||||
| import store from '@/store' | ||||
|  | ||||
| export default { | ||||
|   components: { Button }, | ||||
|   props: { | ||||
|     product: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       placeholderWidth: '0px', | ||||
|       color: Math.random() > 0.5 ? 'yellow' : 'blue', | ||||
|       inStock: Math.random() > 0.5, | ||||
|       selectedVariation: { discount: Math.random() > 0.5 ? true : false } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     setTimeout(this.squareImagePlaceholder, 10); | ||||
|   }, | ||||
|   // TODO remove | ||||
|   watch: { | ||||
|     product: { | ||||
|       deep: true, | ||||
|       handler(newVal) { | ||||
|         if (this.product.color) | ||||
|           this.color = this.product.color; | ||||
|         if (newVal.image == '') | ||||
|           setTimeout(this.squareImagePlaceholder, 10);  | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     addToCart() { | ||||
|       store.dispatch('cartModule/addItemToCart', { ...this.product }); | ||||
|     }, | ||||
|     squareImagePlaceholder() { | ||||
|       const { placeholder } = this.$refs; | ||||
|       if (placeholder) | ||||
|         placeholder.style.height = placeholder.clientWidth + 'px'; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import '/frontend//styles/spacing'; | ||||
| @import '/frontend//styles/variables'; | ||||
|  | ||||
| main { | ||||
|   color: var(--color-background); | ||||
| } | ||||
|  | ||||
| section.product { | ||||
|   overflow: auto; | ||||
|  | ||||
|   @include mobile { | ||||
|     .col-6 { | ||||
|       width: 100%; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .image-preview img { | ||||
|   border-radius: 6px; | ||||
|   width: 100%; | ||||
| } | ||||
| .image-preview .image-placeholder { | ||||
|   width: 100%; | ||||
|   border: 2px solid var(--color-background); | ||||
|   border-radius: 6px; | ||||
|  | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|  | ||||
|   h3 { | ||||
|     font-style: italic; | ||||
|     text-align: center; | ||||
|     width: 100%; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .details { | ||||
|   margin-top: 1rem; | ||||
|   font-size: 1.3rem; | ||||
|  | ||||
|   > div, > p { | ||||
|     margin-bottom: 2.25rem; | ||||
|   } | ||||
|  | ||||
|   .stock { | ||||
|     padding: 1rem; | ||||
|     border: 4px solid var(--color-green); | ||||
|     width: max-content; | ||||
|  | ||||
|     .icon { | ||||
|       position: relative; | ||||
|       top: 2px; | ||||
|       left: -4px; | ||||
|     } | ||||
|  | ||||
|     &.soldOut { | ||||
|       border-color: var(--color-pink); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .pricing { | ||||
|     font-size: 2rem; | ||||
|  | ||||
|     & .sale { | ||||
|       text-decoration: line-through; | ||||
|       color: gray; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| </style> | ||||
| @@ -39,6 +39,16 @@ let routes = [ | ||||
|     path: '/admin', | ||||
|     component: (resolve) => require(['./components/Admin.vue'], resolve) | ||||
|   }, | ||||
|   { | ||||
|     name: 'Add', | ||||
|     path: '/add', | ||||
|     component: (resolve) => require(['./components/editProduct.vue'], resolve) | ||||
|   }, | ||||
|   { | ||||
|     name: 'Edit', | ||||
|     path: '/edit/:slug', | ||||
|     component: (resolve) => require(['./components/editProduct.vue'], resolve) | ||||
|   } | ||||
|   // { | ||||
|   //   name: 'styleguide', | ||||
|   //   path: '/styleguide', | ||||
|   | ||||
| @@ -41,6 +41,9 @@ h3 { | ||||
|     font-size: calc(0.785 * var(--text-lg)); | ||||
|   } | ||||
| } | ||||
| h4 { | ||||
|   font-size: var(--text-md); | ||||
| } | ||||
|  | ||||
| .flex { | ||||
|   display: flex; | ||||
| @@ -77,6 +80,12 @@ h3 { | ||||
| } | ||||
|  | ||||
|  | ||||
| .pos-abs { | ||||
|   position: absolute; | ||||
|   z-index: 10; | ||||
| } | ||||
|  | ||||
|  | ||||
| .col-wrap { | ||||
|   padding-left: 120px; | ||||
|   padding-right: 120px; | ||||
| @@ -117,6 +126,7 @@ h3 { | ||||
|  | ||||
|   @include mobile { | ||||
|     padding-top: var(--space-xl); | ||||
|     padding-bottom: var(--space-xl); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,9 +21,11 @@ app.use(express.json()); | ||||
| // app.use(express.urlencoded()); | ||||
|  | ||||
| router.get('/products', productsController.allProducts) | ||||
| router.get('/product/schema', productsController.getProductSchema) | ||||
| router.get('/product/:slug', productsController.productBySlug) | ||||
| router.post('/product', productsController.addNewProduct) | ||||
|  | ||||
| router.get('/variation/schema', variationsController.getVariationSchema) | ||||
| router.post('/variation/:id', variationsController.addNewVariationToProduct); | ||||
|  | ||||
| router.post('/applepay/validateSession', applePayController.validateSession) | ||||
|   | ||||
| @@ -46,8 +46,14 @@ const addNewProduct = (req, res) => { | ||||
|     .then(resp => res.send(resp)) | ||||
| } | ||||
|  | ||||
| const getProductSchema = (req, res) => { | ||||
|   return products.productSchema() | ||||
|     .then(schema => res.json(schema)) | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   allProducts, | ||||
|   productBySlug, | ||||
|   addNewProduct | ||||
|   addNewProduct, | ||||
|   getProductSchema | ||||
| }; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| const { saveNewVariation } = require('src/variation.js') | ||||
| const { saveNewVariation, variationSchema } = require('src/variation.js') | ||||
| const Products = require('src/products'); | ||||
| const products = new Products(); | ||||
|  | ||||
| @@ -35,6 +35,13 @@ const addNewVariationToProduct = async (req, res) => { | ||||
|     .catch(err => handleError(err, res)) | ||||
| } | ||||
|  | ||||
|  | ||||
| const getVariationSchema = (req, res) => { | ||||
|   return variationSchema() | ||||
|     .then(schema => res.json(schema)) | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   addNewVariationToProduct | ||||
|   addNewVariationToProduct, | ||||
|   getVariationSchema | ||||
| }; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| const Product = require('schemas/Product'); | ||||
| const { slugify } = require('src/utils'); | ||||
| const Variation = require('schemas/Variation'); | ||||
| const { slugify, nulledSchema } = require('src/utils'); | ||||
|  | ||||
| class Products { | ||||
|   constructor() { | ||||
| @@ -29,6 +30,13 @@ class Products { | ||||
|     return Product.find().populate('variations'); | ||||
|   } | ||||
|  | ||||
|   productSchema() { | ||||
|     const product = { ...Product.schema.obj }; | ||||
|     const productSchema = nulledSchema(product); | ||||
|  | ||||
|     return Promise.resolve(productSchema); | ||||
|   } | ||||
|  | ||||
|   getBySlug(slug) { | ||||
|     return Product.findOne({ urlSlug: slug }).populate('variations'); | ||||
|   } | ||||
|   | ||||
| @@ -14,6 +14,12 @@ const slugify = (string) => { | ||||
|     .replace(/-+$/, '') // Trim - from end of text | ||||
| } | ||||
|  | ||||
| const nulledSchema = schema => Object.keys(schema).reduce((accumulator, current) => { | ||||
|   accumulator[current] = ""; | ||||
|   return accumulator; | ||||
| }, {}); | ||||
|  | ||||
| module.exports = { | ||||
|   slugify | ||||
|   slugify, | ||||
|   nulledSchema | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| const Variation = require('schemas/Variation'); | ||||
| const { nulledSchema } = require('src/utils'); | ||||
|  | ||||
| const updateVariation = () => { | ||||
|   return | ||||
| @@ -17,6 +18,15 @@ const saveNewVariation = async (variation) => { | ||||
|   return newVariation; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   saveNewVariation | ||||
| const variationSchema = () => { | ||||
|   const variation = { ...Variation.schema.obj }; | ||||
|   const variationSchema = nulledSchema(variation); | ||||
|  | ||||
|   return Promise.resolve(variationSchema); | ||||
| } | ||||
|  | ||||
|  | ||||
| module.exports = { | ||||
|   saveNewVariation, | ||||
|   variationSchema | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user