ThomPoppins / MERN-Enterprise-Search

Open Enterprise Search App (early development fase)
2 stars 0 forks source link

Finish v0.0.3 release notes and issue release #16

Open ThomPoppins opened 11 months ago

ThomPoppins commented 11 months ago

Version v0.0.3 Release Notes

Get a small impression

Note: Below this line a GIF image of 75 MB is loading in, if you wait for a moment for the image to load you will see a small demo of the application live screenrecorded.

Demo

New Features

Basic Search Functionality

Find button is disabled if no search input value is given: Search Field

Search is ready to find pro's!

Search Query Ready To Find

CSS Grid used for structuring the Result components (the clickable company specific rectangle that redirects to their profile/details page)

Search Results Grid

Click Company Result

Current user is member (owner) of this company, private details revealed:

Company Profile/Details Page

Private details hidden, user not member of this company:

Company Private Details Hidden

Image Cropping

/profile Profile page from a new user that has not uploaded a profile image yet.

New User Profile Page

Clicking the 'Upload' button on the placeholder

Click Upload Button

Drop an image file on the square

Drop Image File In Dropzone

frontend\src\components\users\EditProfilePictureModal.jsx: Implementing image file-drop zone with react-dropzone.

import React, { useCallback } from 'react'
import { useDropzone } from 'react-dropzone'

const EditProfilePictureModal = ({ onClose }) => {
...etc.
  const onDrop = useCallback((acceptedFiles) => {
    acceptedFiles.forEach((file) => {
      const reader = new FileReader()

      reader.onabort = () => console.log('file reading was aborted')
      reader.onerror = () => console.log('file reading has failed')
      reader.onload = () => {
        // Do whatever you want with the file contents
        const dataURL = reader.result
        console.log(dataURL)

        setUpImg(dataURL)
      }
      reader.readAsDataURL(file)
    })
  }, [])

...etc.
const { getRootProps, getInputProps } = useDropzone({ onDrop })
...
return (
...etc.
          <div
            {...getRootProps({
              className:
                'dropzone mx-auto w-[300px] h-[300px] mt-16 bg-purple-500 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded top-0 right-0 left-0 bottom-0 z-50 flex justify-center items-center',
            })}
          >
            <input
              {...getInputProps({
                accept: 'image/*',
                onChange: onSelectFile,
              })}
            />
            <p>Drag &apos;n&apos; drop image here, or click to select image</p>
          </div>
...etc.
)

After dropping the image in the dropzone a ReactCrop component is rendered and a preview canvas:

Crop Profile Picture

Setting the crop I wish to download a PNG file of.

Set Crop To Download

After clicking the 'Download Cropped Image' button, a blob file is created from the canvas preview using the canvas API that converts a base64 binary string into a raw Blob data file.

frontend\src\components\users\EditProfilePictureModal.jsx:

/*
 * Function to create a blob from canvas (the crop preview) and download it as png file.
 */
function generateDownload(canvas, crop) {
  if (!crop || !canvas) {
    return
  }

  // toBlob() is not supported by IE11.
  // toBlob() is a method from the canvas API that converts the canvas image to a blob.
  // A blob is a file-like object of immutable, raw data.
  canvas.toBlob(
    (blob) => {
      // The blob is then converted to a URL using URL.createObjectURL().
      const previewUrl = window.URL.createObjectURL(blob)

      const anchor = document.createElement('a')
      anchor.download = 'cropPreview.png'
      // The URL is then used to create a link element with the download attribute.
      anchor.href = URL.createObjectURL(blob)
      // The link element is then clicked to download the file.
      anchor.click()

      // The URL is then revoked to free up memory.
      window.URL.revokeObjectURL(previewUrl)
    },
    'image/png',
    1,
  )
}

I zoomed in too much on purpose so it's very clear the cropped image base64 image preview converted into a Blob raw data object, then that raw data saved to file with PNG format.

Download PNG Cropped Image Result

When you have selected the crop you wish, the user can click the 'Upload' button:

Upload Cropped Image Button Click

**When the user clicks Upload after cropping, Blob binary (raw data) object is generated and written to a image file with PNG extension. Then the File is uploaded to the /backend Express.js server that will serve the static image file.

  const saveProfileImage = (canvas, completedCrop) => {
    if (!completedCrop || !canvas) {
      console.log(completedCrop)
      return
    }

    canvas.toBlob(
      (blob) => {
        // Create a new FormData object
        const formData = new FormData()

        // Make the blob into a file
        const file = new File([blob], 'profile-picture.png')

        // Add the image data to the FormData object
        formData.append('image', file)

        // Send the image to the server with FormData header set so the image can be send
        axios
          .post(`${BACKEND_URL}/upload/image`, formData, {
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          })
          .then((response) => {
            if (response.data.imageId) {
              // Save the image id of the profile picture to the user's document in the database
              axios
                .put(`${BACKEND_URL}/users/profile-picture`, {
                  imageId: response.data.imageId,
                  userId,
                })
                .then(() => {
                  // Get the user's updated document from the database and update the user state
                  axios
                    .get(`${BACKEND_URL}/users/user/${userId}`)
                    .then((response) => {
                      const userData = response.data

                      console.log('user DATA', userData)

                      // Update the user state
                      store.dispatch({ type: 'USER', payload: userData })
                      onClose()
                    })
                    .catch((error) => {
                      console.log(error)
                    })
                })
                .catch((error) => {
                  console.log(error)
                })
            }
          })
          .catch((error) => {
            console.log(error)
          })
      },
      'image/png',
      1,
    )
  }

/backend/routes/uploadRoute.js: Here the file will get received by the POST /upload/image end-point, using Multer for handling the File, naming it and giving it a destination, the /backend/public/uploads/images folder. (The /public folder is the served static files directory of Express.js by calling app.use(Express.static('public')) in /backend/index.js)

import { Image } from '../models/imageModel.js'
import { getURLSuffixFromPath } from '../middleware/files/staticFiles.js'
import express from 'express'
import mongoose from 'mongoose'
import multer from 'multer'
import apiLimiter from '../middleware/rate-limiter/apiLimiter.js'

const router = express.Router(),
  // Multer disk storage configuration.
  storage = multer.diskStorage({
    // `destination` is the folder where the uploaded file will be stored.
    destination(request, file, callback) {
      callback(null, './public/uploads/images')
    },
    fileFilter(request, file, callback) {
      // eslint-disable-next-line
      if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
        // Send status 400 response if the file is not an image and a (error) message to inform the client.
        return callback(new Error('Only images allowed!'))
      }

      // Image file is accepted. Pass `true` to the callback.
      return callback(null, true)
    },
    // Filename is the name of the uploaded file.
    filename(request, file, callback) {
      // Split the file name and extension.
      const [fileName, fileExtension] = file.originalname.split('.'),
        timestamp = Date.now()
      // e file name to multer.
      callback(null, `${fileName}-${timestamp}.${fileExtension}`)
    },
  }),
  // Create multer instance with the storage configuration.
  upload = multer({ storage })

// POST image upload route, will be in the uploadRoute.js file if it works.
router.post(
  '/image',
  apiLimiter,
  upload.single('image'),
  async (request, response) => {
    // If the file upload was successful, the file will be stored in the "uploads/images" folder.
    console.log('REQUEST FILE: ', request.file)

    if (!request.file) {
      console.log('No image file. `request`: ', request)

      return response.status(400).send({
        message: 'No image uploaded.',
      })
    }

    // Prepare response object to send to client with image path and database Image._id.
    const responseObj = {
        message: 'Image uploaded successfully!',
        imagePath: request.file.path,
        url: getURLSuffixFromPath(request.file.path),
        imageId: new mongoose.Types.ObjectId(),
      },
      // Create Instance of Image model with the image path to safe as docyment in the MongoDB Image collection
      image = new Image({
        path: request.file.path,
        url: getURLSuffixFromPath(request.file.path),
      })

    // Save new Image document to database
    await image
      .save()
      .then((result) => {
        console.log('Image saved to database!')

        console.log('Result saving image call: ', result)

        responseObj.imageId = result._id
        responseObj.imageUrl = result.url
      })
      .catch((error) => {
        console.log('Error saving image to database: ', error)

        // TOGOLIVE: [MERNSTACK-260] Remove error message to the frontend before going into production
        return response.status(500).send({
          message: `Error saving image to database! ${error.message}`,
        })
      })

    console.log('Response object: ', responseObj)

    return response.status(200).send(responseObj)
  },
)

export default router

As you can see is the profile picture now updated and used:

Upload Cropped Image Button Click

Company Logo Cropping

Company "logo's" can get cropped the same way as profile pictures with 1 / 1 aspect ratio:

Upload Company Logo Button

Select crop:

Crop Logo Modal

Upload image:

Crop Logo Modal

After saving the edited or registered company, the cropped image served by Express.js is used everywhere where the main company logo should be displayed.

Company Modal

Professions in Company Schema

Storybook Integration

Testing

Animations

Subtle edit of Tailwind preinstalled animations making it a lot more joyful and grabs the user attention better:

/frontend/tailwind.config.js

// 
export default {
  content: [
    './src/components/**/*.jsx',
    './src/pages/**/*.jsx',
    './src/**/*.jsx',
    './src/index.html',
  ],
  theme: {
    extend: {
      keyframes: {
        wave: {
          '0%': { transform: 'rotate(0.0deg)' },
          '10%': { transform: 'rotate(28.0deg)' },
          '20%': { transform: 'rotate(-16.0deg)' },
          '30%': { transform: 'rotate(28.0deg)' },
          '40%': { transform: 'rotate(-8.0deg)' },
          '50%': { transform: 'rotate(20.0deg)' },
          '60%': { transform: 'rotate(0.0deg)' },
          '100%': { transform: 'rotate(0.0deg)' },
        },
        bounce: {
          '0%': {
            transform: 'translateY(-25%)',
            'animation-timing-function': 'cubic-bezier(0.8, 0, 1, 1)',
          },

          '50%': {
            transform: 'translateY(0)',
            'animation-timing-function': 'cubic-bezier(0, 0, 0.2, 1)',
          },
          '100%': {
            transform: 'translateY(-25%)',
            'animation-timing-function': 'cubic-bezier(0.8, 0, 1, 1)',
          },
        },
      },
      animation: {
        'waving-button': 'wave 1s linear infinite',
        'bounce-fast': 'bounce 0.4s infinite',
        'bounce-slow': 'bounce 4s infinite',
        'bounce-reverse': 'bounce-reverse 4s reverse',
        'spin-fast': 'spin 0.4s linear infinite',
        'ping-once': 'ping 1s linear',
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Co-Ownership Invites

Demo

Pending issues

Version v0.0.2 Release Notes

Backend server CDN for static files

File Upload

Version v0.0.1 Release Notes

Registering an Account

Logging In

Company Registration and Ownership

How to Register a Company

  1. Log in to your account.
  2. Navigate to Companies.
  3. Click the plus icon to add a new company.
  4. Fill in company details with the KVK-number and submit the registration form.
  5. Upon successful registration and KVK API validation, the user becomes the owner with access to all administrative functionalities.

How to Add a Co-owner to a Company

  1. Log in to your account.
  2. Navigate to Companies.
  3. Click the pencil icon to edit a company.
  4. Search for a user by name, username, or email.
  5. Click the add button to add the user as an owner to the company.