File upload with React and Firebase

Créer un projet

Créer un projet

Créer un projet

Créer un projet

Créer un projet

Cliquez sur projet web

Créer un projet

Créer un projet

Créer un projet

mettre la config dans un fichier : firebase/config.ts

Installer firebase

npm i firebase --save

Importer firebase

import * as firebase from 'firebase/app';

Importer storage et firestore

import 'firebase/storage';

import 'firebase/firestore';

Inititaliser storage et firestore

const projectStorage = firebase.storage();

const projectFirestore = firebase.firestore();

export { projectStorage, projectFirestore };

Code

import * as firebase from 'firebase/app';
import 'firebase/storage';
import 'firebase/firestore';

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: 'AIzaSyDAmgRgchDRU0du9-WPfy7XFnkgk7r2mVw',
  authDomain: 'image-upload-d2866.firebaseapp.com',
  databaseURL: 'https://image-upload-d2866.firebaseio.com',
  projectId: 'image-upload-d2866',
  storageBucket: 'image-upload-d2866.appspot.com',
  messagingSenderId: '993562178799',
  appId: '1:993562178799:web:fdf294227df4f043eb571a',
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

const projectStorage = firebase.storage();
const projectFirestore = firebase.firestore();

export { projectStorage, projectFirestore };

Créer une base de données

Créer une base de données

Créer une base de données

Créer une base de données

Créer un service storage

Créer un service storage

Créer un service storage

Créer un service storage

Changement des règles

allow read, write: if request.auth != null;

devient  allow read, write

Changement des règles

Création de notre uploader

import React from 'react';

export default function Uploader() {

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    console.log(event.currentTarget.files);
  }

  return (
    <form>
      <input type="file" onChange={handleChange}/>
    </form>
  );
}

Création de notre uploader

Création de notre uploader

Gestion du state de l'uploader

import React, { useState } from 'react';

export default function Uploader() {

  const [file, setFile] = useState<any>(undefined)

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if(event.currentTarget.files === null){
        return;
    }
    const selectedFile =  event.currentTarget.files[0];

    if(selectedFile){
        setFile(selectedFile);
    }
  }

  return (
    <form>
      <input type="file" onChange={handleChange}/>
    </form>
  );
}

Gestion des types

import React, { useState } from 'react';

export default function Uploader() {
  const [file, setFile] = useState<any>(undefined);
  const [error, setError] = useState<string | undefined>(undefined);

  const types = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if (event.currentTarget.files === null) {
      return;
    }
    const selectedFile = event.currentTarget.files[0];

    if (selectedFile && types.includes(selectedFile.type)) {
      setFile(selectedFile);
      setError(undefined);
    } else {
      setFile(undefined);
      setError('Le format du fichier est incorrect');
    }
  };

  return (
    <form>
      <input type="file" onChange={handleChange} />
      {error ? <p>{error}</p> : null}
    </form>
  );
}

Test gestion des types

Création d'un hook useStorage

import { useState, useEffect } from 'react';
import { projectStorage } from '../../../firebase/config';

const useStorage = (file: any) => {
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState<any | undefined>(undefined);
  const [url, setUrl] = useState<any | undefined>(undefined);

  useEffect(() => {
    const storageRef = projectStorage.ref(file.name);
    storageRef.put(file).on(
      'state_changed',
      (snap) => {
        let percentage = Math.round((snap.bytesTransferred / snap.totalBytes) * 100);
        setProgress(percentage);
      },
      (err) => {
        setError(err);
      },
      async () => {
        const url = await storageRef.getDownloadURL();
        setUrl(url);
      }
    );
  }, [file]);

  return { progress, url, error };
};

export default useStorage;

Création d'une progress bar

import React, { useEffect } from 'react';
import useStorage from './hooks/useStorage';

export default function ProgressBar({
  file,
  onFileUploadCompleted,
}: {
  file: any;
  onFileUploadCompleted: Function;
}) {
  const { url, progress } = useStorage(file);

  useEffect(() => {
      if(url) {
        onFileUploadCompleted(url);
      }
  }, [url, onFileUploadCompleted]);
  return (
    <div>
      <p>{progress} %</p>
      <div style={{ width: `${progress}%` }}></div>
    </div>
  );
}

Style de la progress bar

.progressBar {
    height: 5px;
    background: #2ecc71;
}

style.module.scss

import styles from './styles.module.scss';
 return (
   <div>
     <p>{progress} %</p>
     <div className={styles.progressBar} style={{ width: `${progress}%` }}></div>
   </div>
 );

progressBar.tsx

Test de notre composant

Le storage Firebase

Faire le lien entre le storage et la base de données

const getTimestamp = firebase.firestore.FieldValue.serverTimestamp;

export { projectStorage, projectFirestore, getTimestamp };

Nous avons besoin de cela dans notre fichier de configuration afin d'enregistrer la date de création de l'image dans la base de données

Faire le lien entre le storage et la base de données

import { useState, useEffect } from 'react';
import { projectStorage, projectFirestore, getTimestamp } from '../../../firebase/config';

const useStorage = (file: any) => {
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState<any | undefined>(undefined);
  const [url, setUrl] = useState<any | undefined>(undefined);

  useEffect(() => {
    const storageRef = projectStorage.ref(file.name);
    const collectionRef = projectFirestore.collection('image');

    storageRef.put(file).on(
      'state_changed',
      (snap) => {
        let percentage = Math.round((snap.bytesTransferred / snap.totalBytes) * 100);
        setProgress(percentage);
      },
      (err) => {
        setError(err);
      },
      async () => {
        const url = await storageRef.getDownloadURL();
        collectionRef.add({ url, createdAt: getTimestamp() })
        setUrl(url);
      }
    );
  }, [file]);

  return { progress, url, error };
};

export default useStorage;

Code uploader

import React, { useState } from 'react';
import ProgressBar from './ProgressBar';

export default function Uploader() {
  const [file, setFile] = useState<any>(undefined);
  const [error, setError] = useState<string | undefined>(undefined);

  const types = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'];

  const onFileUploadCompleted = (url: string) => {
    setFile(undefined);
    setError(undefined);
  }
 
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if (event.currentTarget.files === null) {
      return;
    }
    const selectedFile = event.currentTarget.files[0];

    if (selectedFile && types.includes(selectedFile.type)) {
      setFile(selectedFile);
      setError(undefined);
    } else {
      setFile(undefined);
      setError('Le format du fichier est incorrect');
    }
  };

  return (
    <form>
      <input type="file" onChange={handleChange} />
      {error ? <p>{error}</p> : null}
      {file && <ProgressBar file={file} onFileUploadCompleted={onFileUploadCompleted}/>}
    </form>
  );
}

Image grid component

import React from 'react';
import styles from './styles.module.scss';

export default function ImageGrid() {

    return (
        <div className={styles.imgGrid}>
            images
        </div>
    )
}
.imgGrid {}

Créer useFirestore hook

import { useState, useEffect } from 'react';
import { projectFirestore } from '../../../firebase/config';

export default function useFirestore(collection: any) {
  const [docs, setDocs] = useState<Array<any>>([]);

  useEffect(() => {
    const unsuscribe = projectFirestore
      .collection(collection)
      .orderBy('createdAt', 'desc')
      .onSnapshot((snap) => {
        let documents: Array<any> = [];
        snap.forEach((doc) => documents.push({ ...doc.data(), id: doc.id }));
        setDocs(documents);
      });

    return () => unsuscribe();
  }, [collection]);

  return { docs };
}

Utilisation du hook dans Image grid

import React from 'react';
import styles from './styles.module.scss';
import useFirestore from '../../components/Uploader/hooks/useFirestore';

export default function ImageGrid({ onSelectedImage}: {onSelectedImage: Function}) {
  const { docs } = useFirestore('image');

  return (
    <div className={styles.imgGrid}>
      {docs &&
        docs.map((doc) => {
          return (
            <div key={doc.id} className={styles.imgWrapper}>
              <img src={doc.url} onClick={() => onSelectedImage(doc.url)} />
            </div>
          );
        })}
    </div>
  );
}

Style du composant image grid

.imgGrid {
  margin: 20px auto;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-gap: 25px;
}

.imgWrapper {
  overflow: hidden;
  height: 0;
  padding: 50% 0;
  position: relative;

  img {
    min-width: 100%;
    min-height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    max-width: 150%;
  }
}

Resultat

Création d'une modal

import React from 'react';
import styles from './styles.module.scss';

export default function Modal({
  image,
  onBackdropClick,
}: {
  image: string | undefined;
  onBackdropClick: any;
}) {
  return (
    <div className={styles.backdrop} onClick={onBackdropClick}>
      <img src={image} />
    </div>
  );
}

Style de la modal

.backdrop {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(30, 30, 30, 0.5);
  z-index: 9999;

  img {
    margin: 60px auto;
    display: block;
    max-width: 70%;
    max-height: 70%;
  }
}

Interaction entre nos composants

import React, { useState } from 'react';
import styles from './styles.module.scss';
import Topbar from '../../components/Topbar';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import { Link } from 'react-router-dom';
import Uploader from '../../components/Uploader';
import ImageGrid from '../../components/ImageGrid';
import Modal from '../../components/Modal';

export default function Dashboard() {
  const [selectedImage, setSelectedImage] = useState<string | undefined>(
    undefined
  );

  const handleSelectedImage = (image: string) => {
    setSelectedImage(image);
  };

  const handleOnCloseBackdrop = () => {
    setSelectedImage(undefined);
  }

  return (
    <div>
      <Topbar>
        <Link to="/" className={styles.backButton}>
          <FontAwesomeIcon icon={faChevronLeft} />
        </Link>
      </Topbar>
      <div className="mt-4 p-4">
        <Uploader />
      </div>
      <div className="mt-4 p-4">
        <ImageGrid onSelectedImage={handleSelectedImage} />
      </div>
      {selectedImage ? <Modal image={selectedImage} onBackdropClick={handleOnCloseBackdrop} /> : null}
    </div>
  );
}

Résultat

File upload with React and Firebase

By NicoHash

File upload with React and Firebase

  • 240