/* eslint-disable @next/next/no-img-element */ import dynamic from 'next/dynamic' import React, { useState, useEffect, useCallback, useRef } from 'react' import classnames from 'classnames' import { useDropzone, FileRejection } from 'react-dropzone' import { CSSTransition } from 'react-transition-group' import { useDrag } from '@use-gesture/react' import splitbee from '@splitbee/web' import { headTemplate } from 'utils/favicon' import { isTouchCapable } from 'utils/device' import { Typography } from 'components/typography' import { CodeHighlight } from 'components/code-highlight' import { SvgDropZone } from 'components/svgs/svg-drop-zone' import { SvgImageUpload } from 'components/svgs/svg-image-upload' import { SvgInfo } from 'components/svgs/svg-info' import { ONE_MB, ACCEPT_MIME_TYPES, getImageFileSizes, RECOMMENDED_MIME_TYPES, MIN_SIZE, MIN_PWA_SIZE, SVG_MIME_TYPE, downloadFile, } from 'utils/files' import { Button } from 'components/button' import { SvgCheck } from 'components/svgs/svg-check' import { SvgError } from 'components/svgs/svg-error' import { Checkbox } from 'components/checkbox' import { useToggle } from 'hooks/use-toggle' import { useMediaQueryContext } from 'components/media-query-provider' import styles from './index.module.scss' const Modal = dynamic(() => import('react-modal')) const Clipboard = dynamic(() => import('react-clipboard.js')) export type DragAndDropProps = { onFile: (hasFile: boolean) => void onGenerate: (image: File, pwa: boolean, darkMode: boolean) => Promise onError: (message: string) => void } const DragAndDrop = ({ onFile, onGenerate, onError }: DragAndDropProps) => { const { isMobile } = useMediaQueryContext() const [imageSizes, setImageSizes] = useState({ width: 0, height: 0 }) const [image, setImage] = useState() const [imageBase64, setImageBase64] = useState('') const [imageBgBase64, setImageBgBase64] = useState('') const [imageData, setImageData] = useState({ name: '', extension: '' }) const [title, setTitle] = useState('') const [isSquare, setIsSquare] = useState(false) const [isPngOrSvg, setIsPngOrSvg] = useState(false) const [isSvg, setIsSvg] = useState(false) const [is310px, setIs310px] = useState(false) const [is512px, setIs512px] = useState(false) const [isLoading, setIsLoading] = useState(false) const [pwa, togglePwa] = useToggle(false) const [darkMode, toggleDarkMode] = useToggle(false) const [zipData, setZipData] = useState() const [isModalOpen, setIsModalOpen] = useState(false) const modalBodyRef = useRef(null) const [copied, setCopied] = useState(false) const PWADisabled = !isSvg && !is512px let sizesCount = 16 if (isSvg) { sizesCount += 1 } if (pwa) { sizesCount += 3 } const bind = useDrag( (state) => { const { swipe, // [swipeX, swipeY] 0 if no swipe detected, -1 or 1 otherwise } = state const isSwipeDown = swipe[1] === 1 if (isSwipeDown && modalBodyRef.current?.scrollTop === 0) { setIsModalOpen(false) } }, { enabled: isTouchCapable(), filterTaps: true, } ) const generateImgFromCanvas = useCallback(() => { const canvas = document.createElement('canvas') canvas.width = imageSizes.width canvas.height = imageSizes.height const ctx = canvas.getContext('2d') if (ctx) { ctx.fillStyle = 'white' ctx.fillRect(0, 0, imageSizes.width, imageSizes.height) return canvas.toDataURL() } return '' }, [imageSizes]) useEffect(() => { const reader = new FileReader() reader.onloadend = () => { setImageBase64(reader.result as string) setImageBgBase64(generateImgFromCanvas()) } if (image) { reader.readAsDataURL(image) const matches = /(.+)(\..+)$/.exec(image.name) as RegExpExecArray const length = isMobile ? 15 : 30 const name = matches[1].length > length ? `${matches[1].slice(0, length)}..` : matches[1].slice(0, length) setImageData({ name, extension: matches[2] }) } }, [image, zipData, generateImgFromCanvas, isMobile]) useEffect(() => { if (!image) { return } else if ((isPngOrSvg && isSquare && is310px) || isSvg) { setTitle('Good to go!') } else if (isSquare && isPngOrSvg && !is310px && !isSvg) { setTitle('Your image should be higher than 310px') } else if ((isSquare && !isPngOrSvg && !is310px) || (!isSquare && !isPngOrSvg && is310px)) { setTitle('Your image has a few problems') } else if (isSquare && !isPngOrSvg && is310px) { setTitle('Your image should be in PNG or SVG') } else if (!isSquare && isPngOrSvg && is310px) { setTitle('Your image should be squared') } else { setTitle('Your image is not in a good place') } }, [image, isSquare, is310px, isPngOrSvg, isSvg]) const generateFavicon = async (file: File) => { try { setIsLoading(true) const zip = await onGenerate(file, pwa, darkMode) setZipData(zip) splitbee.track('Favicon generated', { type: file.type, size: file.size, pwa, darkMode, }) } catch (error: any) { const message = (error.message as string) || 'Something went wrong. Please try again.' onError(message) splitbee.track('Favicon error', { type: file.type, size: file.size, pwa, darkMode, message, }) } finally { setIsLoading(false) } } const onDownload = (zipData: ArrayBuffer) => { downloadFile(zipData, 'favicons.zip') splitbee.track('Download Favicon') } const resetImage = () => { setImage(undefined) onFile(false) setZipData(undefined) } const onDrop = async (acceptedFiles: File[], fileRejections: FileRejection[]) => { if (acceptedFiles.length) { const file = acceptedFiles[0] const sizes = await getImageFileSizes(file) setImageSizes(sizes) setIsSquare(sizes.width === sizes.height) setIsPngOrSvg(RECOMMENDED_MIME_TYPES.includes(file.type)) setIsSvg(file.type === SVG_MIME_TYPE) setIs310px(sizes.width >= MIN_SIZE && sizes.height >= MIN_SIZE) setIs512px(sizes.width >= MIN_PWA_SIZE && sizes.height >= MIN_PWA_SIZE) onError('') onFile(true) setImage(file) splitbee.track('Drop Accepted', { ...sizes, type: file.type, size: file.size, }) } else if (fileRejections.length) { const file = fileRejections[0].file let message = '' if (!Object.keys(ACCEPT_MIME_TYPES).includes(file.type)) { message = `The image file should be a ${Object.values(ACCEPT_MIME_TYPES).join(' or ')}` } else if (file.size > ONE_MB) { message = `The ${ACCEPT_MIME_TYPES[file.type]} file needs to be lower than 1 MB` } else { message = 'idk' } onError(message) onFile(false) splitbee.track('Drop Rejected', { type: file.type, size: file.size, message, }) } } const { getRootProps, getInputProps, isDragActive } = useDropzone({ // eslint-disable-next-line @typescript-eslint/no-misused-promises onDrop, accept: Object.keys(ACCEPT_MIME_TYPES), maxSize: ONE_MB, multiple: false, }) const onCopy = () => { setCopied(true) setTimeout(() => { setCopied(false) }, 3000) splitbee.track('Code copied') } const [isHover, setIsHover] = useState(false) return (
{!image && !zipData && ( <>
setIsHover(true)} onMouseLeave={() => setIsHover(false)} >
Generate all the sizes for your
favicon. Tap to upload a file.
Drag &  {isDragActive ? 'Drop' : 'drop'}  an  image file here ... ,
or click to select a file.
{' '} We recommend a square PNG or SVG with at least 310px No data or images are stored
)} {image && !zipData && ( <>
Favicon preview background Favicon preview
{title} {imageData.name} {imageData.extension} ({imageSizes.width}x{imageSizes.height})
{isSquare ? : } Square image
{isPngOrSvg ? : } PNG or SVG
{isSvg || is310px ? : } 310px or higher
Advanced
PWA compatible 512px or higher{' '}
Dark Mode version Coming soon
)} {zipData && ( <>
Favicon preview circle
Favicon preview square
Favicon preview rounded
Your new favicon is ready! We’ve prepared a zip file with all the sizes and a README with the code plus other resources.
{sizesCount} different sizes
Code generated
)} setIsModalOpen(false)} className={styles.content} overlayClassName={styles.overlay} closeTimeoutMS={200} contentLabel="Code generated" >
Code
Insert the following code in the <head> section of your pages:
Insert the following code in the <head> section of your pages:
) } export { DragAndDrop }