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.
New Features
Basic Search Functionality
Users can now search for companies by name.
Search results are listed in a grid with company images.
Clicking on a company navigates the user to the company details page (Company Profile).
If the current logged in user is a member (owner, employee, admin etc.) of the company, private details will be shown on the company profile page.
Find button is disabled if no search input value is given:
Search is ready to find pro's!
CSS Grid used for structuring the Result components (the clickable company specific rectangle that redirects to their profile/details page)
Current user is member (owner) of this company, private details revealed:
Private details hidden, user not member of this company:
Image Cropping
Users can upload an image and crop it via drag-n-drop or file selection.
Preview canvas shows the user how the crop will be in a round circled shape.
The cropped image is displayed as the default profile picture.
After cropping, users can download the cropped file.
Cropped base64 binary images are converted into BLOB and uploaded to the Express.js server as a png file.
Images are served as static files by the /backend Express.js server, acting as an images/files CDN for the frontend application.
/profile Profile page from a new user that has not uploaded a profile image yet.
Clicking the 'Upload' button on the placeholder
Drop an image file on the square
frontend\src\components\users\EditProfilePictureModal.jsx: Implementing image file-drop zone with react-dropzone.
After dropping the image in the dropzone a ReactCrop component is rendered and a preview canvas:
Setting the crop I wish to download a PNG file of.
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.
/*
* 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.
When you have selected the crop you wish, the user can click the 'Upload' button:
**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:
Company Logo Cropping
Implemented image crop functionality for company logos in the register and edit company pages.
Company logos are displayed in a circle-shaped frame throughout the application.
When cropping the logo image, companies can see how the logo will look within the circle-shaped border.
Company "logo's" can get cropped the same way as profile pictures with 1 / 1 aspect ratio:
Select crop:
Upload image:
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.
Professions in Company Schema
Added a new field to the company model for professions.
Companies can add professions to their profiles during registration.
Registereing companies professions will help the search result to become more relevant and dynamic.
Storybook Integration
Installed Storybook for component development.
Added a few initial components to Storybook.
Testing
Jest and React-Testing-Library installed and functional.
Ongoing work on writing tests for almost every component.
Animations
Added subtle animations for a more interactive user experience.
Notification animations: when a user has a pending 'Invite,' the icon in the navigation bar right to their name turns yellow and starts wiggling, asking the user for attention.
Dropdown has an "Invites" menu item on top with jumping letters to get the user's attention to the Invites page after clicking "Invites" in the dropdown menu.
Subtle edit of Tailwind preinstalled animations making it a lot more joyful and grabs the user attention better:
Notification icon wiggles when a user is invited for co-ownership of a company.
"Invites" menu item is dynamically added to the dropdown menu.
The menu item jumps to draw attention to pending co-ownership invites.
Users can accept or decline co-ownership invites.
After accepting or declining all pending invites (so no pending invites are left), the user gets redirected to the /companies page where all companies are listed that the user now (co-)owns.
Safety measures implemented to prevent users from being invited twice, ensuring a unique co-owner status and no duplicate ownership registrations.
Pending issues
Edit company professions feature is still pending.
More components need to be added to Storybook.
Ongoing work on writing tests for components.
Editing company professions is still pending.
After implementing professions in the Edit company page, the main search functionality will use professions as the second value to match on.
Version v0.0.2 Release Notes
Backend server CDN for static files
The backend server now acts as a CDN for static files like images.
The backend server serves static files from the /backend/public folder.
This enables the frontend application to access images from the backend server without storing them in the frontend application.
It also allows using the backend server as a CDN for other applications needing to access the images.
File Upload
Users can upload a profile picture.
The profile picture is saved in the /backend/public/uploads/images folder.
The image path is saved in the database.
The backend server serves the image from the /backend/public folder.
This enables the frontend application to access the image from the backend server, with the image path stored in the database.
Version v0.0.1 Release Notes
Registering an Account
Users can easily create an account by visiting the homepage of the application.
The registration process requires basic information such as email address, a secure password, and additional required details.
Once registered, users gain access to the full suite of functionalities.
Logging In
Registered users can log in using their provided credentials.
The login process is secure, ensuring only authorized users access their accounts.
Upon login, a JWT token is generated and stored in the browser's local storage for authentication.
Company Registration and Ownership
Users can register a company upon logging in, automatically designating them as the owner.
Ownership privileges grant full administrative control over the company's operations.
Co-ownership functionality allows users to add co-owners to a company.
How to Register a Company
Log in to your account.
Navigate to Companies.
Click the plus icon to add a new company.
Fill in company details with the KVK-number and submit the registration form.
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
Log in to your account.
Navigate to Companies.
Click the pencil icon to edit a company.
Search for a user by name, username, or email.
Click the add button to add the user as an owner to the company.
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.
New Features
Basic Search Functionality
Image Cropping
Company Logo Cropping
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:
Co-Ownership Invites
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
How to Add a Co-owner to a Company