mirror of
				https://github.com/KevinMidboe/planetposen-frontend.git
				synced 2025-10-29 13:10:12 +00:00 
			
		
		
		
	Warehouse gets image upload, edit images and all properties of product
This commit is contained in:
		
							
								
								
									
										174
									
								
								src/lib/components/ImageUpload.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/lib/components/ImageUpload.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| <script lang="ts"> | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import Button from './Button.svelte'; | ||||
|   import Loading from './loading/index.svelte'; | ||||
|  | ||||
|   const dispatch = createEventDispatcher(); | ||||
|  | ||||
|   let dragOver: boolean = false; | ||||
|   let fileInput: HTMLInputElement; | ||||
|  | ||||
|   /* eslint-disable @typescript-eslint/no-explicit-any */ | ||||
|   let resolveUploadPromise: (value: any) => void; | ||||
|   let rejectUploadPromise: (resason: any | null) => void; | ||||
|   let uploadPromise: Promise<any> | null; | ||||
|   /* eslint-enable @typescript-eslint/no-explicit-any */ | ||||
|  | ||||
|   async function uploadImages() { | ||||
|     const { files } = fileInput; | ||||
|     if (!files) return; | ||||
|  | ||||
|     for (let i = 0; i < files.length; i++) { | ||||
|       const file = files[i]; | ||||
|       await uploadImage(file); | ||||
|     } | ||||
|  | ||||
|     setTimeout(() => (fileInput.value = ''), 3000); | ||||
|   } | ||||
|  | ||||
|   function resetUploadInput() { | ||||
|     fileInput.value = ''; | ||||
|     uploadPromise = null; | ||||
|   } | ||||
|  | ||||
|   async function uploadImage(file: File) { | ||||
|     console.log('uploading file:', file); | ||||
|  | ||||
|     const formData = new FormData(); | ||||
|     formData.append('file', file); | ||||
|  | ||||
|     let url = '/api/v1/images'; | ||||
|     if (window?.location?.href?.includes('localhost')) { | ||||
|       url = 'http://localhost:8001' + url; | ||||
|     } | ||||
|  | ||||
|     const options = { | ||||
|       method: 'POST', | ||||
|       body: formData | ||||
|     }; | ||||
|  | ||||
|     startImageUploadLoader(); | ||||
|     // return | ||||
|  | ||||
|     try { | ||||
|       const imageUploadResponse = await fetch(url, options).then((resp) => resp.json()); | ||||
|       console.log('response from image upload:', imageUploadResponse); | ||||
|       resolveUploadPromise(true); | ||||
|  | ||||
|       const { remote_url } = imageUploadResponse; | ||||
|       if (remote_url) dispatch('added', remote_url); | ||||
|     } catch (error) { | ||||
|       console.log('Unexpected error from upload:', error); | ||||
|       rejectUploadPromise(error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function startImageUploadLoader() { | ||||
|     uploadPromise = new Promise((resolve, reject) => { | ||||
|       resolveUploadPromise = resolve; | ||||
|       rejectUploadPromise = reject; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   function onFileSelected(event: Event) { | ||||
|     let target = event.target as HTMLInputElement; | ||||
|     if (!target) return; | ||||
|  | ||||
|     let file = target.files?.[0]; | ||||
|     if (!file) return; | ||||
|     fileInput.files = target.files; | ||||
|     // uploadImage(file) | ||||
|   } | ||||
|  | ||||
|   function onFileDrop(event: Event) { | ||||
|     const { files } = event?.dataTransfer; | ||||
|     if (files) { | ||||
|       fileInput.files = files; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   $: hasFiles = fileInput?.files?.length > 0 || false; | ||||
| </script> | ||||
|  | ||||
| <div | ||||
|   id="drop_zone" | ||||
|   class="{dragOver ? 'highlighted' : ''}" | ||||
|   on:click="{() => fileInput.click()}" | ||||
|   on:drop|preventDefault="{onFileDrop}" | ||||
|   on:dragover|preventDefault="{() => (dragOver = true)}" | ||||
|   on:dragenter|preventDefault="{() => (dragOver = true)}" | ||||
|   on:dragleave|preventDefault="{() => (dragOver = false)}" | ||||
|   on:drop|preventDefault="{() => (dragOver = false)}" | ||||
| > | ||||
|   {#if !dragOver} | ||||
|     <p>Click or drag images <i>here</i> to upload</p> | ||||
|   {:else} | ||||
|     <p>Drop image(s) to add to uploads</p> | ||||
|   {/if} | ||||
| </div> | ||||
| <input | ||||
|   bind:this="{fileInput}" | ||||
|   type="file" | ||||
|   accept=".jpg, .jpeg, .png" | ||||
|   on:change="{onFileSelected}" | ||||
|   multiple | ||||
| /> | ||||
|  | ||||
| {#if hasFiles} | ||||
|   <span>Files found to upload:</span> | ||||
|  | ||||
|   <div class="thumbnails"> | ||||
|     {#each fileInput.files || [] as file} | ||||
|       <img src="{window.URL.createObjectURL(file)}" /> | ||||
|     {/each} | ||||
|   </div> | ||||
|  | ||||
|   <div class="upload"> | ||||
|     <Button text="upload" on:click="{uploadImages}" /> | ||||
|  | ||||
|     {#if uploadPromise} | ||||
|       <Loading promise="{uploadPromise}" /> | ||||
|     {/if} | ||||
|   </div> | ||||
| {/if} | ||||
|  | ||||
| <style lang="scss"> | ||||
|   input { | ||||
|     display: none; | ||||
|   } | ||||
|  | ||||
|   #drop_zone { | ||||
|     margin: 1rem 0; | ||||
|     padding: 0 1rem; | ||||
|     border: 2px dashed rgba(0, 0, 0, 0.4); | ||||
|     width: 300px; | ||||
|     text-align: center; | ||||
|     cursor: pointer; | ||||
|  | ||||
|     p { | ||||
|       margin: 0.75rem 0; | ||||
|     } | ||||
|     &.highlighted { | ||||
|       color: darkslategray; | ||||
|       background-color: rgb(215, 247, 194); | ||||
|       border-color: rgb(0, 105, 8); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .upload { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     margin-top: 1rem; | ||||
|   } | ||||
|  | ||||
|   .thumbnails { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|  | ||||
|     img { | ||||
|       width: 80px; | ||||
|       margin: 0 0.5rem 0.5rem 0; | ||||
|       border-radius: 4px; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
| @@ -4,6 +4,7 @@ export interface IProduct { | ||||
|   subtext?: string; | ||||
|   description?: string; | ||||
|   images?: IImage[]; | ||||
|   image?: string; | ||||
|   primary_color?: string; | ||||
|  | ||||
|   variation_count?: string; | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
|   import { goto } from '$app/navigation'; | ||||
|   import ProductList from './WarehouseProductList.svelte'; | ||||
|   import Button from '$lib/components/Button.svelte'; | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
|   import { buildApiUrl } from '$lib/utils/apiUrl'; | ||||
|   import type { IProduct } from '$lib/interfaces/IProduct'; | ||||
|   import type { PageData } from './$types'; | ||||
|  | ||||
| @@ -9,22 +11,18 @@ | ||||
|   const products = data.products as Array<IProduct>; | ||||
|  | ||||
|   async function createProduct() { | ||||
|     let url = '/api/product'; | ||||
|     if (window.location.href.includes('localhost')) { | ||||
|       url = 'http://localhost:30010'.concat(url); | ||||
|     } | ||||
|  | ||||
|     const url = buildApiUrl('/api/v1/product'); | ||||
|     fetch(url, { method: 'POST' }) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then((response) => { | ||||
|         console.log('response::', response); | ||||
|         const { product } = response; | ||||
|         goto(`/warehouse/${product.product_no} `); | ||||
|         goto(`/warehouse/${product?.product_no} `); | ||||
|       }); | ||||
|   } | ||||
|   console.log('warehouse:', products); | ||||
| </script> | ||||
|  | ||||
| <PageMeta title="Warehouse" description="View and edit products in warehouse/stock" /> | ||||
| <div class="warehouse-page"> | ||||
|   <h1>Warehouse</h1> | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,6 @@ | ||||
| </table> | ||||
|  | ||||
| <style lang="scss"> | ||||
|   // @import "../styles/global.scss"; | ||||
|   @import '../../styles/media-queries.scss'; | ||||
|  | ||||
|   table { | ||||
| @@ -85,7 +84,10 @@ | ||||
|     .image-column { | ||||
|       width: 4rem; | ||||
|       max-width: 4rem; | ||||
|       margin: 0 0.5rem; | ||||
|  | ||||
|       @include desktop { | ||||
|         margin: 0 0.5rem; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .name-and-price > p { | ||||
| @@ -98,7 +100,6 @@ | ||||
|  | ||||
|     td, | ||||
|     th { | ||||
|       white-space: nowrap; | ||||
|       padding: 0.4rem 0.6rem; | ||||
|     } | ||||
|  | ||||
| @@ -109,15 +110,9 @@ | ||||
|  | ||||
|       img { | ||||
|         width: 4rem; | ||||
|         height: 4rem; | ||||
|         border-radius: 0.4rem; | ||||
|       } | ||||
|  | ||||
|       .image-column { | ||||
|         display: grid; | ||||
|         place-items: center; | ||||
|       } | ||||
|  | ||||
|       tr { | ||||
|         border-bottom: 1px solid rgba(0, 0, 0, 0.05); | ||||
|         cursor: pointer; | ||||
| @@ -128,12 +123,6 @@ | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // @include mobile { | ||||
|     //   tr > *:first-child { | ||||
|     //     display: none; | ||||
|     //   } | ||||
|     // } | ||||
|  | ||||
|     @include mobile { | ||||
|       tr > *:last-child, | ||||
|       tr > :nth-child(4) { | ||||
|   | ||||
| @@ -1,18 +1,16 @@ | ||||
| import { dev } from '$app/environment'; | ||||
| import { env } from '$env/dynamic/private'; | ||||
| import type { PageServerLoad } from './$types'; | ||||
|  | ||||
| export const load: PageServerLoad = async ({ fetch, params }) => { | ||||
|   const { id } = params; | ||||
|  | ||||
|   // let url = `/api/warehouse/product/${id}`; | ||||
|   let url = `/api/warehouse/${id}`; | ||||
|   if (dev || env.API_HOST) { | ||||
|     url = (env.API_HOST || 'http://localhost:30010').concat(url); | ||||
|   } | ||||
|   const productRes = await fetch(`/api/v1/warehouse/${id}`); | ||||
|   const { product } = await productRes.json(); | ||||
|  | ||||
|   const res = await fetch(url); | ||||
|   const product = await res.json(); | ||||
|   console.log('product::', product); | ||||
|   return product; | ||||
|   const auditRes = await fetch(`/api/v1/warehouse/${id}/audit`); | ||||
|   const { logs } = await auditRes.json(); | ||||
|  | ||||
|   return { | ||||
|     product, | ||||
|     logs | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -2,41 +2,76 @@ | ||||
|   import PricingSection from './PricingSection.svelte'; | ||||
|   import DetailsSection from './DetailsSection.svelte'; | ||||
|   import Button from '$lib/components/Button.svelte'; | ||||
|   import { buildApiUrl } from '$lib/utils/apiUrl'; | ||||
|   import type { PageServerData } from './$types'; | ||||
|   import type { IProduct } from '$lib/interfaces/IProduct'; | ||||
|   import type IProductVariation from '$lib/interfaces/IProductVariation'; | ||||
|  | ||||
|   export let data: PageServerData; | ||||
|   const product = data.product as IProduct; | ||||
|   let product = data.product as IProduct; | ||||
|   let logs = data.logs; | ||||
|   let form: HTMLFormElement; | ||||
|   let edit = false; | ||||
|  | ||||
|   function save() { | ||||
|     console.log('savvvving'); | ||||
|   function saveProduct(event: SubmitEvent) { | ||||
|     const formData = new FormData(event.target as HTMLFormElement); | ||||
|     const productUpdate = { | ||||
|       name: formData.get('name'), | ||||
|       subtext: formData.get('subtext'), | ||||
|       description: formData.get('description'), | ||||
|       primary_color: formData.get('primary_color') | ||||
|     } as IProduct; | ||||
|  | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}`); | ||||
|     const options = { | ||||
|       method: 'PUT', | ||||
|       body: JSON.stringify(productUpdate), | ||||
|       headers: { 'Content-Type': 'application/json' } | ||||
|     }; | ||||
|  | ||||
|     fetch(url, options) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then((response) => (product = response?.product || product)); | ||||
|     edit = false; | ||||
|   } | ||||
|  | ||||
|   function sumVariations(variations: IProductVariation[] | undefined) { | ||||
|     if (!variations) return 0; | ||||
|  | ||||
|     let sum = 0; | ||||
|     variations.forEach((variation: IProductVariation) => (sum += variation.stock)); | ||||
|     return sum; | ||||
|   } | ||||
|  | ||||
|   $: totalVariations = sumVariations(product.variations); | ||||
|   $: displayImage = | ||||
|     product?.images?.find((image) => image?.default_image)?.url || product?.images?.[0]?.url || ''; | ||||
| </script> | ||||
|  | ||||
| <h1>Product details</h1> | ||||
| <div class="info row"> | ||||
|   <img src="{product.image}" alt="Product" /> | ||||
|   <div class="name-and-price"> | ||||
|     <p>{product.name}</p> | ||||
|     <p>NOK {product?.price}</p> | ||||
| <form bind:this="{form}" on:submit|preventDefault="{saveProduct}"> | ||||
|   <div class="info row"> | ||||
|     <img src="{displayImage}" alt="Product" /> | ||||
|     <div class="name-and-price"> | ||||
|       <p>{product.name}</p> | ||||
|       <p>Left in stock: {totalVariations}</p> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="edit-button"> | ||||
|   <div class="row edit-button"> | ||||
|     {#if !edit} | ||||
|       <Button text="Edit" on:click="{() => (edit = !edit)}" /> | ||||
|     {:else} | ||||
|       <Button text="Save" on:click="{save}" /> | ||||
|       <Button type="submit" text="Save" /> | ||||
|     {/if} | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <h2>Details</h2> | ||||
| <DetailsSection product="{product}" edit="{edit}" /> | ||||
|   <h2>Details</h2> | ||||
|   <DetailsSection bind:product="{product}" edit="{edit}" /> | ||||
| </form> | ||||
|  | ||||
| <h2>Variations</h2> | ||||
| <PricingSection product="{product}" /> | ||||
| <PricingSection bind:product="{product}" /> | ||||
|  | ||||
| <h2>Metadata</h2> | ||||
| <div> | ||||
| @@ -45,12 +80,24 @@ | ||||
|  | ||||
| <h2>Audit log</h2> | ||||
| <div> | ||||
|   <p class="empty">No logs</p> | ||||
|   {#if logs?.length > 0} | ||||
|     <pre class="audit-logs"> | ||||
|       {#each logs as log} | ||||
|         <code>{log?.changed_fields}</code> | ||||
|       {/each} | ||||
|     </pre> | ||||
|   {:else} | ||||
|     <p class="empty">No logs</p> | ||||
|   {/if} | ||||
| </div> | ||||
|  | ||||
| <style lang="scss"> | ||||
|   @import '../../../styles/media-queries.scss'; | ||||
|  | ||||
|   h1 { | ||||
|     word-break: break-all; | ||||
|   } | ||||
|  | ||||
|   .row { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
| @@ -65,25 +112,38 @@ | ||||
|   .info { | ||||
|     align-items: center; | ||||
|  | ||||
|     .edit-button { | ||||
|       margin-left: auto; | ||||
|     } | ||||
|  | ||||
|     img { | ||||
|       width: 64px; | ||||
|       height: 64px; | ||||
|       width: 4rem; | ||||
|       border-radius: 6px; | ||||
|     } | ||||
|  | ||||
|     .name-and-price > p { | ||||
|       margin: 0.5rem 0 0.5rem 2rem; | ||||
|  | ||||
|       &:first-of-type { | ||||
|         font-size: 1.5rem; | ||||
|       } | ||||
|  | ||||
|       &:nth-of-type(2) { | ||||
|         opacity: 0.6; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .edit-button { | ||||
|     margin-top: 1rem; | ||||
|  | ||||
|     @include tablet { | ||||
|       justify-content: end; | ||||
|       margin-top: -3rem; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   pre.audit-logs { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|   } | ||||
|  | ||||
|   .label, | ||||
|   .empty { | ||||
|     color: grey; | ||||
|   | ||||
| @@ -1,8 +1,48 @@ | ||||
| <script lang="ts"> | ||||
|   import Time from '$lib/components/Time.svelte'; | ||||
|   import ImageUpload from '$lib/components/ImageUpload.svelte'; | ||||
|   import { buildApiUrl } from '$lib/utils/apiUrl'; | ||||
|   import type { IProduct } from '$lib/interfaces/IProduct'; | ||||
|  | ||||
|   export let product: IProduct; | ||||
|   export let edit: boolean; | ||||
|  | ||||
|   async function addImage(event: CustomEvent) { | ||||
|     const imageUrl = event.detail; | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}/image`); | ||||
|     const options = { | ||||
|       method: 'POST', | ||||
|       body: JSON.stringify({ | ||||
|         url: imageUrl | ||||
|       }), | ||||
|       headers: { 'Content-Type': 'application/json' } | ||||
|     }; | ||||
|  | ||||
|     const resp = await fetch(url, options); | ||||
|     const { images } = await resp.json(); | ||||
|  | ||||
|     product.images = images; | ||||
|   } | ||||
|  | ||||
|   async function deleteImage(image_id: number) { | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}/image/${image_id}`); | ||||
|     const resp = await fetch(url, { method: 'DELETE' }); | ||||
|     const { images } = await resp.json(); | ||||
|  | ||||
|     console.log('received images from DELETE:', images); | ||||
|     product.images = images; | ||||
|  | ||||
|     console.log('product.images:', product.images); | ||||
|   } | ||||
|  | ||||
|   function setDefault(image_id: number) { | ||||
|     if (!product.images) return; | ||||
|  | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}/image/${image_id}/default`); | ||||
|     fetch(url, { method: 'POST' }) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then((response) => (product.images = response?.images)); | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <div class="details row"> | ||||
| @@ -13,39 +53,54 @@ | ||||
|         {#if !edit} | ||||
|           <span>{product.name}</span> | ||||
|         {:else} | ||||
|           <input bind:value="{product.name}" /> | ||||
|           <input name="name" class="wide" id="name" bind:value="{product.name}" /> | ||||
|         {/if} | ||||
|       </li> | ||||
|  | ||||
|       <li> | ||||
|         <span class="label">Created</span> | ||||
|         <span>{product.created}</span> | ||||
|         <Time time="{product.created}" /> | ||||
|       </li> | ||||
|  | ||||
|       <li> | ||||
|         <span class="label">Subtext</span> | ||||
|         <span class="label">Updated</span> | ||||
|         <Time time="{product.updated}" /> | ||||
|       </li> | ||||
|  | ||||
|       <li> | ||||
|         <label for="subtext" class="label">Subtext</label> | ||||
|         {#if !edit} | ||||
|           <span>{product.subtext || '(empty)'}</span> | ||||
|         {:else} | ||||
|           <input bind:value="{product.subtext}" /> | ||||
|           <input name="subtext" id="subtext" bind:value="{product.subtext}" /> | ||||
|         {/if} | ||||
|       </li> | ||||
|  | ||||
|       <li> | ||||
|         <span class="label">Description</span> | ||||
|         <label for="description" class="label">Description</label> | ||||
|         {#if !edit} | ||||
|           <span>{product.description || '(empty)'}</span> | ||||
|           <span class="description">{product.description || '(empty)'}</span> | ||||
|         {:else} | ||||
|           <input class="wide" bind:value="{product.description}" /> | ||||
|           <textarea | ||||
|             rows="6" | ||||
|             class="wide description" | ||||
|             name="description" | ||||
|             id="description" | ||||
|             bind:value="{product.description}"></textarea> | ||||
|         {/if} | ||||
|       </li> | ||||
|  | ||||
|       <li> | ||||
|         <span class="label">Primary color</span> | ||||
|         <label for="primary_color" class="label">Primary color</label> | ||||
|         {#if !edit} | ||||
|           <span>{product.primary_color || '(empty)'}</span> | ||||
|         {:else} | ||||
|           <input bind:value="{product.primary_color}" /> | ||||
|           <input | ||||
|             type="color" | ||||
|             name="primary_color" | ||||
|             id="primary_color" | ||||
|             bind:value="{product.primary_color}" | ||||
|           /> | ||||
|         {/if} | ||||
|         {#if product.primary_color} | ||||
|           <div class="color-box" style="{`--color: ${product.primary_color}`}"></div> | ||||
| @@ -60,8 +115,22 @@ | ||||
|   </div> | ||||
|  | ||||
|   <div class="right"> | ||||
|     <span class="label">Image</span> | ||||
|     <img src="{product.image}" alt="Product" /> | ||||
|     <span class="label">Images</span> | ||||
|     <div class="images-and-upload"> | ||||
|       <div class="images"> | ||||
|         {#each product?.images || [] as { url, default_image, image_id } (image_id)} | ||||
|           <div class="img" on:drop="{() => setDefault(image_id)}" on:dragover|preventDefault> | ||||
|             <img src="{url}" alt="Product" /> | ||||
|             <button class="delete" on:click="{() => deleteImage(image_id)}">❌</button> | ||||
|             {#if default_image} | ||||
|               <button class="default_image" draggable="true">🟣</button> | ||||
|             {/if} | ||||
|           </div> | ||||
|         {/each} | ||||
|       </div> | ||||
|  | ||||
|       <ImageUpload on:added="{addImage}" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| @@ -78,28 +147,69 @@ | ||||
|     } | ||||
|  | ||||
|     input { | ||||
|       margin: 0; | ||||
|       margin: -2px 0 -2px -2px; | ||||
|       padding: 0; | ||||
|     } | ||||
|  | ||||
|       &.wide { | ||||
|         width: -webkit-fill-available; | ||||
|       } | ||||
|     .wide { | ||||
|       width: 100%; | ||||
|       width: -webkit-fill-available; | ||||
|       margin-right: 0.5rem; | ||||
|     } | ||||
|  | ||||
|     span.description { | ||||
|       white-space: pre-line; | ||||
|     } | ||||
|  | ||||
|     textarea.description { | ||||
|       height: auto; | ||||
|     } | ||||
|  | ||||
|     .right { | ||||
|       width: 50%; | ||||
|       display: flex; | ||||
|  | ||||
|       @include mobile { | ||||
|         flex-direction: column; | ||||
|       } | ||||
|  | ||||
|       span { | ||||
|         padding: 0.4rem 0; | ||||
|         margin-right: 1.5rem; | ||||
|       } | ||||
|  | ||||
|       img { | ||||
|       .images-and-upload { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|  | ||||
|       .images .img { | ||||
|         position: relative; | ||||
|         display: inline-block; | ||||
|         width: 110px; | ||||
|         height: 110px; | ||||
|         border-radius: 4px; | ||||
|         margin-top: 0.4rem; | ||||
|         margin: 0 0.4rem 0.4rem 0; | ||||
|  | ||||
|         img { | ||||
|           width: inherit; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       button { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         border: none; | ||||
|         background-color: transparent; | ||||
|         cursor: pointer; | ||||
|         margin: 0; | ||||
|         padding: 0.25rem; | ||||
|       } | ||||
|       .delete { | ||||
|         right: 0; | ||||
|       } | ||||
|       .default_image { | ||||
|         cursor: move; | ||||
|         left: 0; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -127,7 +237,7 @@ | ||||
|         align-items: center; | ||||
|       } | ||||
|  | ||||
|       li span:first-of-type { | ||||
|       li span:first-of-type, li label:first-of-type { | ||||
|         display: inline-block; | ||||
|         margin-bottom: auto; | ||||
|         min-width: 30%; | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|   import Button from '$lib/components/Button.svelte'; | ||||
|   import Badge from '$lib/components/Badge.svelte'; | ||||
|   import BadgeType from '$lib/interfaces/BadgeType'; | ||||
|   import { buildApiUrl } from '$lib/utils/apiUrl'; | ||||
|  | ||||
|   export let product: IProduct; | ||||
|   let table: HTMLElement; | ||||
| @@ -13,61 +14,44 @@ | ||||
|     success: boolean; | ||||
|   } | ||||
|  | ||||
|   const setEditIndex = (index: number, event: MouseEvent) => { | ||||
|     if (!table.contains(event.target as Node)) { | ||||
|       console.log('outside?', index); | ||||
|       // editingVariationIndex = -1; | ||||
|     } else { | ||||
|       console.log('inside'); | ||||
|     } | ||||
|     editingVariationIndex = index; | ||||
|   }; | ||||
|   const resetEditingIndex = () => (editingVariationIndex = -1); | ||||
|   const updateProductsVariations = (response: ISkuResponse) => { | ||||
|     product.variations = response?.skus; | ||||
|     product.variations = response?.skus || []; | ||||
|     editingVariationIndex = product.variations.length - 1; | ||||
|   }; | ||||
|  | ||||
|   function setDefault(variation: IVariation) { | ||||
|     if (!product.variations) return; | ||||
|  | ||||
|     let url = `/api/product/${product.product_no}/sku/${variation.sku_id}/default_price`; | ||||
|     if (window?.location?.href.includes('localhost')) { | ||||
|       url = 'http://localhost:30010'.concat(url); | ||||
|     } | ||||
|     const url = buildApiUrl( | ||||
|       `/api/v1/product/${product.product_no}/sku/${variation.sku_id}/default` | ||||
|     ); | ||||
|     fetch(url, { method: 'POST' }) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then((response) => (product.variations = response?.skus)); | ||||
|   } | ||||
|  | ||||
|   function addSkuVariation() { | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}/sku`); | ||||
|     fetch(url, { method: 'POST' }) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then(updateProductsVariations); | ||||
|   } | ||||
|  | ||||
|   function addSkuVariation() { | ||||
|     // ADD OVER API - update product.variations with result --> set edit | ||||
|     // const newSku: IProductVariation = { | ||||
|     //   sku_id: null, | ||||
|     //   stock: 0, | ||||
|     //   size: null, | ||||
|     //   price: 0, | ||||
|     //   default_price: false | ||||
|     // } | ||||
|  | ||||
|     // if (!product.variations || product.variations.length === 0) { | ||||
|     //   product.variations = [] | ||||
|     // } | ||||
|  | ||||
|     // product.variations.push(newSku) | ||||
|     // editingVariationIndex = product.variations.length - 1 | ||||
|  | ||||
|     let url = `/api/product/${product.product_no}/sku`; | ||||
|     if (window?.location?.href.includes('localhost')) { | ||||
|       url = 'http://localhost:30010'.concat(url); | ||||
|     } | ||||
|  | ||||
|     fetch(url, { method: 'POST' }) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then(updateProductsVariations) | ||||
|       .then(() => (editingVariationIndex = product.variations.length - 1)); | ||||
|   } | ||||
|  | ||||
|   function saveSkuVariation(variation: IVariation) { | ||||
|     let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`; | ||||
|     if (window?.location?.href.includes('localhost')) { | ||||
|       url = 'http://localhost:30010'.concat(url); | ||||
|     } | ||||
|  | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}/sku/${variation?.sku_id}`); | ||||
|     const { stock, size, price } = variation; | ||||
|     const options = { | ||||
|       method: 'PATCH', | ||||
|       method: 'PUT', | ||||
|       body: JSON.stringify({ stock, price, size }), | ||||
|       headers: { | ||||
|         'Content-Type': 'application/json' | ||||
| @@ -81,29 +65,14 @@ | ||||
|   } | ||||
|  | ||||
|   function deleteVariation(variation: IVariation) { | ||||
|     console.log('delete it using api', variation); | ||||
|     let url = `/api/product/${product.product_no}/sku/${variation?.sku_id}`; | ||||
|     if (window?.location?.href.includes('localhost')) { | ||||
|       url = 'http://localhost:30010'.concat(url); | ||||
|     } | ||||
|  | ||||
|     const url = buildApiUrl(`/api/v1/product/${product.product_no}/sku/${variation?.sku_id}`); | ||||
|     fetch(url, { method: 'DELETE' }) | ||||
|       .then((resp) => resp.json()) | ||||
|       .then(updateProductsVariations) | ||||
|       .then(() => resetEditingIndex()); | ||||
|   } | ||||
|  | ||||
|   function disableEditIfClickOutsideTable(event: MouseEvent) { | ||||
|     console.log('target:', event.target); | ||||
|     if (!table.contains(event.target as Node)) { | ||||
|       console.log('outside?'); | ||||
|     } else { | ||||
|       console.log('inside'); | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <svelte:window on:click="{disableEditIfClickOutsideTable}" /> | ||||
| <table class="pricing" bind:this="{table}"> | ||||
|   <thead> | ||||
|     <tr> | ||||
| @@ -123,36 +92,31 @@ | ||||
|   <tbody> | ||||
|     {#if product?.variations?.length} | ||||
|       {#each product?.variations as variation, index} | ||||
|         {#if editingVariationIndex !== index} | ||||
|           <tr | ||||
|             on:click="{() => (editingVariationIndex = index)}" | ||||
|             on:drop="{() => setDefault(variation)}" | ||||
|             on:dragover|preventDefault | ||||
|           > | ||||
|             <td> | ||||
|         <tr | ||||
|           class="edit" | ||||
|           on:click="{(event) => setEditIndex(index, event)}" | ||||
|           on:drop="{() => setDefault(variation)}" | ||||
|           on:dragover|preventDefault | ||||
|         > | ||||
|           <td> | ||||
|             {#if editingVariationIndex !== index} | ||||
|               <span>Nok {variation.price}</span> | ||||
|               {#if variation.default_price} | ||||
|                 <div draggable="true"> | ||||
|                   <Badge title="Default price" type="{BadgeType.PENDING}" icon="{''}" /> | ||||
|                 </div> | ||||
|               {/if} | ||||
|             </td> | ||||
|             {:else} | ||||
|               <span>Nok <input type="number" bind:value="{variation.price}" /></span> | ||||
|             {/if} | ||||
|  | ||||
|             {#if variation.default_price} | ||||
|               <div draggable="true"> | ||||
|                 <Badge title="Default price" type="{BadgeType.PENDING}" icon="" /> | ||||
|               </div> | ||||
|             {/if} | ||||
|           </td> | ||||
|           {#if editingVariationIndex !== index} | ||||
|             <td>{variation.stock}</td> | ||||
|             <td>{variation.size}</td> | ||||
|             <td></td> | ||||
|             <td></td> | ||||
|           </tr> | ||||
|         {:else} | ||||
|           <tr class="edit" on:drop="{() => setDefault(variation)}" on:dragover|preventDefault> | ||||
|             <td> | ||||
|               <span>Nok <input type="number" bind:value="{variation.price}" /></span> | ||||
|  | ||||
|               {#if variation.default_price} | ||||
|                 <div draggable="true"> | ||||
|                   <Badge title="Default price" type="{BadgeType.PENDING}" icon="{''}" /> | ||||
|                 </div> | ||||
|               {/if} | ||||
|             </td> | ||||
|           {:else} | ||||
|             <td><input type="number" bind:value="{variation.stock}" /></td> | ||||
|             <td><input bind:value="{variation.size}" /></td> | ||||
|             <td class="cta"> | ||||
| @@ -161,8 +125,8 @@ | ||||
|             <td class="cta"> | ||||
|               <button on:click="{() => deleteVariation(variation)}">🗑️</button> | ||||
|             </td> | ||||
|           </tr> | ||||
|         {/if} | ||||
|           {/if} | ||||
|         </tr> | ||||
|       {/each} | ||||
|     {/if} | ||||
|   </tbody> | ||||
|   | ||||
| @@ -1,16 +1,9 @@ | ||||
| import { dev } from '$app/environment'; | ||||
| import { env } from '$env/dynamic/private'; | ||||
| import type { PageServerLoad } from './$types'; | ||||
|  | ||||
| export const load: PageServerLoad = async ({ fetch, params }) => { | ||||
|   const { id } = params; | ||||
|  | ||||
|   let url = `/api/warehouse/${id}`; | ||||
|   if (dev || env.API_HOST) { | ||||
|     url = (env.API_HOST || 'http://localhost:30010').concat(url); | ||||
|   } | ||||
|  | ||||
|   const res = await fetch(url); | ||||
|   const res = await fetch(`/api/v1/warehouse/${id}`); | ||||
|   const product = await res.json(); | ||||
|   return product; | ||||
| }; | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  | ||||
|   <div> | ||||
|     <h2>Images</h2> | ||||
|     <img src="{product.image}" /> | ||||
|     <img src="{product.image}" alt="Default product" /> | ||||
|   </div> | ||||
|  | ||||
|   <div class="variations"> | ||||
| @@ -48,9 +48,9 @@ | ||||
|       <option>Wine</option> | ||||
|     </select> | ||||
|  | ||||
|     <Input label="Price" type="number" /> | ||||
|     <Input label="Stock" type="number" /> | ||||
|     <input type="checkbox" checked /> | ||||
|     <Input label="Price" type="number" value="" /> | ||||
|     <Input label="Stock" type="number" value="" /> | ||||
|     <input type="checkbox" checked value="" /> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user