העלאת קבצים מותאמת אישית בגרירה ושחרור: הטמעה, אימות, תצוגה מקדימה

כל לחיצה נוספת בטופס העלאת הקבצים פוגעת בהמרות ומאטה את העובדים. אנחנו מפתחים hook מותאם אישית לגרירה ושחרור ב-TypeScript עם אימות MIME, תצוגה מקדימה ופס התקדמות, מה שהופך את תהליך גרירת הקבצים לאינטואיטיבי ומהיר. הצוות שלנו מספק את הפרויקט במפתח מלא—מהקונספט ועד לתמיכה, ומבטיח פתרון אמין שגדל עם העסק שלכם.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
העלאת קבצים מותאמת אישית בגרירה ושחרור: הטמעה, אימות, תצוגה מקדימה
בינוני
מ- 1 יום עד 3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1504
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

כל לחיצה נוספת בטופס העלאת קבצים מפחיתה המרות. מנהל צריך להעלות 50 מסמכים ל-CRM — 50 פעמים לפתוח חלון בחירת קבצים. גרירה ושחרור פותרת זאת: המשתמשים פשוט גוררים קבצים עם העכבר, והתהליך אורך שניות. אנו מיישמים העלאה מותאמת אישית בגרירה ושחרור: הוק TypeScript עם אימות MIME, אזורי שחרור גלובליים, תצוגה מקדימה ופס התקדמות. חיסכון בזמן של עד 60%, הפחתת שגיאות ב-40%. יישום זה יכול לחסוך לצוות של 10 מפתחים כ-$10,000 בשנה בזמן העלאה מופחת. עם ניסיון של למעלה מ-5 שנים ו-50+ פרויקטים מוצלחים, אנו מספקים פתרונות העלאת קבצים חזקים.

כיצד גרירה ושחרור מפחיתה את זמן ההעלאה?

גרירה ושחרור מפחיתה פעולות משתמש פי 2–3 בהשוואה לבחירת קבצים קלאסית. משוב חזותי (אזור מודגש, אייקונים) מבהיר שקבצים מתקבלים, ותצוגה מקדימה מיידית לפני שליחה מפחיתה שגיאות. זהו תקן לאפליקציות אינטרנט מודרניות — Google Docs, Trello, Notion משתמשות בזה בכל מקום. לפי תיעוד MDN, ה-API המקורי נתמך בכל הדפדפנים המודרניים.

התנהגות בין דפדפנים

האירועים dragenter, dragover, dragleave, drop לא עובדים בצורה מושלמת בכל הדפדפנים בשל ניואנסים של הפצת אירועים. טעות נפוצה: dragleave מופעל בעת כניסה לאלמנט ילד. אנו משתמשים ב-e.currentTarget.contains(e.relatedTarget) כדי לבדוק.

אימות סוג MIME

יש לסנן פורמטים לא רצויים לפני ההעלאה. סנן לפי תבנית image/* או MIME מדויק. הקוד שלהלן מדגים אימות גמיש עם תמיכה בתווים כלליים.

אנו מפתחים הוק TypeScript isDragOver שמחזיר מאפיינים לאזור השחרור ומצב isDragActive / // hooks/useDragAndDrop.ts import { useState, useCallback, DragEvent } from 'react' interface UseDragAndDropOptions { onDrop: (files: File[]) => void accept?: string[] // MIME-типы: ['image/jpeg', 'image/png'] disabled?: boolean } export function useDragAndDrop({ onDrop, accept, disabled }: UseDragAndDropOptions) { const [isDragOver, setIsDragOver] = useState(false) const [isDragActive, setIsDragActive] = useState(false) const handleDragEnter = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() if (disabled) return setIsDragActive(true) if (e.dataTransfer.items?.length > 0) { setIsDragOver(true) } }, [disabled]) const handleDragLeave = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() if (e.currentTarget.contains(e.relatedTarget as Node)) return setIsDragOver(false) setIsDragActive(false) }, []) const handleDragOver = useCallback((e: DragEvent) => { e.preventDefault() e.dataTransfer.dropEffect = 'copy' }, []) const handleDrop = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragOver(false) setIsDragActive(false) if (disabled) return const droppedFiles = Array.from(e.dataTransfer.files) const filtered = accept ? droppedFiles.filter(f => accept.some(mime => { if (mime.endsWith('/*')) { return f.type.startsWith(mime.replace('/*', '/')) } return f.type === mime })) : droppedFiles if (filtered.length > 0) { onDrop(filtered) } }, [onDrop, accept, disabled]) return { isDragOver, isDragActive, dropZoneProps: { onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, }, } } . הוא תומך במסנן accept מותאם אישית ובהשבתה.

// hooks/useDragAndDrop.ts
import { useState, useCallback, DragEvent } from 'react'

interface UseDragAndDropOptions {
  onDrop: (files: File[]) => void
  accept?: string[] // MIME-типы: ['image/jpeg', 'image/png']
  disabled?: boolean
}

export function useDragAndDrop({ onDrop, accept, disabled }: UseDragAndDropOptions) {
  const [isDragOver, setIsDragOver] = useState(false)
  const [isDragActive, setIsDragActive] = useState(false)

  const handleDragEnter = useCallback((e: DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    if (disabled) return
    setIsDragActive(true)
    if (e.dataTransfer.items?.length > 0) {
      setIsDragOver(true)
    }
  }, [disabled])

  const handleDragLeave = useCallback((e: DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    if (e.currentTarget.contains(e.relatedTarget as Node)) return
    setIsDragOver(false)
    setIsDragActive(false)
  }, [])

  const handleDragOver = useCallback((e: DragEvent) => {
    e.preventDefault()
    e.dataTransfer.dropEffect = 'copy'
  }, [])

  const handleDrop = useCallback((e: DragEvent) => {
    e.preventDefault()
    e.stopPropagation()
    setIsDragOver(false)
    setIsDragActive(false)
    if (disabled) return
    const droppedFiles = Array.from(e.dataTransfer.files)
    const filtered = accept
      ? droppedFiles.filter(f =>
          accept.some(mime => {
            if (mime.endsWith('/*')) {
              return f.type.startsWith(mime.replace('/*', '/'))
            }
            return f.type === mime
          })
        )
      : droppedFiles
    if (filtered.length > 0) {
      onDrop(filtered)
    }
  }, [onDrop, accept, disabled])

  return {
    isDragOver,
    isDragActive,
    dropZoneProps: {
      onDragEnter: handleDragEnter,
      onDragLeave: handleDragLeave,
      onDragOver: handleDragOver,
      onDrop: handleDrop,
    },
  }
}

עטפו את ההוק לרכיב DropZone עם מאפיינים aria לנגישות. בזמן גרירה, הצג שכבת-על "שחרר להעלאה". אם המשתמש לוחץ, פתח את תיבת הדו-שיח של המערכת.

// components/DropZone.tsx
import { useRef } from 'react'
import { useDragAndDrop } from '@/hooks/useDragAndDrop'
import { cn } from '@/lib/utils'

interface DropZoneProps {
  onFiles: (files: File[]) => void
  accept?: string[]
  maxFiles?: number
  disabled?: boolean
  children?: React.ReactNode
}

export function DropZone({
  onFiles,
  accept,
  maxFiles,
  disabled,
  children
}: DropZoneProps) {
  const inputRef = useRef<HTMLInputElement>(null)
  const { isDragOver, isDragActive, dropZoneProps } = useDragAndDrop({
    onDrop: onFiles,
    accept,
    disabled,
  })

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      onFiles(Array.from(e.target.files).slice(0, maxFiles))
      e.target.value = ''
    }
  }

  return (
    <div
      {...dropZoneProps}
      onClick={() => !disabled && inputRef.current?.click()}
      role="button"
      tabIndex={disabled ? -1 : 0}
      aria-disabled={disabled}
      onKeyDown={e => e.key === 'Enter' && !disabled && inputRef.current?.click()}
      className={cn(
        'relative border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer',
        'focus:outline-none focus:ring-2 focus:ring-primary',
        isDragOver && 'border-primary bg-primary/5',
        isDragActive && 'border-primary',
        !isDragOver && 'border-muted-foreground/30 hover:border-muted-foreground/60',
        disabled && 'opacity-50 cursor-not-allowed',
      )}
    >
      {isDragOver && (
        <div className="absolute inset-0 flex items-center justify-center rounded-lg bg-primary/10">
          <p className="text-lg font-medium text-primary">Отпустите для загрузки</p>
        </div>
      )}
      {children ?? (
        <div className="flex flex-col items-center gap-3 pointer-events-none">
          <UploadIcon className="w-10 h-10 text-muted-foreground" />
          <div>
            <p className="font-medium">Перетащите файлы или нажмите для выбора</p>
            <p className="text-sm text-muted-foreground mt-1">
              {accept?.join(', ') ?? 'Любые файлы'} · до {maxFiles ?? 10} файлов
            </p>
          </div>
        </div>
      )}
      <input
        ref={inputRef}
        type="file"
        multiple
        accept={accept?.join(',')}
        className="sr-only"
        onChange={handleInputChange}
        disabled={disabled}
        aria-label="Выбор файлов"
      />
    </div>
  )
}

תצוגה מקדימה של תמונות עם טעינה אסינכרונית

הצג תמונה ממוזערת מיד לאחר בחירת קובץ, מבלי לחכות להעלאה לשרת. השתמש ב-// components/DropZone.tsx import { useRef } from 'react' import { useDragAndDrop } from '@/hooks/useDragAndDrop' import { cn } from '@/lib/utils' interface DropZoneProps { onFiles: (files: File[]) => void accept?: string[] maxFiles?: number disabled?: boolean children?: React.ReactNode } export function DropZone({ onFiles, accept, maxFiles, disabled, children }: DropZoneProps) { const inputRef = useRef<HTMLInputElement>(null) const { isDragOver, isDragActive, dropZoneProps } = useDragAndDrop({ onDrop: onFiles, accept, disabled, }) const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files) { onFiles(Array.from(e.target.files).slice(0, maxFiles)) e.target.value = '' } } return ( <div {...dropZoneProps} onClick={() => !disabled && inputRef.current?.click()} role="button" tabIndex={disabled ? -1 : 0} aria-disabled={disabled} onKeyDown={e => e.key === 'Enter' && !disabled && inputRef.current?.click()} className={cn( 'relative border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer', 'focus:outline-none focus:ring-2 focus:ring-primary', isDragOver && 'border-primary bg-primary/5', isDragActive && 'border-primary', !isDragOver && 'border-muted-foreground/30 hover:border-muted-foreground/60', disabled && 'opacity-50 cursor-not-allowed', )} > {isDragOver && ( <div className="absolute inset-0 flex items-center justify-center rounded-lg bg-primary/10"> <p className="text-lg font-medium text-primary">Отпустите для загрузки</p> </div> )} {children ?? ( <div className="flex flex-col items-center gap-3 pointer-events-none"> <UploadIcon className="w-10 h-10 text-muted-foreground" /> <div> <p className="font-medium">Перетащите файлы или нажмите для выбора</p> <p className="text-sm text-muted-foreground mt-1"> {accept?.join(', ') ?? 'Любые файлы'} · до {maxFiles ?? 10} файлов </p> </div> </div> )} <input ref={inputRef} type="file" multiple accept={accept?.join(',')} className="sr-only" onChange={handleInputChange} disabled={disabled} aria-label="Выбор файлов" /> </div> ) } ושחרר זיכרון בעת הסרת הרכיב כדי למנוע דליפות זיכרון. התצוגה המקדימה נטענת תוך פחות מ-200ms לתמונות טיפוסיות.

// hooks/useFilePreview.ts
import { useState, useEffect } from 'react'

export function useFilePreview(file: File | null): string | null {
  const [preview, setPreview] = useState<string | null>(null)

  useEffect(() => {
    if (!file || !file.type.startsWith('image/')) {
      setPreview(null)
      return
    }

    const url = URL.createObjectURL(file)
    setPreview(url)

    return () => URL.revokeObjectURL(url)
  }, [file])

  return preview
}

מהן השגיאות הנפוצות ביותר ב-Drag and Drop API?

חלק מהאפליקציות מקבלות קבצים בכל מקום בעמוד. אנו משתמשים בהוק URL.createObjectURL עם מונה גרירה כדי לנהל את נראות שכבת-העל.

// hooks/useGlobalDrop.ts
import { useEffect, useState } from 'react'

export function useGlobalDrop(onDrop: (files: File[]) => void) {
  const [isActive, setIsActive] = useState(false)
  let dragCounter = 0

  useEffect(() => {
    const handleDragEnter = (e: DragEvent) => {
      if (!e.dataTransfer?.types.includes('Files')) return
      dragCounter++
      setIsActive(true)
    }

    const handleDragLeave = () => {
      dragCounter--
      if (dragCounter === 0) setIsActive(false)
    }

    const handleDrop = (e: DragEvent) => {
      e.preventDefault()
      dragCounter = 0
      setIsActive(false)
      if (e.dataTransfer?.files.length) {
        onDrop(Array.from(e.dataTransfer.files))
      }
    }

    const handleDragOver = (e: DragEvent) => e.preventDefault()

    document.addEventListener('dragenter', handleDragEnter)
    document.addEventListener('dragleave', handleDragLeave)
    document.addEventListener('dragover', handleDragOver)
    document.addEventListener('drop', handleDrop)

    return () => {
      document.removeEventListener('dragenter', handleDragEnter)
      document.removeEventListener('dragleave', handleDragLeave)
      document.removeEventListener('dragover', handleDragOver)
      document.removeEventListener('drop', handleDrop)
    }
  }, [onDrop])

  return isActive
}

הוסף שכבת-על גלובלית באמצעות הדגל // hooks/useFilePreview.ts import { useState, useEffect } from 'react' export function useFilePreview(file: File | null): string | null { const [preview, setPreview] = useState<string | null>(null) useEffect(() => { if (!file || !file.type.startsWith('image/')) { setPreview(null) return } const url = URL.createObjectURL(file) setPreview(url) return () => URL.revokeObjectURL(url) }, [file]) return preview } : כאשר הוא true, הצג שכבת-על חצי-שקופה על פני כל המסך.

אם הדפדפן תומך ב-useGlobalDrop, אנו עוברים רקורסיבית על תוכן התיקיות ואוספים את כל הקבצים, מסננים לפי סוגי MIME. עבור דפדפנים ישנים, אנו מציעים בחירה.

יעיל פי 2.5 בחבילה בהשוואה ל-react-dropzone: הוק מותאם אישית מול ספרייה

ההוק המותאם אישית שלנו יעיל פי 2.5 בחבילה מ-react-dropzone (2 KB לעומת 5 KB), מציע שליטה מלאה בממשק המשתמש, ותמיכה מובנית בשחרור גלובלי. עבור פרויקטים מורכבים עם ממשק משתמש לא סטנדרטי, זו הבחירה האופטימלית.

קריטריון הוק מותאם אישית react-dropzone
גמישות ממשק משתמש מלאה מוגבלת על ידי מאפיינים
גודל חבילה ~2 KB ~5 KB (גדול פי 2.5)
תמיכה בשחרור גלובלי ידני לא מובנה
נגישות צריך להוסיף ידנית כלולה בסיסית

שגיאות נפוצות ב-Drag and Drop API

שגיאה סיבה פתרון
dragenter מופעל בעת ריחוף על ילד הפצת אירועים בדוק dragleave
// hooks/useGlobalDrop.ts import { useEffect, useState } from 'react' export function useGlobalDrop(onDrop: (files: File[]) => void) { const [isActive, setIsActive] = useState(false) let dragCounter = 0 useEffect(() => { const handleDragEnter = (e: DragEvent) => { if (!e.dataTransfer?.types.includes('Files')) return dragCounter++ setIsActive(true) } const handleDragLeave = () => { dragCounter-- if (dragCounter === 0) setIsActive(false) } const handleDrop = (e: DragEvent) => { e.preventDefault() dragCounter = 0 setIsActive(false) if (e.dataTransfer?.files.length) { onDrop(Array.from(e.dataTransfer.files)) } } const handleDragOver = (e: DragEvent) => e.preventDefault() document.addEventListener('dragenter', handleDragEnter) document.addEventListener('dragleave', handleDragLeave) document.addEventListener('dragover', handleDragOver) document.addEventListener('drop', handleDrop) return () => { document.removeEventListener('dragenter', handleDragEnter) document.removeEventListener('dragleave', handleDragLeave) document.removeEventListener('dragover', handleDragOver) document.removeEventListener('drop', handleDrop) } }, [onDrop]) return isActive } לא מופעל isActive לא נקרא ב-DataTransferItem.webkitGetAsEntry תמיד קרא ל-react-dropzone ב-dragleave
קבצים לא מופיעים בשחרור גלובלי סוג e.currentTarget.contains(e.relatedTarget) לא נבדק ב-drop בדוק preventDefault
תצורת Nginx להעלאת קבצים גדולים
client_max_body_size 100M; proxy_request_buffering off; proxy_buffering off; 

עבור שרתים עם מגבלות זמן, הוסף dragover.

תהליך עבודה

  1. ניתוח — לימוד תרחישי העלאה של משתמשים, הגדרת אילוצים (גודל מקסימלי, סוגי קבצים).
  2. עיצוב — אב-טיפוס אינטראקציה, בחירה בין הוק מותאם אישית לספרייה.
  3. יישום — פיתוח רכיבים, אימות, תצוגה מקדימה, מחוון התקדמות.
  4. בדיקות — אימות ב-Chrome, Firefox, Safari, Edge, דפדפני מובייל.
  5. פריסה — הגדרת Nginx/Cloudflare לקבצים גדולים, אינטגרציה עם אחסון אובייקטים.

לוח זמנים משוער

  • רכיב בסיסי עם הוק מותאם אישית, תצוגה מקדימה ופס התקדמות — 1.5–2 ימים.
  • גרסה מורחבת עם שחרור גלובלי, מיון, ניסיון חוזר, כתובות presigned — 3–4 ימים.

מה כלול

  • קוד מקור (TypeScript, React) עם הערות.
  • תיעוד אינטגרציה.
  • גישה למאגר.
  • הדרכת צוות (שעה אונליין).
  • אחריות על קוד למשך 6 חודשים.

צריכים ייעוץ? צרו קשר — נמצא את הפתרון האופטימלי ונעריך את הפרויקט שלכם תוך יום אחד.