ThomPoppins / MERN-Enterprise-Search

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

MERN Enterprise Search ⚑

Quick Overview

Check Out a Sneak Peek

Look below this text for a GIF (a kind of moving image) that's loading. After a short wait, you'll see a quick demo of the application in action. (there will be a new version soon, this one cuts of at a certain point, I'm sorry about that)

Demo

πŸš€ Hey there! 🌟

Guess what? I'm currently working on a super cool FullStack JavaScript application, and I'm on the lookout for fellow tech enthusiasts who share my passion for learning and building awesome stuff!

This project started as a fun educational journey for me, but now I'm throwing the doors wide open. Whether you're a self-taught wizard or just getting started, I'm all ears and totally cool if you don't have a treasure trove of certificates. πŸ§™β€β™‚οΈβœ¨

Picture this: we're delving deep into the MERN stack, leveraging React (yup, we've got Redux for the ultimate state management party) on the client side. Our ExpressJS backend is the backstage maestro, serving up API endpoints on Node.js and ensuring our data seamlessly moves between the client and MongoDB database. And hey, we've got the NPM Mongoose package – our secret sauce for authenticated data transfer magic! 🎩🐍

But here's the exciting part! This application isn't just any project; it's a vision. We're dreaming big – envisioning a globally recognized platform that not only elevates lives with killer functionalities but also delivers an experience that's as social and enjoyable as your favorite Friday night plans. πŸŒπŸš€

Let me spill the beans on a game-changer feature: our app is a search engine for registered companies. Companies can register for free! The search results are revolutionary; we sort companies by their ratings and relevance. It's way better than sifting through Google to find services or productsβ€”our approach ensures you always discover the best-rated, most relevant companies.

Sure, the road ahead might be a bit of a rollercoaster, but guess what? I'm psyched about every twist and turn. And I want you to be a part of it!

If you're itching to dive headfirst into this tech wonderland, explore, and make your mark on a project tailor-made for budding developers, I'm sending you a VIP invite. Drop a pull request, toss in your thoughts in the designated tab – heck, even just shoot me a message with your wildest ideas. Your genius is not only welcomed but cherished! πŸ’‘

This project is more than just a learning pathway; it's an adventure where we collectively sculpt the path, giving you the freedom to bring your ideas to life with all the support and none of the stress. Oh, and did I mention? No deadlines! We're in this for skill development, not racing against the clock.

So, if you're a tech wizard with big dreams, looking to ride this rollercoaster of growth and collaboration, consider this your golden ticket.

Jump on board now and become a star player in our tech-tastic team! πŸš€βœ¨

Shoot me an email at thompoppins@gmail.com.

Cheers to the adventure ahead! πŸŒŸπŸš€

:zap: Table of Contents

🏠 Homepage Exploration:

When you're logged in, the homepage transforms into a powerful search engine for finding professionals with expertise. It's like a search superpower for users seeking skilled individuals.

Search Engine in Action

πŸ§‘β€πŸ’Ό Profile Page Magic:

Right after you sign up, your profile is pretty empty, and your profile picture is a stand-in - a male image for guys and a female image for gals.

Profile Placeholder

🌐 Uploading Your Profile Pic:

Once you log in for the first time, just hit the upload button on the placeholder profile picture. A window will pop up, letting you pick an image from your device. Click on browse..., and voila! Your picture is ready to roll.

Upload Modal

πŸ–ΌοΈ Preview Before You Commit:

After you choose an image from your device, you get a sneak peek. You can change your mind or cancel the upload if the image isn't quite what you had in mind.

Picture Preview

And hey, if you want, you can still swap it out or hit the cancel button before the image is uploaded. Oh, and that preview? It's like a secret code, but for images.

🎨 Decoding BLOB: What's the Binary Large Object?

Here's the lowdown: a BLOB is a fancy term for a collection of binary data stored as one unit. Usually, it's images, audio, or other cool stuff. Sometimes, even secret binary codes chill out in a BLOB.

<img src={blobValueString} />

Source: /client/src/components/users/EditProfilePicture.jsx

In case you're curious about the code behind the scenes, there's a script called EditProfilePictureModal that handles all the picture-editing magic. It's like the conductor of the image orchestra.

Explore New Features

1. Search Functionality

Note: The search button is only clickable when there's a search input value. Check out the image below for reference:

Search Field

Ready to Find Pros!

Search Query Ready To Find

CSS Grid for Structure

Search Results Grid

Click Company Result

Member Access:

Company Profile/Details Page

Non-Member Access:

Company Private Details Hidden

Image Cropping Made Easy

Example Scenario:

New User Profile Page

  • A new user's profile page without a picture.

Click Upload Button

  • Clicking the 'Upload' button to add a picture.

Drop Image File In Dropzone

  • Dragging and dropping an image into the provided area.

Crop Profile Picture

  • The cropped image is previewed, and the user can adjust the crop.

Set Crop To Download

  • Choosing the desired crop and generating a downloadable PNG file.

Creating and Downloading a Cropped Image:

  1. After clicking 'Download Cropped Image':
    • A special file (Blob) is created from the previewed canvas using the canvas API.
    • This Blob file is a raw data representation of the cropped image.
// JavaScript function to create a blob from the canvas and download it as a PNG file
function generateDownload(canvas, crop) {
  if (!crop || !canvas) {
    return
  }

  canvas.toBlob(
    (blob) => {
      const previewUrl = window.URL.createObjectURL(blob)
      const anchor = document.createElement('a')

      anchor.download = 'cropPreview.png'
      anchor.href = URL.createObjectURL(blob)
      anchor.click()

      window.URL.revokeObjectURL(previewUrl)
    },
    'image/png',
    1,
  )
}
  1. Result after Download:
    • A PNG file is downloaded with the cropped image.

Download PNG Cropped Image Result

  1. Uploading the Cropped Image:
    • When the user clicks 'Upload' after cropping, a Blob binary object is generated.
    • This Blob object is then written to an image file in PNG format.
    • The image file is uploaded to the Express.js server.
// JavaScript function to save the cropped image to the server
const saveProfileImage = (canvas, completedCrop) => {
  if (!completedCrop || !canvas) {
    return
  }

  canvas.toBlob(
    (blob) => {
      const formData = new FormData()
      const file = new File([blob], 'profile-picture.png')
      formData.append('image', file)

      axios
        .post(`${BACKEND_URL}/upload/image`, formData, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        })
        .then((response) => {
          // Update user profile picture information in the database
          // and refresh the user's data on the frontend.
        })
        .catch((error) => {
          // Handle any errors that may occur during the upload process.
        })
    },
    'image/png',
    1,
  )
}
  1. Backend Processing:
    • On the server side (Express.js), the uploaded image is handled using Multer.
    • The file is named, given a destination, and stored in the "/public/uploads/images" folder.
    • The server responds with details about the uploaded image.
// Express.js route for handling image uploads
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.
    // A response is sent back to the client with image path, URL, and database Image._id.
  },
)
  1. Result after Uploading:
    • The user's profile picture is updated and utilized.

Upload Cropped Image Button Click

Simplified Explanation:

Improvements in the System:

Professions in Company Schema

Storybook Integration

Testing

Animations

Tailwind CSS Animation Tweaks:

Co-Ownership Invites

Demo

User profile page and data structure

Profile page: Profile Page With Profile Picture

At this point there are only a few details a user can set when registering a new account. Of course this will be expend (largely) in the future. For now in this stage of the development process of the application, it's useful to keep minimalistic, clean and keep everything simple now there is not any dependency on yet and over complicate everything. Dependencies for users details could be a detailed profile pages, location/address information, media, posts on a timeline (or feed) or many other things users would want to save personally to their account eventually.

User schema

Schema fields:

Additional fields:

Mongoose:

Source: /backend/models/userModel.js

// Instantiate User schema
const userSchema = new mongoose.Schema(
  {
    username: {
      type: String,
      required: true,
      unique: true,
      default: '',
    },
    email: {
      type: String,
      required: true,
      unique: true,
      default: '',
    },
    hashedPassword: {
      type: String,
      required: true,
      default: '',
    },
    firstName: {
      type: String,
      required: true,
      default: '',
    },
    lastName: {
      type: String,
      required: true,
      default: '',
    },
    gender: {
      type: String,
      required: true,
    },
    profilePicture: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Image',
    },
  },
  { timestamps: true },
)

Mongoose User model:

/backend/models/userModel.js:

// Instantiate User model
const User = mongoose.model('User', userSchema)

Companies

Listing page

On the /companies page the user can see all companies that he owns and has the choice between listing the companies in card view or in table view. The view of choice will be saved as a Redux state so the user preference will be kept as long as they are logged in. I am planning to save this configuration to the database so the user preference will never be lost and can be dispatched to the Redux state every time they log in to their account.

Note: I opened the dropdown menu.

Card view: Companies Listing Page Card View

Table view: Companies Listing Page Table View

When the user clicks on the eye icon on a listed company, a modal will pop up that will display the main and most important public company information so the owner of the company can check the company current state quickly at a glance without having to navigate to another company specific details page and lose track of what they were doing or planning to do from the companies listing page.

Note: At this stage in development, companies do not have that many details yet to show. There will be a lot of work to these pages yet and they do not reflect a final version.

Show Company Details Modal

Registration

An owner of a company can register his company in my application. On this companies listing page you see a green + icon in the top right corner. When a user clicks on that, he will navigate to the company register page where the user can register a new company that hasn't registered yet by filling in a company registration form.

Company registration form: Company Registration Form Top Company Registration Form Bottom

Form field validation

All form input fields in my application have to be validated. I've written my own validators for all fields. I've used regular expressions to make sure it is correct data as I expect to receive from the user input.

Example validator:

Source: /client/utils/validation/emailValidator.js

const emailValidator = (email) => {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u
  return regex.test(email)
}

export default emailValidator

Invalid value notifications:

Invalid Values Error Notifications

Code example communicating invalid values in the UI of company registration page:

Source: /client/src/pages/companies/RegisterCompany.jsx

import React, { useEffect, useState } from 'react'
import axios from 'axios'
import { useSnackbar } from 'notistack'
import emailValidator from '../../utils/validation/emailValidator'
// ... (and a lot of other imports and validator imports here)

const RegisterCompany = () => {
 const [name, setName] = useState(''),
 // email form field input value
   [email, setEmail] = useState(''),
   // ... (states for all other form field values)
   // If value is invalid, emailError would become true
   [nameError, setNameError] = useState(false),
   [emailError, setEmailError] = useState(false),
   // ... (errors states for all form fields here)
   // useSnackbar is a hook that allows us to show a notification that pops up in the left bottom corder (see image above)
   { enqueueSnackbar } = useSnackbar()

 // Functions that will call the name and email validators and sets the error state dependent on the return value from the validators.
 // This function is called directly by the onBlur event listener on the name and email input fields, so it is called when the input
 // field loses focus.
 const validateCompanyName = () => {
     if (companyNameValidator(name)) {
       setNameError(false)
     } else {
       setNameError(true)
     }
   },
   validateEmail = () => {
     if (emailValidator(email)) {
       setEmailError(false)
     } else {
       setEmailError(true)
     }
   },
   // ... (a lot of other validateFormField() functions here)

 // Handle onChange events for all input fields
 const handleNameChange = (event) => {
     setName(event.target.value)
     if (nameError) {
       validateCompanyName()
     }
   },
   handleEmailChange = (event) => {

     setEmail(event.target.value)

     if (emailError) {
       validateEmail()
     }
   },
 // ... (a lot of input field change handlers here)

 // Handle onChange events for all input fields
 const handleNameChange = (event) => {
     // Set the name state to the current name input field value
     setName(event.target.value)
     if (nameError) {
       // Only IF the name error state is ALREADY true, then validate name always onChange. This prevents a notification when the user
       // hasn't completed his input and would otherwise already show after typing the first character in to the field. onBlur()
       //calls the validateName function initially after losing focus the first time.
       validateCompanyName()
     }
   },
   handleEmailChange = (event) => {
     // Set the email state to the current email input field value
     setEmail(event.target.value)
     if (emailError) {
       // Only IF the email error state is ALREADY true, then validate email always onChange. Initially called by onBlur like the name field.
       validateEmail()
     }
   },
   // ... (here all other onChange handler for the other input fields)

 // Display error messages if the user enters invalid input with useSnackbar
 useEffect(() => {
   if (nameError) {
     // Trigger snackbar notification
     enqueueSnackbar('Company name is invalid!', {
       variant: 'error', // Display notification in a red box
       preventDuplicate: true, // Prevents notification spamming
     })
   }
   // Trigger snackbar notification
   if (emailError) {
     enqueueSnackbar('Email is invalid!', {
       variant: 'error', // Display notification in a red box
       preventDuplicate: true, // Prevents notification spamming
     })
   }
   // ... (rest of the input field if statement whether to display a invalid value error notification)
 }, [
   // This dependency array is set to the error states of the input fields. Every time a state value from this array changes,
   // this useEffect hook function will trigger.
   nameError,
   emailError,
   phoneError,
   kvkNumberError,
   sloganError,
   descriptionError,
   startYearError,
 ])

 // Function that is being called when the user presses the Save button.
 const handleSaveCompany = async () => {
   // Validate all fields before sending the request to the backend, otherwise return
   validateCompanyName()
   validateEmail()
   // ... (validate other fields here)

   // If there are any invalid form fields left, notify the active user and return without saving and without redirect.
   if (
     nameError ||
     emailError ||
     phoneError ||
     kvkNumberError ||
     sloganError ||
     startYearError ||
     !name ||
     !email ||
     !phone ||
     !kvkNumber ||
     !slogan ||
     !startYear
   ) {
     enqueueSnackbar(
       'Please fill in all fields correctly before saving this company!',
       {
         variant: 'error',
         preventDuplicate: true,
       },
     )
     return
   }

   // If all values are correct, prepare object for company save request
   const data = {
     name,
     logo,
     email,
     phone,
     kvkNumber,
     slogan,
     startYear,
     description,
     owners: [{ userId }],
   }
   // Render loading animation for as long as the request takes
   setLoading(true)
   axios
     .post(`${BACKEND_URL}/companies`, data)
     .then(() => {
       // Saving company success
       // Stop loading animation
       setLoading(false)
       // Notify the user about success
       enqueueSnackbar('Company registered successfully!', {
         variant: 'success',
         preventDuplicate: true,
       })
       // Redirect back to companies listing page
       navigate('/companies')
     })
     .catch((error) => {
       // If request failed notify active user accordingly to the problem that occurred.
       // Company with the KvK number already existed, is not unique
       if (error.response.status === 409) {
         enqueueSnackbar('Company with this KVK number already exists!', {
           variant: 'error',
           preventDuplicate: true,
         })
         // Set KvK error to true
         setKvkNumberError(true)
         // Display a more fitting message below the input field.
         setKvkNumberErrorMessage(
           'Company with this KVK number already exists!',
         )
       }
       // Disable animation
       setLoading(false)
       // Always notify user saving company failed
       enqueueSnackbar('Error registering company!', {
         variant: 'error',
         preventDuplicate: true,
       })
     })
 }

 return (
   // ... (Top of the register page)

     <div className='my-4'>
       <label className='text-xl mr-4' htmlFor='company-name-input'>
         Name
       </label>
       <input
         className={`border-2 border-purple-900 bg-cyan-100 focus:bg-white rounded-xl text-gray-800 px-4 py-2 w-full ${
           nameError ? 'border-red-500' : ''
         }`}
         data-test-id='company-name-input'
         id='company-name-input'
         onBlur={validateCompanyName} // onBlur event validate name field function call
         onChange={handleNameChange} // onChange event name field change handler function call
         type='text'
         value={name}
       />
       { /* Conditionally render the error notification text below the input field: */}
       {nameError ? (
         <p className='text-red-500 text-sm'>
           Company name must be between 1 and 60 characters long and can
           only contain letters, numbers, spaces, and the following
           characters: &#45;, &apos;, and &#46;
         </p>
       ) : (
         ''
       )}
     </div>
     <div className='my-4'>
       <label className='text-xl mr-4' htmlFor='company-email-input'>
         Email
       </label>
       <input
         className={`border-2 border-purple-900 bg-cyan-100 focus:bg-white rounded-xl text-gray-800 px-4 py-2 w-full ${
           emailError ? 'border-red-500' : ''
         }`}
         data-test-id='company-email-input'
         id='company-email-input'
         onBlur={validateEmail} // onBlur event validate email field function call
         onChange={handleEmailChange} // onChange event email field change handler function call
         type='text'
         value={email}
       />
       { /* Conditionally render the error notification text below the input field: */}
       {emailError ? (
         <p className='text-red-500 text-sm'>
           Email must be a valid email address.
         </p>
       ) : (
         ''
       )}
     </div>
 )
KVK number validation

Invalid KvK Number

Companies in the Netherlands (my home country) are always registered to the "Kamer van Koophandel" which is the Chamber of Commerce in the Netherlands. It is a government agency that plays a crucial role in the registration and documentation of businesses operating in my country.

I've connected the backend application to the KvK test API for validation of company KvK numbers. When a user registers a company to my application and fills in the KvK number, when the input field loses focus (onBlur()), automatically there will be a request to the KvK (test) API for KvK number validation.

GET route to get KvK data:

Source: /backend/routes/kvkRoute.js

import { getKvkData } from '../controllers/kvkController.js'
import express from 'express'
import cors from 'cors'

const router = express.Router()

// GET route to get KvK data from the KvK API by KvK number
router.get('/', cors(), getKvkData)

export default router

KvK controller for handling request:

Source: /backend/controllers/kvkController.js

import axios from 'axios'
import fs from 'fs'
import https from 'https'
import { KVK_TEST_API_KEY } from '../config.js'

const PATH_TO_KVK_API_CERTIFICATE_CHAIN_RELATIVE_TO_INDEX_APP =
  './certs/kvkApi/Private_G1_chain.pem'

// Function to get data from the KVK API
export const getKvkData = async (request, response) => {
  try {
    // Get the query from the request query parameters
    const { kvkNumber } = request.query,
      // Get the certificate chain from the file system
      certificateChain = fs.readFileSync(
        PATH_TO_KVK_API_CERTIFICATE_CHAIN_RELATIVE_TO_INDEX_APP,
        'utf8',
      ),
      // Create an https agent with the certificate chain
      // https://nodejs.org/api/https.html#https_https_request_options_callback
      agent = new https.Agent({
        ca: certificateChain,
      }),
      // Get the data from the KVK API GET request
      { data } = await axios.get(
        `https://api.kvk.nl/test/api/v1/naamgevingen/kvknummer/${kvkNumber}`,
        {
          headers: {
            apikey: KVK_TEST_API_KEY,
          },
          httpsAgent: agent,
        },
      )

    // Send status 200 response and the data to the client
    return response.status(200).json(data)
  } catch (error) {
    console.log('Error in GET /kvk: ', error)
    // If the error is a 400 error, send a 400 response with the error message
    if (error.response.status === 400) {
      return response.status(400).send({ message: error.message })
    }
    // Else, send a 500 response with the error message
    return response.status(500).send({ message: error.message })
  }
}

For now, only number validation is enough, but in the future also the company name, owners and other company details will be verified against this API to rule out the need for human verification as much as possible to safe costs and make the user experience a much faster because users can get started with their company in the application right away without having to wait for a manual verification of their business.

Subsidiary companies:: KvK numbers have to be unique so companies can't get registered more then once, in the future this uniqueness has to be combination between Kvk number and company name (and also maybe other company details) because companies can have subsidiary companies with the same number and these subsidiary companies should be able to be registered as valid companies to the application because for a regular user using the app to find a company they need, it is not important to know that a company has a parent company. If companies find it necessary to inform the regular user (and potential customer) about their subsidiarity of a parent company, then they should be able to inform users about that on their company profile page (in very early development).

Company document data structure

When I first got the business idea for building this application I decided to make companies the main central starting point to focus on, find out what is necessary to get companies on board with my application and want to register and pay for premium features. Almost the first thing I started building was a company model that has all required fields where companies would be dependent on realizing the ideas I have in mind for my application, resulting in a Company model with many fields. At this stage of development only a few of there defined fields are actually used and populated with data at the moment, but because it is not a requirement to populate every field with data before saving and editing Company documents in the database, I feel no need to simplify the model for the time being at all.

Company schema

Schema fields:

  1. Name:

    • Type: String
    • Required: true
    • Description: The name of the company.
  2. Logo:

    • Type: String (Base64 format)
    • Required: false
    • Default: ""
    • Description: The company's logo (still) in Base64 format.
  3. Email:

    • Type: String
    • Required: true
    • Default: ""
    • Description: The company's email address for correspondence.
  4. Phone:

    • Type: String
    • Required: true
    • Default: ""
    • Description: The company's contact phone number.
  5. KVK Number:

    • Type: String
    • Required: true
    • Unique: true
    • Default: ""
    • Description: Kamer van Koophandel (KVK) number of the company.
  6. KVK Validated:

    • Type: Boolean
    • Required: true
    • Default: false
    • Description: Indicates whether the KVK number is validated using the already fully functional and authenticated KVK test API end point connection.
  7. Slogan:

    • Type: String
    • Required: true
    • Default: ""
    • Description: The company's slogan.
  8. Description:

    • Type: String
    • Required: true
    • Default: ""
    • Description: A short description of the company.
  9. Address:

    • Type: Object
    • Required: false
    • Default: {}
    • Description: The registered address of the company.
  10. Billing Address:

    • Type: Object
    • Required: false
    • Default: {}
    • Description: The address to send invoices to.
  11. Address Format:

    • Type: ObjectId (Reference to Address Format model)
    • Required: false
    • Default: null
    • Description: The country specific address format of the country the registered company is in.
  12. Country:

    • Type: String
    • Required: false
    • Default: "NL"
    • Description: The country of the company's billing address.
  13. Region:

    • Type: String
    • Required: false
    • Default: ""
    • Description: The region of the company's billing address.
  14. Owners:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of objects containing owner their User ObjectId's corresponding with their documents' ID in the of the users collection. Owners will always have the right to admin level access to company configuration and can disable admin level access to these configurations any time for safety, they can also enable these admin rights whenever is necessary and will be prompted regularly to disable the elevated admin access to prevent any unintended possible disasters (like deleting the company by accident and losing all reviews, score and status).
  15. Company Admins:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of ObjectId's containing company admins User ID's who have elevated access to Company configuration. Admins have elevated access to company configurations and can disable admin level accessibility to these configurations any time for safety, they can also enable these admin rights whenever is necessary and will be prompted regularly to disable the elevated admin access to prevent any unintended possible disasters just like owners. Admins have the right to add other admins to a company when they have elevated access enabled, but initially a company owner with elevated access had to add the first admin (who is not company owner).
  16. Locations:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of objects representing company locations. This will be ObjectIds corresponding to Address documents in the address collection.
  17. Departments:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of objects representing company departments. To be decided the format this will be in.
  18. Business Config:

    • Type: Object
    • Required: false
    • Default: {}
    • Description: Configurable settings for company owners and admins with elevated access enabled.
  19. Payment Details:

    • Type: Object
    • Required: false
    • Default: {}
    • Description: Payment details for the company. Think about anything solely necessary for financial transactions in any direction.
  20. Start Year:

    • Type: Number
    • Required: false
    • Default: 0
    • Description: The year the company was started.
  21. Active:

    • Type: Boolean
    • Required: false
    • Default: true
    • Description: Indicates if the company is currently active. (Open for business)
  22. Industries:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of industries associated with the company for grouping companies and search result improvement.
  23. Public:

    • Type: Boolean
    • Required: false
    • Default: true
    • Description: Indicates if the company is public or private.
  24. Reviews:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of ObjectIds of Review documents in the review collection in the database representing this companies' reviews.
  25. Rating:

    • Type: Number
    • Required: false
    • Min: 0
    • Max: 5
    • Default: 0
    • Description: The overall rating of the company. Every User can vote on this only a single time but might be able to edit their rating of the company. In what format ratings should be tracked and saved is to be decided.
  26. Customers:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of customers User ObjectIds in from the users collection in database.
  27. Premium:

    • Type: ObjectId (Reference to Premium Type model)
    • Required: false
    • Default: null
    • Description: The premium type associated with the company. Like "none" "bronze", "silver", "gold" or "platinum". What every premium subscription level has to cost and what advantages or features these provide for subscribed companies is to be decided, think about company profile cosmetic changes or being able to have actions, discounts or events, BUT companies will never be able to pay for a higher place in the search result because that defeats the purpose of this application completely.
  28. Vendor:

    • Type: ObjectId (Reference to Vendor model)
    • Required: false
    • Default: null
    • Description: Can this company sell to other companies? If so, this company will be marked as vendor and probably have a corresponding Vendor document in the (yet un-existing) vendors collection where all to vendors specific data will be saved.
  29. Employees:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of User ObjectId's of users who accepted the Invite to become employee of this company and will be able to have some functionalities within this company like writing Story posts under their own name and communicate with (potential) customers (users of this application).
  30. Stories:

    • Type: Array
    • Required: false
    • Default: []
    • Description: ObjectId's of Story documents in the stories collection. Stories are posts placed on a timeline where you can see what the company has been active in lately and in the past. Stories can differ a lot from one another, companies have to be able to have a large spectrum of possibilities adding stories that fit their wishes.
  31. Products:

    • Type: Array
    • Required: false
    • Default: []
    • Description: Products a company can offer and users can buy. Probably will be an array of ObjectId's, but have to decide how to structure product data. Maybe product selling functionality would require a compete new platform to be with a realtime connection synchronizing with this application.
  32. Services:

    • Type: Array
    • Required: false
    • Default: []
    • Description: A company can offer and sell services to users. The exact format this will be build in is to be decided.
  33. Agenda:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of agenda objects associated with the company. Format is to be decided.
  34. Appointments:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of appointments with users and other companies, format is to be decided.
  35. Messages:

    • Type: Array
    • Required: false
    • Default: []
    • Description: Corresponds with messages in the messages collection ObjectId's of Message documents. Still need to decide on the messages' format and data structure.
  36. Notifications:

    • Type: Array
    • Required: false
    • Default: []
    • Description: An array of corresponding Notification documents'
  37. Events:

    • Type: Array
    • Required: false
    • Default: []
    • Description: ObjectId's corresponding to Event documents in the events collection. Events could be anything that is organized and it is still to decide in which many ways and configurations events could be created by users of the application.
  38. Tasks:

    • Type: Array
    • Required: false
    • Default: []
    • Description: Array of ObjectId's of Task documents in the tasks collection. Could be anything a user or company could have to do and I will decide later on all the functionalities and data structure of tasks later on.
  39. Invoices:

    • Type: Array
    • Required: false
    • Default: []
    • Description: Array of Invoice document ObjectId's in the invoices collection. Invoice data structure has to be decided on yet.
  40. Orders:

    • Type: Array
    • Required: false
    • Default: []
    • Description: Array of Order document ObjectId's in the orders collection which will contain all kind of orders users and companies could make and contains information of all order specific data like order status and much more.
  41. Payments:

    • Type: Array
    • Required: false
    • Unique: true
    • Default: []
    • Description: Array of Payment document ObjectId's in the payments collection which keeps track of all financial transactions between everybody.
  42. Main Image ID:

    • Type: String
    • Required: false
    • Default: ""
    • Description: The main image should be the first thing people see when searching for a company and should be the eye catcher of the company to attract people to look into them. This is meant to be a different image then the company logo, the logo is also displayed in the first glance of a user searching for a company but smaller in a corner (something like that).
  43. Images:

    • Type: Array
    • Required: false
    • Description: An array of image objects associated with the company.
  44. Timestamps:

    • Type: Object
    • Description: Automatically adds createdAt and updatedAt fields to the user doc

Mongoose:

Schema:

// Instantiate `Company` schema
const companySchema = new mongoose.Schema(
  {
    // ... (all schema fields are defined here)
  },
  { timestamps: true },
)

Note: To see the complete code of the Company schema instantiation with all fields here.

Model:

// Instantiate `Company` model
const Company = mongoose.model('Company', companySchema)

Edit company

When a company owner clicks on the pencil icon on the companies listing page the owner is able to edit the company.

Edit Company Page Edit Company Page

Company ownership

Companies are automatically owned by the User that registers the company to the application.

If a company has more than one owner, the company owners is able to invite other users for company ownership, giving the other co-owners the same admin level elevated access to the configuration of their company.

Find other users and invite them for co-ownership: Find Other Users For Company Co-ownership

A company owner can find users of the application with the search box on the "edit company" page and send them a invite by clicking the invite button.

When a user is invited by the owner for co-ownership the user "result" will be removed from the search results list and a "Pending invites" section will appear with the invited user. I invited the user Kaya Lowe in this example.

User Invited On Edit Company Page

Note: In the future this Invite information will be the user details, but I have to make a future decision about where I want this data to be served from the backend to the client application, that's why it is only containing ObjectId information of the Invite document. See the Invite schema data structure further down below.

When the User is invited to become co-owner of the company, that user will receive a invite notification in the navigation bar.

User Invited On Edit Company Page

Clicking on the Invites dropdown menu item, the user will navigate to the invites page and be able to Accept or Decline the invite by clicking the buttons in the Operations section in the Invites table listing the pending invites.

Invites Page

After clicking Accept or Decline and there is no pending invite left, the user will navigate to the companies listing page and the companies they accepted will be listed there with their name added as co-owner.

Invite Accepted

Note: The invite notification has disappeared, the Invites dropdown menu item isn't listing anymore.

After accepting the invite, the Owners section of the edit company page is updated with the new owner and the Pending invites Section disappeared since there are no pending invites left.

Owners Section Updated

Note: In React I use conditional rendering and state management to easily always keep the UI up-to-date with the current state of the application when the state (current data) has been changed.

Invite schema

Schema fields:

  1. Sender ID:

    • Type: mongoose.Schema.Types.ObjectId
    • Reference: "User"
    • Description: The ID of the user sending the invitation.
  2. Receiver ID:

    • Type: mongoose.Schema.Types.ObjectId
    • Reference: "User"
    • Description: The ID of the user receiving the invitation.
  3. Kind:

    • Type: String
    • Description: Specifies the type of invitation, with possible values: "company_ownership", "friend", "other". Default value is "other".
  4. Company ID:

    • Type: mongoose.Schema.Types.ObjectId
    • Reference: "Company"
    • Description: If the invitation is related to company ownership, this field contains the ID of the associated company.
  5. Kind:

    • Type: String
    • Default: "pending"
    • Description: Represents the status of the invitation. Only four possible values: "pending", "accepted", "declined", and "canceled".
  6. Timestamps:

    • Type: Automatically generated timestamps for document creation and modification.

Mongoose:

Schema:

// Instantiate `Invite` schema
const inviteSchema = new mongoose.Schema(
  {
    senderId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
      required: true,
    },
    receiverId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
      required: true,
    },
    kind: {
      type: String,
      required: true,
      default: 'other',
    },
    companyId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Company',
    },
    status: {
      type: String,
      required: true,
      default: 'pending',
    },
  },
  { timestamps: true },
)

Model:

// Create `Invite` model from `inviteSchema`
const Invite = mongoose.model('Invite', inviteSchema)

This was the visual demo for now, I will update this later on, so come back in a while to check it out!

Quick Start

To run this application locally, follow these steps:

  1. Create a free MongoDB database to connect with and obtain a MongoDB authentication URL.

  2. Clone the Repository:

    git clone git@github.com:ThomPoppins/MERN_STACK_PROJ..git MERN_STACK_PROJ && cd MERN_STACK_PROJ
  3. Set Up Backend Configuration:

    • Navigate to the /backend folder in your file explorer.
    • Create a config.js file.
    • Add the following constants and update them to your personal values:

      // backend/config.js
      // port for the server to listen on
      export const PORT = 5555
      
      // YOUR MongoDB database connection URL (if you want to test this application without creating your own database,
      // contact me at thompoppins@gmail.com, I'll provide you with a database URL)
      export const mongoDBURL =
      'mongodb+srv://exampleuser:examplepasswork@example-mern-stack-project.xhvmidl.mongodb.net/?retryWrites=true&w=majority'
      
      // Secret key for JWT signing and encryption (just generate a random string or keep it like it is for testing purposes)
      export const JWT_SECRET = 'yoursecretkey'
      
      // TEST API key for KVK API (also required)
      export const KVK_TEST_API_KEY = 'l7xx1f2691f2520d487b902f4e0b57a0b197'
      
      // PROD API key for KVK API (also required)
      export const KVK_PROD_API_KEY = ''
  4. Set Up Frontend Configuration:

    • Navigate to the /frontend folder.
    • Create a config.js file if it doesn't exist.
    • Add the following constant and export it:

      // frotend/config.js
      export const BACKEND_URL = 'http://localhost:5555'
      // Disable company validation by KVK API (If you want to test the KVK company validation, mail me at thompoppins@gmail.com for
      // instructions how to set this up.)
      export const TEST_KVK_API = false
  5. Install Dependencies:

    • Inside the /backend folder, run:

      npm install
    • Inside the /frontend folder, run:

      npm install
  6. Start the Servers:

    • Inside the /backend folder, run:

      npm run dev
    • In a separate terminal, inside the /frontend folder, run:

      npm run dev
  7. Access the Application:

    • Visit the web application in your browser using the link printed by the Vite.js server after starting the frontend server.

Now you have the application up and running locally!

Version v0.0.3 Release Notes

New Features

Basic Search Functionality

Image Crop

Company Logo Cropping

Professions in Company Profile

Storybook Integration

Testing

Animations

Co-Ownership Invites

ES Lint and Prettier configuration

Upcoming features

Version v0.0.2 Release Notes

Backend server CDN for static files

The backend server is now a CDN for static files like images. This means that the backend server will serve the static files from the /backend/public folder. This way, the frontend application can access the images from the backend server without having to store the images in the frontend application. This also makes it possible to use the backend server as a CDN for other applications that need to access the images.

File upload

Users can now upload a profile picture. The profile picture will be saved in the /backend/public/uploads/images folder and the path to the image will be saved in the database. The backend server will serve the image from the /backend/public folder. This way, the frontend application can access the image from the backend server and the image path is stored in the database.

Upload Profile Picture Modal Image Unselected

Upload Profile Picture Modal Image Selected

Profile Picture Uploaded

Version v0.0.1 Release Notes

Registering an Account

Users can easily create an account by visiting the homepage of my application. The registration process is straightforward and requires users to provide basic information such as their email address, a secure password, and any additional required details. Once registered, users gain access to the full suite of functionalities offered by the application.

Logging In

Registered users can log in to their accounts using their previously provided credentials. This allows them to access and utilize all features and services provided by the application. The login process is secure and ensures that only authorized users can access their accounts.

When you log in a JWT token is generated and stored in the browser's local storage. This token is used to authenticate the user and to make sure that the user is authorized to access the application. The token is also used to make sure that the user is authorized to access certain resources in the application. For example, the user can only access his own company resources and not the company resources of other users.

Company Registration and Ownership

Upon logging in to their account, users have the capability to register a company that they own. This action automatically designates the user as the owner of the registered company, granting them administrative privileges within the application.

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 KVK-number and submit the registration form.

Upon successful registration and validation from the KVK API, the user will be recognized as the owner of the company and will have access to all administrative functionalities associated with it.

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 a owner to the company.

Technical description:

Frontend

On the frontend, I've chosen to install React with Vite (Next Generation Frontend Tooling) for building user interfaces and Redux for state management.

Vite

The React frontend application was installed using Vite.js, a modern build tool that provides fast development server and efficient build process.

React

React is a popular JavaScript library for building user interfaces. It provides a declarative syntax for defining UI components, and uses a virtual DOM to efficiently update the UI in response to changes in state. Some of the main advantages of React include:

Redux

Redux is a state management library that is often used in conjunction with React. It provides a centralized store for managing application state, and uses a unidirectional data flow to ensure that state changes are predictable and easy to reason about. Some of the main advantages of using Redux with React include:

Backend

In the backend, I've set up RESTful API endpoints to create, read, update, and delete documents from collections. These collections are defined and configured in the /backend/models folder, ensuring a structured and organized approach to data management.

Express.js

Efficient Routing: Express.js provides a robust routing system, making it seamless to define endpoints for handling various HTTP methods like GET, POST, PUT, and DELETE. This helps in organizing the backend logic effectively, ensuring clean and maintainable code.

Middleware Support:

Express.js offers a wide range of middleware options that can be easily integrated into the application's request-response cycle. This enables functionalities like request parsing, authentication, logging, and error handling, enhancing the security and performance of the backend.

Streamlined Database Interactions: When combined with database libraries like Mongoose (for MongoDB), Express.js simplifies the process of interacting with the database. This allows for smooth retrieval, creation, updating, and deletion of data, which is essential for building a robust API.

Asynchronous Request Handling: Express.js supports asynchronous programming paradigms, allowing for non-blocking I/O operations. This is crucial for handling multiple concurrent requests efficiently, ensuring optimal performance even under heavy loads.

Cross-Origin Resource Sharing (CORS) Cross-Origin Resource Sharing (CORS) is a critical security feature that safeguards my application from unwanted sources attempting to access your resources. Express.js provides built-in support for CORS, making it easy to configure and enforce CORS policies. This helps in preventing malicious attacks like cross-site scripting (XSS) and cross-site request forgery (CSRF). It also helps in preventing unauthorized access to sensitive data.

Static files server (functioning kind of like a CDN): Overall, Express.js provides a robust and secure foundation for building RESTful APIs.

MongoDB and Mongoose

MongoDB is a popular NoSQL database that provides a flexible and scalable solution for storing and retrieving data. It uses a document-based data model, which means that data is stored in JSON-like documents instead of tables and rows. This makes it easy to store and retrieve complex data structures, and allows for more flexible data modeling compared to traditional relational databases.

Mongoose is a popular Node.js library that provides a convenient and flexible way to interact with MongoDB. It provides a schema-based approach to defining and creating models, which makes it easier to validate and enforce data consistency. It also provides a wide range of data types and validators, making it easy to ensure that my data is stored correctly and consistently.

Mongoose also provides a built-in query builder that allows you to construct complex queries using a fluent API. This makes it easy to build queries that are easy to read and understand, and can be easily modified and reused.

Mongoose also provides a middleware system that allows you to add custom behavior to your models. This includes things like pre- and post-save hooks, virtual properties, and more. This makes it easy to add custom behavior to your models without having to modify the underlying schema.

Overall, Mongoose provides a convenient and flexible way to interact with MongoDB, and it is widely used in the Node.js community for this purpose.

Secure User Authentication with JWT: In this repository, I implement secure user authentication using JSON Web Tokens (JWT). This approach offers several advantages over traditional session-based authentication methods. Below are key reasons why JWT-based authentication is a safe and effective choice:

Stateless Nature: JWTs are stateless, meaning they do not require server-side storage of session data. This eliminates the need for server-side sessions or database queries to validate user authenticity. Instead, the server can validate the token by checking its signature and expiration date, resulting in improved scalability and reduced server load.

Data Integrity and Confidentiality: JWTs are digitally signed using a secret key known only to the server. This signature ensures that the token's content has not been tampered with during transmission. Additionally, sensitive information can be encrypted within the token, providing an extra layer of security.

JWTs can include custom claims, allowing for fine-grained control over user permissions. This means you can specify which resources or actions a user is allowed to access, providing a robust authorization mechanism.

Cross-Origin Resource Sharing (CORS) Support: JWTs can be easily integrated with Cross-Origin Resource Sharing (CORS) policies. This allows for secure communication between the client and server even when they reside on different domains, without compromising security.

Easy Integration with Frontend Frameworks: JWTs can be conveniently stored on the client side, typically in browser cookies or local storage. This facilitates seamless integration with frontend frameworks and libraries (like React), enabling a smooth user experience.

Expiration and Refresh Tokens: JWTs can be configured with expiration times, reducing the window of opportunity for potential attackers. Additionally, you can implement refresh tokens to obtain new JWTs without requiring users to re-enter their credentials.

Conclusion: By implementing user authentication with JWTs, this repository ensures a robust and secure authentication mechanism. The stateless nature, data integrity, and ease of integration make JWTs an excellent choice for validating user authenticity. With careful implementation and adherence to best practices, this approach provides a reliable foundation for secure user authentication in my application.

ES Lint and Prettier

ES Lint: I'm using ES Lint to get my code up-to-date with strict code standards as much as is tolerable and logical (most strict setting is not a logical configuration). The VS Code ES Lint extension has some cool features like auto-fixing a lot of errors like the rule to enforce sorting props alphabetically so props will always be in the same order as parameter of a components as the place where the components is used in the JSX.

![ES Lint in VS Code](Invite Accepted

ES Lint config:

Frontend (Vite application with React):

Source: /frontend/.eslint.cjs

  extends: [
    'eslint:all',
    'plugin:react/all',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/strict',
    'prettier',
  ],
  overrides: [
    {
      env: {
        node: true,
      },
      files: ['.eslintrc.{js,cjs}'],
      parserOptions: {
        sourceType: 'script',
      },
    },
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['react', 'react-hooks', 'jsx-a11y'],
  rules: {
    'consistent-return': 'off',
    'max-lines-per-function': 'off',
    'no-magic-numbers': 'off',
    'no-nested-ternary': 'off',
    'no-ternary': 'off',
    'no-warning-comments': 'off',
    'one-var': 'off',
    'react-hooks/exhaustive-deps': 'warn',
    'react-hooks/rules-of-hooks': 'error',
    'react/display-name': 'error',
    'react/forbid-component-props': [
      'error',
      { allow: ['className'], forbid: [] },
    ],
    'react/function-component-definition': [
      'error',
      {
        namedComponents: 'arrow-function',
        unnamedComponents: 'arrow-function',
      },
    ],
    'react/jsx-key': 'error',
    'react/jsx-max-depth': ['error', { max: 5 }],
    // Allow arrow functions in JSX props (Remove this rule when performance becomes an issue)
    'react/jsx-no-bind': ['error', { allowArrowFunctions: true }],
    'react/jsx-no-comment-textnodes': 'error',
    'react/jsx-no-literals': 'off',
    'react/jsx-no-target-blank': 'error',
    'react/jsx-no-undef': 'error',
    'react/jsx-uses-react': 'error',
    'react/jsx-uses-vars': 'error',
    'react/no-children-prop': 'error',
    'react/no-danger-with-children': 'error',
    'react/no-deprecated': 'error',
    'react/no-direct-mutation-state': 'error',
    'react/no-find-dom-node': 'error',
    'react/no-is-mounted': 'error',
    'react/no-render-return-value': 'error',
    'react/no-string-refs': 'error',
    'react/no-unescaped-entities': 'error',
    'react/no-unknown-property': 'error',
    'react/prop-types': 'error',
    'react/react-in-jsx-scope': 'error',
    'react/require-render-return': 'error',
    'sort-imports': 'off',
    'sort-vars': 'off',
    'sort-keys': 'off',
    // TODO: Set the no-console rule to error when going in to production
    'no-console': 'warn',
  },
  settings: {
    react: {
      linkComponents: [
        // Components used as alternatives to <a> for linking, eg. <Link to={ url } />
        'Hyperlink',
        { linkAttribute: 'to', name: 'Link' },
      ],
      version: 'detect',
    },
  },
}

Backend config

Source: /backend/.eslint.cjs

'use strict'

module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: 'eslint:all',
  overrides: [
    {
      env: {
        node: true,
      },
      files: ['.eslintrc.{js,cjs}'],
      parserOptions: {
        sourceType: 'script',
      },
    },
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  rules: {
    'new-cap': 'off',
    'no-magic-numbers': 'off',
    'one-var': 'off',
    'sort-imports': 'off',
    'sort-vars': 'off',
    'sort-keys': 'off',
    'no-console': 'off',
    'multiline-comment-style': 'off',
  },
}

Prettier code formatter

I use Prettier code formatter to format my code in a way I find most readable.

Source: /frontend/.prettierrc and /backend/.prettierrc

{
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "trailingComma": "all",
  "jsxSingleQuote": true,
  "bracketSpacing": true,
  "endOfLine": "lf"
}

Project management

Jira

Im using a Jira board with 4 swim lanes: TODO, IN PROGRESS, BUSY and DONE to sort my project issues.

Jira Board

TODO issues I haven't started working on yet.

IN PROGRESS issues are in progress.

BUSY issues are the issues I am working on and have higher priority to finish. A lot of the times other issues are dependent on the BUSY issues, that's the main reason I chose to add this swim lane.

DONE issues are finished.

Project Issue Progression

Production checklist

Issues to be created in Jira

Prerequisites

Steps

Step 1: Record Your Screen

Use a screen recording tool to capture the content you want. OBS Studio, ShareX, or QuickTime (on macOS) are good options.

Step 2: Edit the Recording (if needed)

Trim unnecessary portions from your recording using video editing software.

Step 3: Convert Video to GIF with FFmpeg

Open a command prompt or terminal and use FFmpeg to convert your video to GIF. Adjust the parameters to achieve the desired quality and file size:

ffmpeg -i input.mp4 -vf "fps=15,scale=640:-1:flags=lanczos" -c:v gif output.gif

Step 4: Optimize GIF with Gifski

Use Gifski to optimize and compress the GIF:

gifski -o output.gif input.mp4

This command will compress the GIF while maintaining high quality.

Step 5: Check File Size

Ensure the resulting GIF is smaller than 100MB. If it's still too large, consider adjusting parameters or shortening the duration.

Step 6: Repeat if Necessary

If the GIF is still too large, you may need to compromise on quality, reduce resolution, or shorten the duration further.

Step 7: Upload to GitHub

Once satisfied with the quality and size, upload the GIF to your GitHub repository.

Remember to test the GIF on GitHub to ensure it meets the platform's requirements. If necessary, further optimization may be needed. END INSTRUCTIONS

DocsGPT

Absolutely! Nextra is a great choice for building an informational website. To provide an interactive experience and keep your readers entertained, here are some suggestions:

  1. Embed Interactive Widgets: You can embed interactive widgets such as maps, charts, sliders, or calculators to make the content more engaging. Tools like Google Maps API, Chart.js, and React Slider can be helpful for this purpose.

  2. Add Animations: Animations can add visual interest to your website. Use libraries like React Spring or Framer Motion to easily incorporate animations into different elements such as text, images, or buttons.

  3. Include Interactive Quizzes or Surveys: Integrate interactive quizzes or surveys within your website to make it more interactive. Tools like Typeform or Google Forms can help you create and embed interactive forms.

  4. Implement Interactive Code Examples: If you are writing about code or programming concepts, consider using code playgrounds like CodePen or CodeSandbox to provide interactive code examples that users can modify and execute directly on your website.

  5. Integrate Social Media Feeds: Display social media feeds related to your application or topic to provide real-time updates and encourage user engagement. You can use APIs provided by popular social media platforms like Twitter, Instagram, or Facebook to fetch and display the feeds.

  6. Include Interactive Infographics: Infographics can present complex information in a visually appealing way. Use tools like Infogram or Canva to create interactive infographics and embed them into your website.

  7. Incorporate Gamification Elements: Gamify your website by adding interactive features like badges, points, or progress bars. Users can earn rewards for completing certain actions or reaching milestones on your website.

Remember to consider your target audience and the purpose of your website while incorporating interactive elements. Too many or overly complex interactive features may distract users from the main content. Strive for a balance between interactivity and readability to enhance the overall user experience.