@bonhomie/cloudinary-super-uploader
Version:
A powerful React + Node Cloudinary toolkit with drag & drop, browser compression, EXIF checks, duplicate detection, signed uploads, and more.
2 lines (1 loc) • 5.4 kB
JavaScript
import c,{useRef as J,useState as P,useEffect as K}from"react";import{useCallback as L,useState as A}from"react";function z(a,t=2e3,n=2e3){return new Promise(l=>{let r=new Image,i=new FileReader;i.onload=s=>{r.src=s.target.result},r.onload=()=>{let{width:s,height:p}=r,f=Math.min(t/s,n/p,1);s=s*f,p=p*f;let g=document.createElement("canvas");g.width=s,g.height=p,g.getContext("2d").drawImage(r,0,0,s,p),g.toBlob(h=>{let v=new File([h],a.name,{type:"image/jpeg",lastModified:Date.now()});l({success:!0,file:v})},"image/jpeg",.8)},i.onerror=()=>l({success:!1,error:"FileReader failed"}),i.readAsDataURL(a)})}import G from"exif-parser";function T(a){return new Promise(t=>{let n=new FileReader;n.onload=l=>{try{let r=l.target.result,s=G.create(r).parse();t({success:!0,data:s.tags})}catch(r){t({success:!1,error:r.message})}},n.onerror=()=>t({success:!1,error:"EXIF read failed"}),n.readAsArrayBuffer(a)})}async function $(a){try{let t=await a.arrayBuffer(),n=await crypto.subtle.digest("SHA-1",t);return{success:!0,hash:Array.from(new Uint8Array(n)).map(i=>i.toString(16).padStart(2,"0")).join("")}}catch(t){return{success:!1,error:t.message}}}function C(a,t=600,n=600){return new Promise(l=>{let r=new Image,i=new FileReader;i.onload=s=>{r.src=s.target.result},r.onload=()=>{r.width<t||r.height<n?l({success:!1,error:`Image too small: ${r.width}x${r.height} (min ${t}x${n})`}):l({success:!0,width:r.width,height:r.height})},r.onerror=()=>l({success:!1,error:"Invalid image"}),i.readAsDataURL(a)})}function S(a,t={}){if(!a||typeof a!="string")return a;let{width:n=400,height:l=400,crop:r="fill",quality:i="auto",format:s="auto"}=t;if(!a.includes("/upload/"))return a;let p=`c_${r},w_${n},h_${l},q_${i},f_${s}`;return a.replace("/upload/",`/upload/${p}/`)}function E(a={}){let{uploadUrl:t,maxWidth:n=2e3,maxHeight:l=2e3,minWidth:r=600,minHeight:i=600,maxFiles:s=10,allowedTypes:p=["image/jpeg","image/png","image/webp"],autoCompress:f=!0,maxAgeDays:g=365}=a,[m,h]=A([]),[v,D]=A(0),[k,B]=A(!1),[O,w]=A([]),_=L(async I=>{let F=Array.from(I),e=[];if(m.length+F.length>s){w(o=>[...o,"Too many files"]);return}for(let o of F){if(!p.includes(o.type)){w(u=>[...u,`Invalid file type: ${o.type}`]);continue}let b=await C(o,r,i);if(!b.success){w(u=>[...u,b.error]);continue}let j=await T(o),y=await $(o);if(m.some(u=>u.hash===y.hash)){w(u=>[...u,"Duplicate image skipped"]);continue}let U=o;if(f){let u=await z(o,n,l);u.success&&(U=u.file)}e.push({file:U,hash:y.hash,exif:j})}if(e.length===0)return;B(!0),D(0);let d=[];for(let o=0;o<e.length;o++){let{file:b,hash:j,exif:y}=e[o],U=new FormData;U.append("file",b);let x=await(await fetch(t,{method:"POST",body:U})).json();if(x.success){let N=[];if(y.success&&y.data.DateTimeOriginal){let X=new Date(y.data.DateTimeOriginal*1e3);(Date.now()-X.getTime())/(1e3*60*60*24)>g&&N.push("Old photo")}let W=x.data.thumbnail||S(x.data.url,{width:400,height:400});d.push({url:x.data.url,thumbnail:W,publicId:x.data.publicId,width:x.data.width,height:x.data.height,hash:j,exif:y.data||null,warnings:N})}else w(N=>[...N,x.error||"Upload error"]);D(Math.round((o+1)/e.length*100))}h(o=>[...o,...d]),B(!1)},[m,t,p,f,n,l,r,i,s,g]),H=L(I=>{h(F=>F.filter(e=>e.publicId!==I))},[]),M=L(I=>{h(I)},[]);return{images:m,progress:v,uploading:k,errors:O,upload:_,removeImage:H,reorderImages:M}}function q(a){let{images:t,progress:n,uploading:l,errors:r,upload:i,removeImage:s,reorderImages:p}=E(a),f=J(null),[g,m]=P(!1),[h,v]=P(null),[D,k]=P(null),B=e=>()=>{k(e)},O=e=>d=>{if(d.preventDefault(),D===null||D===e)return;let o=[...t],[b]=o.splice(D,1);o.splice(e,0,b),p(o),k(e)},w=e=>{e.preventDefault(),k(null)};return K(()=>{if(!r||r.length===0)return;let e=r[r.length-1];v(e);let d=setTimeout(()=>v(null),4e3);return()=>clearTimeout(d)},[r]),c.createElement("div",{className:"bon-cloud-container"},h&&c.createElement("div",{className:"bon-toast"},h),c.createElement("div",{className:`bon-dropzone ${g?"bon-dropzone--active":""}`,onDragOver:e=>{e.preventDefault(),m(!0)},onDragLeave:e=>{e.preventDefault(),m(!1)},onDrop:e=>{e.preventDefault(),m(!1);let d=e.dataTransfer.files;i(d)},onClick:()=>{f.current&&f.current.click()}},c.createElement("p",{className:"bon-dropzone-text"},"Drag & Drop or ",c.createElement("span",{className:"bon-dropzone-highlight"},"Click")," ","to Upload"),c.createElement("input",{ref:f,type:"file",multiple:!0,hidden:!0,onChange:e=>{let d=e.target.files;i(d)},accept:"image/*"})),l&&c.createElement("div",{className:"bon-progress"},c.createElement("div",{className:"bon-bar",style:{width:`${n}%`}})),r.length>0&&c.createElement("div",{className:"bon-errors"},r.map((e,d)=>c.createElement("p",{key:d,className:"bon-error-item"},e))),c.createElement("div",{className:"bon-grid",onDragOver:e=>e.preventDefault(),onDrop:w},t.map((e,d)=>c.createElement("div",{key:e.publicId||d,className:"bon-item",draggable:!0,onDragStart:B(d),onDragEnter:O(d)},c.createElement("img",{src:e.thumbnail||e.url,alt:"",className:"bon-item-img"}),e.warnings&&e.warnings.length>0&&c.createElement("div",{className:"bon-item-warnings"},e.warnings.map((o,b)=>c.createElement("span",{key:b,className:"bon-warning-badge"},o))),c.createElement("button",{type:"button",onClick:()=>s(e.publicId),className:"bon-remove"},"\u2715")))))}export{q as CloudinaryUploader,S as buildThumbnailUrl,z as compressImageBrowser,T as getExifBrowser,$ as hashBrowser,E as useCloudinaryUpload,C as validateDimensions};