Closed16

画像アップロードのUIがほしい

hajimismhajimism
  • 画像がアップロードできる
  • アップロードした画像をプレビューできる
  • アップロードできる画像は1度に1枚だけである
  • アップロードしたファイルを親のstateで管理できる
  • アップロードしなおすことができる

ような画像アップロードのUIがほしい

hajimismhajimism

Hookと

  const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})

Wrapper Componentの2種類のアプローチがある

<Dropzone onDrop={acceptedFiles => console.log(acceptedFiles)}>
  {({getRootProps, getInputProps}) => (
    <section>
      <div {...getRootProps()}>
        <input {...getInputProps()} />
        <p>Drag 'n' drop some files here, or click to select files</p>
      </div>
    </section>
  )}
</Dropzone>
hajimismhajimism

ファイル数はmaxFilesで指定するらしい

  const {
    acceptedFiles,
    fileRejections,
    getRootProps,
    getInputProps
  } = useDropzone({    
    maxFiles:2
  });
hajimismhajimism

file typeはacceptで指定するらしい

useDropzone({
  accept: {
    'image/png': ['.png'],
    'text/html': ['.html', '.htm'],
  }
})
hajimismhajimism

画像ならなんでもおっけー、みたいなのってないのか

hajimismhajimism
  const {
    acceptedFiles,
    fileRejections,
    getRootProps,
    getInputProps
  } = useDropzone({
    accept: {
      'image/jpeg': [],
      'image/png': []
    }
  });

指定したタイプ以外のファイルがなんらかのはずみで入っちゃったときにはエラー対応せねばいけないね。fileRejections.length > 0 && ...みたいな感じで良いかな

hajimismhajimism

おーなんかすげえいい感じの例があった、これをベースにするか

function Previews(props) {
  const [files, setFiles] = useState([]);
  const {getRootProps, getInputProps} = useDropzone({
    accept: {
      'image/*': []
    },
    onDrop: acceptedFiles => {
      setFiles(acceptedFiles.map(file => Object.assign(file, {
        preview: URL.createObjectURL(file)
      })));
    }
  });
  
  const thumbs = files.map(file => (
    <div style={thumb} key={file.name}>
      <div style={thumbInner}>
        <img
          src={file.preview}
          style={img}
          // Revoke data uri after image is loaded
          onLoad={() => { URL.revokeObjectURL(file.preview) }}
        />
      </div>
    </div>
  ));

  useEffect(() => {
    // Make sure to revoke the data uris to avoid memory leaks, will run on unmount
    return () => files.forEach(file => URL.revokeObjectURL(file.preview));
  }, []);

  return (
    <section className="container">
      <div {...getRootProps({className: 'dropzone'})}>
        <input {...getInputProps()} />
        <p>Drag 'n' drop some files here, or click to select files</p>
      </div>
      <aside style={thumbsContainer}>
        {thumbs}
      </aside>
    </section>
  );
}
hajimismhajimism

基本的には動いてるっぽい

type FileWithPreview = File & { preview: string };

type Props = {
  file: FileWithPreview | undefined;
  onFileChange: (_file: FileWithPreview) => void;
};

const withPreview = (file: File) =>
  Object.assign(file, {
    preview: URL.createObjectURL(file),
  });

export const ImageUploader: FC<Props> = ({ file, onFileChange }) => {
  const onDrop = (files: File[]) =>
    files[0] && onFileChange(withPreview(files[0]));

  const { getRootProps, getInputProps } = useDropzone({
    accept: {
      "image/*": [],
    },
    onDrop,
    maxFiles: 1,
  });

  useEffect(() => {
    return () => file && URL.revokeObjectURL(file.preview);
  }, [file]);

  return (
    <section className="container">
      <div {...getRootProps({ className: "dropzone" })}>
        <input {...getInputProps()} />
        <p>Drag & drop some files here, or click to select files</p>
      </div>
      <aside>
        {file && (
          <div key={file.name}>
            <div>
              <Image
                alt=""
                width={800}
                height={450}
                src={file.preview}
                // Revoke data uri after image is loaded
                onLoad={() => {
                  URL.revokeObjectURL(file.preview);
                }}
              />
            </div>
          </div>
        )}
      </aside>
    </section>
  );
};

hajimismhajimism

ここからどうしたいかというと

  • Fileがなにもないときにそれっぽい見た目に
  • 16/9なPreview
  • Previewをclieckしたら再アップロードへ

かな?

hajimismhajimism

うーん、たぶんできた

import "client-only";

import { Image as ImageIcon } from "lucide-react";
import Image from "next/image";
import { FC, useEffect } from "react";
import { useDropzone } from "react-dropzone";

import { cn } from "@/lib/utils";

import { FileWithPreview } from "./type";

type Props = {
  file: FileWithPreview | undefined;
  onFileChange: (_file: FileWithPreview) => void;
  className?: string | undefined;
};

const withPreview = (file: File) =>
  Object.assign(file, {
    preview: URL.createObjectURL(file),
  });

export const ImageUploader: FC<Props> = ({ file, onFileChange, className }) => {
  const onDrop = (files: File[]) =>
    files[0] && onFileChange(withPreview(files[0]));

  const { getRootProps, getInputProps } = useDropzone({
    accept: {
      "image/*": [],
    },
    onDrop,
    maxFiles: 1,
  });

  useEffect(() => {
    return () => file && URL.revokeObjectURL(file.preview);
  }, [file]);

  return (
    <section className="container">
      <div {...getRootProps({ className: "dropzone" })}>
        <input {...getInputProps()} />
        <div
          className={cn(
            "aspect-video w-80 rounded bg-muted-foreground hover:cursor-pointer",
            "flex justify-center items-center",
            className
          )}
        >
          {file ? (
            <div className="relative h-full w-full bg-white">
              <Image
                alt={file.name}
                fill
                className="object-contain"
                src={file.preview}
                onLoad={() => URL.revokeObjectURL(file.preview)}
              />
            </div>
          ) : (
            <ImageIcon />
          )}
        </div>
      </div>
    </section>
  );
};

hajimismhajimism

inputが無限に広がってたのでwidth制御が必要だった

      <div {...getRootProps({ className: "dropzone" })} className="w-80">
hajimismhajimism

というよりclassNameをこっちにつけるほうがいいか

      <div
        {...getRootProps({ className: "dropzone" })}
        className={cn(
          "aspect-video w-80 rounded bg-muted-foreground hover:cursor-pointer",
          "flex justify-center items-center",
          className
        )}
      >
hajimismhajimism

親からwidth制御できる

export default function Page() {
  const [file, setFile] = useState<FileWithPreview>();

  return (
    <ImageUploader file={file} onFileChange={setFile} className="w-[1000px]" />
  );
}
このスクラップは2023/06/30にクローズされました