Landing Page Builder using MERN

ยท

8 min read

Problem Statement: I thought about the problem that some people face as they don't know frontend development but still want their landing page to showcase skills and projects so I build Designfolio , a website where a user can have a portfolio and unique URL link to it in a few steps.

Flow of the Project:

This is the flow of the project I created before starting coding to have a clear about it.

Frontend

This blog is about creating the frontend part of the project. We will discuss the backend code in part 2.

I have created four child components which include four forms(basic details, skills, projects and user image). These four components will be children to one parent component so they will be rendered one by one in the same route. We will be passing all the form data to one parent component using props and then will save all data to a database in one API call.

Parent Component

import { React, useState } from "react";
import FormData from "form-data";
import Details from "../Details/Details";
import Skills from "../Skills/Skills";
import Projects from "../Projects/Projects";
import Imageupload from "../Image/Imageupload";
import "./Portfolio.css";
import Loadingspinner from "../Loadingspinner/Loadingspinner";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { useStateManager } from "react-select";
function Portfolio() {
  const navigate = useNavigate();
  const [personalInfo, setPersonalInfo] = useState({});
  const [skills, setSkills] = useState([]);
  const [projectData, setProjectData] = useState([]);
  const [userImage, setUserImage] = useState({});
  const [currentComponent, setCurrentComponent] = useState(1);
  const [isloading, setLoading] = useState(false);
  const handleCreate = (e) => {
    setLoading(true);
    e.preventDefault();
    const projectsToSend = projectData.map((item) => ({
      projectName: item.projectName,
      projectDescription: item.projectDescription,
      projectLink: item.projectLink,
    }));
    const formData = {
      name: personalInfo.firstName,
      email: personalInfo.email,
      role: personalInfo.role,
      linkedinlink: personalInfo.linkedin,
      githublink: personalInfo.github,
      behancelink: personalInfo.behance || "",
      description: personalInfo.userDescription,
      skill: skills,
      projects: projectsToSend,
      myFile: userImage,
    };
    axios({
      method: "POST",
      url: `${process.env.REACT_APP_PORTFOLIO}/user/create`,
      headers: {
        "Content-Type": "multipart/form-data",
      },
      data: formData,
    })
      .then((res) => {
        navigate("/usercreated", { state: { myProp: personalInfo.email } });
        setLoading(false);
      })
      .catch((err) => {
        console.log(err);
      });
  };
  const handlePersonalInfo = (data) => {
    setPersonalInfo(data);
    setCurrentComponent(currentComponent + 1);
  };

  const handleUserSkills = (data) => {
    setSkills(data);
    setCurrentComponent(currentComponent + 1);
  };
  const handleProjectData = (data) => {
    setProjectData(data);
    setCurrentComponent(currentComponent + 1);
  };
  const handleUserImage = (data) => {
    setUserImage(data);
    setCurrentComponent(currentComponent + 1);
  };
  return (
    <div>
      {currentComponent === 1 && <Details onSubmit={handlePersonalInfo} />}
      {currentComponent === 2 && <Skills onSubmit={handleUserSkills} />}
      {currentComponent === 3 && <Projects onSubmit={handleProjectData} />}
      {currentComponent === 4 && <Imageupload onSubmit={handleUserImage} />}
      {currentComponent === 5 && (
        <div className="final-box">
          <div className="flex justify-center">
            <div className="final-box-input">
              <p className="text-white text-lg font-poppins">
                I hope you had filled all the information correct. To generate
                your portfolio link click the button below ๐Ÿ‘‡.
              </p>
              <div className=" mt-2 text-center">
                <button
                  className="generate-btn font-poppins"
                  onClick={handleCreate}
                >
                  {isloading ? <Loadingspinner /> : <span>Create Portfolio</span>}

                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
export default Portfolio;

This is the parent component where all data will be grabbed on the onSubmit event on all child components.

Child Components

Details form

Here one function was for word counter in the description textarea, that is I am skipping right now. One api call is also made here to double check if email already exists in database or not.

import axios from "axios";
import React, { useState } from "react";
import "./Details.css";
function Details(props) {
  const [firstName, setFirstName] = useState("");
  const [email, setEmail] = useState("");
  const [role, setRole] = useState("");
  const [linkedin, setLinkedin] = useState("");
  const [github, setGithubLink] = useState("");
  const [behance, setBehanceLink] = useState("");
  const [userDescription, setUserDescription] = useState("");
  const [error, setError] = useState("");
  const handleSubmit = (event) => {
    event.preventDefault();
    axios({
      method: "POST",
      url: `${process.env.REACT_APP_PORTFOLIO}/find/user?email=${email}`,
    }).then((res) => {
      if (res.data == "User not found") {
        const personalInfo = {
          firstName,
          email,
          role,
          linkedin,
          github,
          behance,
          userDescription
        };
        props.onSubmit(personalInfo);
        setError("");
      } else {
        setError("Looks like you already have a portfolio.");
      }
    });
  };
  return (
    <div className="detail-main-div">
      <header className="heading font-poppins py-2 px-4 text-center">
        Details
      </header>
      <div className="detail-inputbox flex justify-center">
        <form
          className="detail-form text-white font-poppins"
          onSubmit={handleSubmit}
        >
          <div>
            <label className="px-4 my-2" htmlFor="firstName">
              Full Name*
            </label>
            <br />
            <input
              className="detail-input my-2 mx-2"
              type="text"
              id="firstName"
              placeholder="John Doe"
              value={firstName}
              onChange={(event) => setFirstName(event.target.value)}
              required
            />
          </div>
          <div>
            <label className="px-4 py-2" htmlFor="email">
              Email*
            </label>
            <br />
            <input
              className="detail-input my-2 mx-2"
              type="email"
              id="email"
              placeholder="johndoe@gmail.com"
              value={email}
              onChange={(event) => setEmail(event.target.value)}
              required
            />
          </div>
          <div>
            <label className="px-4 py-2" htmlFor="role">
              Role*
            </label>
            <br />
            <input
              className="detail-input my-2 mx-2"
              type="text"
              id="role"
              value={role}
              onChange={(event) => setRole(event.target.value)}
              placeholder="e.g App Developer"
              required
            />
          </div>
          <div>
            <div style={{ width: "300px" }} className="flex justify-between">
              <label className="px-4 py-2" htmlFor="description">
                Describe Yourself*
              </label>
              <div className="pt-2 text-[#d7d7d7]">{wordCount}/20</div>
            </div>
            <textarea
              className="description-detail-input my-2 mx-2"
              type="text"
              id="description"
              value={userDescription}
              onChange={handleDescriptionChange}
              placeholder="e.g I am a UK based App developer who can create fast and robust apps"
              required
            />
          </div>
          <div>
            <label className="px-4 py-2" htmlFor="linkedin">
              Linked in*
            </label>
            <br />
            <input
              className="detail-input my-2 mx-2"
              type="url"
              id="linkedinUrl"
              value={linkedin}
              onChange={(event) => setLinkedin(event.target.value)}
              placeholder="https://linkedin.com/in/john-doe"
              required
            />
          </div>
          <div>
            <label className="px-4 py-2" htmlFor="github">
              Github (if available)
            </label>
            <br />
            <input
              className="detail-input my-2 mx-2"
              type="url"
              id="githubUrl"
              value={github}
              onChange={(event) => setGithubLink(event.target.value)}
              placeholder="https://github.com/johndoe"
            />
          </div>
          <div>
            <label className="px-4 py-2" htmlFor="behance">
              Behance (if available)
            </label>
            <br />
            <input
              className="detail-input my-2 mx-2"
              type="url"
              id="behanceUrl"
              value={behance}
              onChange={(event) => setBehanceLink(event.target.value)}
              placeholder="https://behance.net/johndoe"
            />
          </div>
          <button
            style={{ border: "1px black solid" }}
            className="mt-4 detail-start-btn"
            type="submit"
          >
            Next
          </button>
          {error !== "" ? (
            <div className="px-2 font-poppins text-white">
              {error}{" "}
              <a
                target="_blank"
                className="underline"
                rel="noreferrer"
                href={`https://designfolio.onrender.com/${username}`}
              >
                Click here to visit
              </a>{" "}
            </div>
          ) : null}
        </form>
      </div>
    </div>
  );
}
export default Details;

Skills form

react-select package is used to create a dropdown menu for over 50 skills in options. Skill data is stored in different js file with label and value of every skill.

import React, { useState } from "react";
import options from "./options.js";
import Select from "react-select";
import "./Skills.css";
function Skills(props) {
  const [selectedOptions, setSelectedOptions] = useState([]);
  const [error, setError] = useState("");
  function handleSelect(data) {
    setSelectedOptions(data);
  }
  const handleSubmit = (event) => {
    event.preventDefault();
    if (selectedOptions.length > 0) {
      const userSkills = selectedOptions.map((item) => item.label);
      props.onSubmit(userSkills);
    } else {
      setError("You have to select atleast 1 skill");
    }
  };
  return (
    <div className="skill-main-div">
      <header className="heading text-center font-poppins py-2 px-4 ">
        Skills
      </header>
      <p className="px-4 text-white text-center font-poppins">
        Select skills you have and tech stack you have worked on.
      </p>
      <div className="flex justify-center mt-4">
        <div className="select-form">
          <Select
            className="select-option text-black font-poppins"
            options={options}
            placeholder="Select Skills"
            value={selectedOptions}
            onChange={handleSelect}
            isSearchable={true}
            isMulti
          />
          {error !== "" ? (
            <div className="px-2 font-poppins text-white">{error}</div>
          ) : null}
          <hr />
          <button
            className="skill-btn font-poppins"
            onClick={handleSubmit}
            type="submit"
          >
            Save
          </button>
        </div>
      </div>
    </div>
  );
}
export default Skills;

Projects form

One array is created to store all the array and then save projects button will handle the sending data of all projects to a parent component. Here I am ignoring the validator and word counter function, you can add as per your choice.

import { React, useState } from "react";
import "./Projects.css";
import validator from "validator";
function Projects(props) {
  const [projectName, setProjectName] = useState("");
  const [projectDescription, setProjectDescription] = useState("");
  const [projectLink, setProjectLink] = useState("");
  const [projectArray, setProjectArray] = useState([]);
  const [error, setError] = useState("");
  const [wordCount, setWordCount] = useState(0); 
  const [emptyArrayError, setArrayError] = useState("");
  const handleAddOption = (event) => {
    event.preventDefault();
    if (projectName !== "" && projectDescription !== "" && projectLink !== "") {
      const newProject = { projectName, projectDescription, projectLink };
      if (newProject) {
        setProjectArray([...projectArray, newProject]);
        setProjectName("");
        setProjectDescription("");
        setProjectLink("");
        setError("")
        setArrayError("")
        document.getElementById("projectLink").style.border = "1px grey solid";
      }
    }
    else{
      setError("Please add all fields to add project")
    }
  };
  const handleSubmit = (event) => {
    event.preventDefault();
    if(projectArray.length >0){
      const userProjects = projectArray;
      props.onSubmit(userProjects);
    }
    else{
      setArrayError("Add atleast 1 project to proceed.")
    }
  };
  return (
    <div className="project-main-div">
      <header className="heading font-poppins text-center py-2 px-4 ">
        Projects
      </header>
      <p className="px-4 text-white text-center font-poppins">
        Add your best projects. Try to add not more than 4 projects and make
        sure you add links of the project correctly
      </p>
      <div className="flex justify-center">
        <form
          className="project-input-box text-white font-poppins"
          onSubmit={handleSubmit}
        >
          <label className="px-4 my-2" htmlFor="projectname">
            Name:
          </label>
          <br />
          <input
            className="project-input my-2 mx-2"
            type="text"
            value={projectName}
            onChange={(event) => setProjectName(event.target.value)}
          />
          <div style={{width:"300px"}} className="flex justify-between">
          <label className="px-4 my-2" htmlFor="projectdescription">
            Short Description:
          </label>
          <div className="pt-2 text-[#d7d7d7]">{wordCount}/30</div>
          </div>
          <textarea
            className="project-description-input my-2 mx-2"
            id="projectDescription"
            type="text"
            value={projectDescription}
            onChange={handleProjectDescriptionChange}
          />
          <br />
          <label className="px-4 my-2" htmlFor="projectlink">
            Link:
          </label>
          <br />
          <input
            id="projectLink"
            className="project-input my-2 mx-2"
            type="url"
            style={{ border: "1px grey solid" }}
            value={projectLink}
            onChange={(event) => validate(event.target.value)}
          />
          <br />
          <button className="project-add-btn" onClick={handleAddOption}>
            Add to list
          </button>
          <br />
          {error!==""? <div className="p-2 font-poppins text-white"> <span>โ—</span> {error}</div> :null}
          <hr />
          <p className="px-2 mt-2 text-white font-poppins">Project List</p>
          <div className="flex flex-wrap">
            {projectArray.map((option) => (
              <div
                style={{
                  backgroundColor: "#f1f0f0",
                  padding: "8px",
                  margin: "8px",
                  borderRadius: "8px",
                }}
                className="text-black font-poppins"
              >
                <p className="font-bold text-lg"> {option.projectName}</p>
              </div>
            ))}
            {emptyArrayError!==""? <div className="px-2 font-poppins text-white">{emptyArrayError}</div> :null}
          </div>
          <br />
          <button className="start-btn" type="submit">
            Save projects
          </button>
        </form>
      </div>
    </div>
  );
}
export default Projects;

Image upload

Two components are handling the uploading of image of user. One component take the data of image that is being uploaded and another component is keeping the file in the state of the component which will trigger the submit event and send a file to a parent component.

import { React, useState } from "react";
import "./Image.css";
function Image({ onFileSelect }) {
  const [selectedFile, setSelectedFile] = useState(null);
  const handleFileInput = (event) => {
    setSelectedFile(event.target.files[0]);
  };
  const handleSubmit = (event) => {
    event.preventDefault();
    onFileSelect(selectedFile);
  };
  return (
    <div className="">
      <form className="text-white font-poppins" onSubmit={handleSubmit}>
        <input type="file" onChange={handleFileInput} />
        <br />
        <button className="upload-btn mt-2" type="submit">
          Upload
        </button>
      </form>
    </div>
  );
}
export default Image;
// handling file upload (Main Component to be rendered)
import { React, useState } from "react";
import Image from "./Image";
import "./Image.css";
function Imageupload(props) {
  const [file, setFile] = useState(null);
  const [error, setError] = useState("");
  const handleFileSelect = (selectedFile) => {
    setFile(selectedFile);
  };
  const handleSubmit = (event)=>{
    event.preventDefault();
    if(file!=null){
      props.onSubmit(file);
    }
    else{
      setError("Please upload file to proceed");
    }
  }
  return (
    <div className="image-div">
      <header className="text-center heading font-poppins py-2 px-4">Upload Image</header>
      <p className='px-4 text-white text-center font-poppins'>Upload a good photo of yours which will be shown in your portfolio</p>
      <div className="flex justify-center my-2 p-4">
        <div className="image-input">
        <Image onFileSelect={handleFileSelect} />
        <br />
        <p className="font-poppins text-white">Selected file: {file && file.name}</p>
        <button className="image-btn" onClick={handleSubmit} type="submit">Next</button>
        <br />
        {error!==""? <div className="px-2 font-poppins text-white"> <span>โ—</span> {error}</div> :null}
        </div>
      </div>
    </div>
  );
}
export default Imageupload;

Wrapping up

Github repo link: https://github.com/ishan249/design-folio-frontend

Live project link: https://designfolio.onrender.com/

Part 2: https://ishanpatel.hashnode.dev/landing-page-builder-backend-part-2

We have successfully created four child components that will pass all user's information to one parent component (Portfolio.jsx) on submit event. In part 2 we will see how to save user's data to MongoDB and render it into the landing page of a user.

ย