Setelah backend, next lanjut buat antarmuka untuk user.

Frontend

Di dalam folder blog_project buat ReactJS App dan jalankan server.

dany@hp:~/blog_project$ npx create-react-app frontend
dany@hp:~/blog_project$ cd frontend
dany@hp:~/blog_project/frontend$ npm run start

Install axio, react-router-dom dan mui/material

dany@hp:~/blog_project/frontend$ npm install --save axios react-router-dom @mui/material

Pertama-tama kita buat file konfigurasi untuk menyimpan parameter API URL backend yang akan digunakan.

class Config {
    static API_URL =  'http://localhost:8000'
    
}

export default Config;

Tambahkan stylesheet font-family Roboto di folder public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;200;300;400;700;900&display=swap" rel="stylesheet">
    
    <title>DanyNotes</title>

    <style>
       *  {
          font-family: 'Roboto', sans-serif;
          margin: 0;
          overflow-x: hidden;
      }

  
    </style>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root">
      
    </div>

  </body>
</html>

Kita buat beberapa komponen untuk portfolio ada intro, menu, portfolio, testimonials, topbar, works dan untuk blog terdapat komponen feature post, kategori menu, blog list, blog detail.

So Lanjut Modifikasi App.jsx beserta scss-nya, dan buat Layout.jsx di dalam folder hocs(higher-order Component)


// App.jsx

import React from 'react';
import { BrowserRouter as Router, Switch } from 'react-router-dom';
import Layout from './hocs/Layout';


class App extends React.Component {
    render() {
        return (
            <Router>
                <Switch>
                    <Layout></Layout>
                </Switch>
            </Router>
        );
    }
}

export default App;

// hocs/layout.jsx

import React, { useEffect, useState } from 'react';
import Menu from "../components/menu/Menu";
import PersonalWebComponent from '../components/PersonalWebComponent';
import BlogWebComponent from '../components/BlogWebComponent';
import { Link } from 'react-router-dom';
import "../app.scss";
import "../components/topbar/topbar.scss";


const Layout = (props) => {
    const [menuOpen, setMenuOpen] = useState(false);
    const [LinkClicked, setLinkClicked] = useState(false);
    const [HideSection, setHideSection] = useState(false);
    const [HideBlog, setHideBlog] = useState(true);

    // persist state on refresh, Using LocalStorage — Functional Components

    useEffect(() => {
        setHideSection(JSON.parse(window.localStorage.getItem('HideSection')));
        setLinkClicked(JSON.parse(window.localStorage.getItem('LinkClicked')));
        setHideBlog(JSON.parse(window.localStorage.getItem('HideBlog')));
    }, []);

    useEffect(() => {
        window.localStorage.setItem('HideSection', HideSection);
        window.localStorage.setItem('LinkClicked', LinkClicked);
        window.localStorage.setItem('HideBlog', HideBlog);
    }, [HideSection, LinkClicked, HideBlog]);

    return (
        
        <div className="app">
            <div className={"topbar " + (menuOpen && "active") } id="topbar">
                <div className="wrapper">
                    <div className="left">
                        <Link to="/" className="logo" onClick={() => { setMenuOpen(false); setLinkClicked(false); setHideSection(false); setHideBlog(true) }}>DNotes</Link>
                    </div>
                    
                    <div className="right">
                        <div className={"linkmenu " + (LinkClicked && "clicked")}>
                            <Link to="/blog" className="linkto" onClick={() => { setLinkClicked(true); setHideSection(true); setHideBlog(false) } }>Blog</Link>
                        </div>
                        
                        <div className="hamburger" onClick={() => { setMenuOpen(!menuOpen);setLinkClicked(false); setHideSection(false); setHideBlog(true) }}>
                            <span className="line1"></span>
                            <span className="line2"></span>
                            <span className="line3"></span>
                        </div>
                    </div>
                </div>
            </div>

            <Menu menuOpen={menuOpen} setMenuOpen={setMenuOpen}/>
            <PersonalWebComponent HideSection={HideSection}/>
            {props.children}
            <BlogWebComponent HideBlog={HideBlog}/>
        </div>
    );
}

export default Layout;

Selanjutnya buat scss dan komponen yang digunakan di layout global.scss, app.scss dan topbar.scss, dan komponen Menu, PersonalWebComponent dan BlogWebComponent


// app.scss

.app{
    height: 100vh;

    .sections{
        width: 100%;
        height: calc(100vh - 70px);
        background-color: whitesmoke;
        position: relative;
        top: 70px;
        scroll-snap-type: y mandatory;
        scroll-behavior: smooth;
       
        scrollbar-width: none; //for firefox
        &::-webkit-scrollbar{
            display: none;
        }

        > *{
            width: 100vw;
            height: calc(100vh - 70px);
            scroll-snap-align: start;
        }

    }

    .clear {
        
        scrollbar-width: none; //for firefox
        &::-webkit-scrollbar{
            display: none;
        }
    }

    .blog {
        width: 100%;
        padding-top: 70px;
     
    }

  
}

// global.scss

$mainColor: #15023a;

$color-primary: #328da8;
$color-secondary: #47acad;
$color-tertiary: #2fa18a;
$color-quaternary: #87369e;

$color-grey-light: #efefef;
$color-grey-dark: #333;

$color-hover: #edf5f8;
$color-link-hover: #ccc;

$color-paragraph: #4a4a4a;

$color-green: #34eb49;

$color-white: #fff;
$color-black: #000;

// button submit
$bgColor: #fafafa;
$shadow: rgba(lighten($bgColor, 10%),.2);
$accentColor: #1eaacd;
$secondaryColor: #C5C5C5;
$btnWidth: 175px;
$btnHeight: 50px; // Firefox users - don't touch!
$borderRadius: 35px;
$btnBorder: 2px;
$loaderStroke: 3px;
$dribbble: #ea4c89;

// Font
$default-font-size: 1.6rem;

// Grid
$grid-width: 114rem;
$gutter-vertical: 4rem;
$gutter-vertical-small: 2rem;
$gutter-horizontal: 4rem;

$width: 768px;

@mixin mobile {
    @media (max-width: #{$width}){
        @content
    }
}

/* components/topbar/topbar.scss */

@import "../../global.scss";


.topbar {
    width: 100%;
    height: 70px;
    background-color: whitesmoke;
    color: $mainColor;
    position: fixed;
    top: 0;
    z-index: 3;
    transition: all 1s ease;
    overflow-y: hidden;

    .wrapper{
        padding: 10px 30px;
        display: flex;
        align-items: center;
        justify-content: space-between;

        .left{
            display: flex;
            align-items: center;

            .logo{
                font-size: 40px;
                font-weight: 700;
                text-decoration: none;
                color:inherits ;
                margin-right: 40px;
            }

            .itemContainer{
                display: flex;
                align-items: center;
                margin-left: 30px;

                @include mobile {
                    display: none;
                }

                .icon{
                    font-size: 18px;
                    margin-right: 5px;
                }

                span{
                    font-size: 15px;
                    font-weight: 500;
                }
            }

        }

        .right{

            display: flex;
            align-items: center;

            .linkmenu {
                padding-right: 40px;

                .linkto {
                    font-size: 18px;
                    font-weight: 500;
                    text-decoration: none;
                    color: inherits;
                }
                
                &.clicked {
                    cursor: default;
                    pointer-events: none;        
                    text-decoration: none;

                    .linkto {
                        color: grey;
                    }
                }

            }

            .hamburger{
                width: 32px;
                height: 25px;
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                cursor: pointer;

                span{
                    width: 100%;
                    height: 3px;
                    background-color: $mainColor;
                    transform-origin: left;
                    transition: all 2s ease;
                }

            }
        }
    }

    &.active{
        background-color: $mainColor;
        color: white;

        .hamburger{
            span{
                &:first-child{
                    background-color: white;
                    transform: rotate(45deg);
                }
                &:nth-child(2){
                    opacity: 0;
                }
                &:last-child{
                    background-color: white;
                    transform: rotate(-45deg);
                }
            }
            overflow-y: hidden;
        }
    }
}

Component Menu dan scss-nya di folder src/components/menu

// src/components/menu/menu.jsx 

import "./menu.scss";


export default function Menu({ menuOpen, setMenuOpen }) {
    const listmenu = [
        {
            id: 1,
            hastag: "home",
            title: "Home",
            
        },
        {
            id: 2,
            hastag: "portfolio",
            title: "Portfolio",
            
        },
            id: 3,
            hastag: "works",
            title: "Works",
            
        },
        {
            id: 4,
            hastag: "testimonials",
            title: "Testimonials",
            
        },
        {
            id: 6,
            hastag: "contact",
            title: "Contact",
            
        },
    ]
    return (
        <div className={"menu " + (menuOpen && "active")} id="menu">
            <ul>
                {listmenu.map((item) => (
                    <li key={item.id} onClick={()=>setMenuOpen(false)}>  
                        <a href={"/#" + item.hastag}>{item.title}</a>
                    </li>
                ))}
            </ul>
        </div>
    );
}

/* src/components/menu/menu.scss */

@import "../../global.scss";


.menu{
    width: 300px;
    height: 100vh;
    background-color: $mainColor;
    position: fixed;
    top:0;
    right: -300px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    transition: all 1s ease;

    &.active{
        right: 0;

    }

    ul {
        margin: 0;
        padding: 0;
        list-style: none;
        font-size: 30px;
        font-weight: 300;
        color: white;
        width: 60%;

        li{
            margin-bottom: 25px;
            a{
                font-size: inherit;
                color:inherit;
                text-decoration: none;
            }

            &:hover {
                font-weight: 500;
            }
        }
    }
}

Personal Web Component

// src/components/PersonalWebComponent.jsx

import React from 'react';
import { Route } from 'react-router-dom';
import Home from "./intro/Intro";
import Portfolio from "./portfolio/Portfolio";
import Works from "./works/Works";
import Testimonials from "./testimonials/Testimonials"
import Contacts from "./contact/Contact";
import PropTypes from 'prop-types';


class PersonalWebComponent extends React.Component {
    render() {
        if(this.props.HideSection) {
            return null;
        } else {
            return (
                <div className={"sections"}>
                    <Route exact path="/" component={Home}/>
                    <Route exact path="/" component={Portfolio}/>
                    <Route exact path="/"><Works/></Route>
                    <Route exact path="/"><Testimonials/></Route>
                    <Route exact path="/"><Contacts/></Route>
                </div> 
            );
        }
    }
}

PersonalWebComponent.propTypes = {
    HideSection: PropTypes.bool
}

PersonalWebComponent.defaultProps = {
    HideSection: false
}

export default PersonalWebComponent;

Next buat komponen-komponen yang terdapat di Personal Web Component (portfolio) :

1. Intro

// src/components/intro/Intro.jsx

import "./intro.scss";
import axios from "axios";
import { init } from 'ityped';
import React, { useState, useEffect, useRef } from "react";
import introimage from "../assets/dany_intro_img.png"
import downarrow from "../assets/down.png"


export default function Intro() {
    const [profile, setProfile] = useState([]);
    const [displayedContent, setDisplayedContent] = useState("");
  
    useEffect(() => {
        const { default: Config } = require("../../utils/config")
        const fetchProfile = async () => {
            try {
                const fullres = await axios.get(Config.API_URL+"/api/blog/profile/");
                setProfile(fullres.data[0].arr_worktitle);
                
            }
            catch (err) {

            }
        }

        fetchProfile();
    }, []);

   

    useEffect(() => {
        setDisplayedContent((displayedContent)=> displayedContent + profile) 
    }, [profile]);

    const textRef = useRef();

    useEffect(() => {
        const strings = [displayedContent]

        init(textRef.current, {
            showCursor: false,
            typeSpeed: 20,
            backDelay: 1500,
            backSpeed: 60,
            strings: [strings],
        });
    }, [displayedContent]);
 
    return (
        <div className="intro" id="home">
            <div className="left">
                <div className="imgContainer">
                    <img src={introimage} alt="" /> 
                </div>
            </div>

            <div className="right">
                <div className="wrapper">
                    <h2>Hi There, I',m</h2>
                    <h1>Dany Christianto</h1>
                    <h3>
                        <span ref={textRef}></span>
                    </h3>
                </div>

                <a href="#portfolio">
                    <img src={downarrow} alt="" />
                </a>
            </div>
        </div>
    );
}
/* src/components/intro/intro.scss */

@import "../../global.scss";


.intro {
    background-color: whitesmoke;
    display: flex;

    @include mobile{
        flex-direction: column;
        align-items: center;
    }

    .left{
        flex: 0.5;
        overflow-y: hidden;

        .imgContainer{
            width: 670px;
            height: 670px;
            background-color: whitesmoke;
            border-radius: 50%;
            display: flex;
            align-items: flex-end;
            justify-content: center;
            float: right;

            @include mobile {
                align-items: flex-start;
            }

            img{
                height: 90%;
                margin-bottom: 0%;
                filter: contrast(110%);

                @include mobile{
                    height: 50%;
                }
            }
        }
    }

    .right{
        flex: 0.5;
        display: flex;

        .wrapper{
            width: 100%;
            height: 100%;
            padding-left: 50px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            
            @include mobile {
                padding-left: 0;
                align-items: center;
            }
            h1 {
                font-size: 60px;
                margin: 10px 0;

                @include mobile {
                    font-size: 40px;
                }
            }
            h2 {
                font-size: 35px;
            }
            h3 {
                font-size: 30px;
            

                @include mobile {
                    font-size: 20px;
                }

                span{
                    font-size: inherit;
                    color: crimson;
                }

                .ityped-cursor {
                    animation: blink 1s infinite;
                }

                @keyframes blink {
                    50%{
                        opacity: 1;
                    }
                    100%{
                        opacity: 0;
                    }
                     
                }
            }

            .simple-cloud .tag-cloud-tag {
                flex-direction: column;
                justify-content: center;
            }
        }

        a {
            position: absolute;
            bottom: 10px;
            left: 60%;
      
            img {
              width: 30px;
              animation: arrowBlink 2s infinite;
            }
      
            @keyframes arrowBlink {
              100% {
                opacity: 0;
              }
            }
        }

        .typed {
            display: inline;
        }

    }
}

2. Portfolio

Didalam komponen portfolio ini kita ambil data list kategori portfolio dan data portfolio berdasarkan id kategori yang dipilih menggunakan django rest framework.

// src/components/portfolio/Portfolio.jsx

import PortfolioList from "../portfoliolist/PortfolioList"
import "./portfolio.scss"
import React, { useEffect, useState } from "react";
import axios from "axios";


export default function Portofolio() {
    const [selected, setSelected] = useState(1);
    const [data, setData] = useState([]);
    const [pfcates, setPfcates] = useState([]);
    
    useEffect(() => {
     
        // load data list portfolio dari DRF
        const fetchListCates = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const res = await axios.get(Config.API_URL +"/api/blog/portofolio_category/");
                setPfcates(res.data);
            }
            catch (err) {

            }
        }

        fetchListCates();
    }, []);


    useEffect(() => {
        const porto_cat_id = selected;

        const config = {
            headers: {
                'Content-Type': 'application/json'
            }
        };

        // load data portfolio berdasarkan list kategori yang dipilih
        const fetchData = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const res = await axios.post(Config.API_URL + `/api/blog/portofolios/`, { porto_cat_id }, config );
                setData(res.data);
            }
            catch (err) {

            }
        };

        fetchData();
    }, [selected]);

    return (
        <div className="portofolio" id="portfolio">
            <h1>Portfolio</h1>
            <ul>
                {pfcates.map((item) => (
                    <PortfolioList
                        key={item.id}
                        title={item.port_cat_name}
                        active={selected === item.id}
                        setSelected={setSelected}
                        id={item.id}
                    />
                ))}
            </ul>
            <div className="container">
                {data.map((d) => ( 
                    <div key={d.id} className="item">
                        <h3>{d.porto_title}</h3>
                        <img
                            src={d.porto_thumbnail}
                            alt=""
                        />
                    </div>
                ))}
            </div>
        </div>
    );
}
/* src/components/portfolio/portfolio.scss */

@import "../../global.scss";


.portofolio { 
    background-color: whitesmoke;
    display: flex;
    flex-direction: column;
    align-items: center;

    h1{
        font-size: 50px;

        @include mobile {
            font-size: 20px;
        }
    }

    ul{
        margin:10px;
        padding: 0;
        list-style: none;
        display: flex;

        @include mobile {
            margin: 10px 0;
            flex-wrap: wrap;
            justify-content: center;
        }

        li {
            font-size: 12px;
            margin-right: 5px;
            padding: 0.7;
            border-radius: 10px;
            cursor: pointer;

            &.active {
                background-color: $mainColor;
                color: white;
            }
        }
     
    }

    .container{
        width: 70%;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-wrap: wrap;

        @include mobile {
            width: 100%;
        }

        .item{
            width: 620px;
            height: 400px;
            border-radius: 20px;
            border: 1px solid rgb(240,239,239);
            margin: 10px 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            position: relative;
            transition: all .5s ease;
            cursor: pointer;

            h3{
                position: absolute;
                font-size: 12px;
            }

            img{
                width: 100%;
                height: 100%;
                object-fit: cover;
                z-index: 1;

            }

            &:hover{
                background-color: $mainColor;
                img {
                    opacity: 0.1;
                    z-index: 0;
                }

            }
        }
    }
}

// src/components/portfoliolist/Portfoliolist.jsx

import "./portfoliolist.scss"


export default function portfoliolist({ id, title, active, setSelected }) {
    return (
        <li key={id}
            className={active ? "portfoliolist active" : "portfoliolist"} 
            onClick={()=>setSelected(id)}
        >
            {title}
        </li>
    )
}
/* src/components/portfoliolist/portfolios.scss */

@import "../../global.scss";


.portfoliolist {
    font-size: 14px;
    margin-right: 50px;
    padding: 7px;
    border-radius: 10px;
    cursor: pointer;

    @include mobile {
        margin-right: 20px;
    }

    &.active{
        background-color: $mainColor;
        color: whitesmoke;
    }
}

3. Works

Komponen works ini untuk menampilkan daftar project yang pernah dikerjakan.

// src/components/works/Works.jsx

import { useState, useEffect } from 'react';
import "./works.scss";
import axios from "axios";


export default function Works() {
    const [currentSlide, setCurrentSlide] = useState(0)
    const [data, setData] = useState([])

    const regex = /(<([^>]+)>)/ig;
    
    // function untuk menghilangkan html tag dari data text
    const RemoveHtmlTag = (word) => {
        if (word)
            return word.replace(regex, '');
        return '';
    };


    useEffect(() => {
        const fetchData = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const fullrest = await axios.get(Config.API_URL + "/api/blog/jobs/");
                setData(fullrest.data);
            }
            catch (err) {

            }
        }
        fetchData();

    }, [])

    const handleClick = (way)=>{
        way === "left" 
        ? setCurrentSlide(currentSlide > 0 ? currentSlide - 1 : 2)
        : setCurrentSlide(currentSlide < data.length - 1 ? currentSlide + 1 : 0);
    }

    return (
        <div className="works" id="works">
          
            <div 
                className="slider" 
                style={ transform: `translateX(-${currentSlide * 100}vw)`}
            >
                {data.map((d) => (
                    <div className="container" key={d.id}>
                        <div className="item">
                            <div className="left">
                                <div className="leftContainer">
                                    <div className="imgContainer">
                                        <img src={d.job_icon} alt="" />
                                    </div>
                                    <h2>{d.job_name}</h2>
                                    
                                    <p>{RemoveHtmlTag(d.job_desc)}</p>
                                   
                                    <span>project</span>
                                </div>
                            </div>
                            <div className="right">
                                <img src={d.jobs_thumbnail} alt="" />
                            </div>
                            
                        </div>

                    </div>
                ))}
            </div>
            <img 
                src="assets/arrow.png" 
                className="arrow left" 
                alt="" 
                onClick={()=>handleClick("left")}
            />
            <img 
                src="assets/arrow.png" 
                className="arrow right" 
                alt=""
                onClick={()=>handleClick("right")}
            />
        </div>
    );
}
/* src/components/works/works.scss */

@import "../../global.scss";


.works {
    background-color: crimson;
    display: flex;
    align-items: center;
    justify-content: center;
  
    .arrow {
        height: 50px;
        position: absolute;

        @include mobile {
            display: none;
        }

        &.left {
            left: 100px;
            transform: rotateY(180deg);
        }

        &.right {
            right: 100px;
        }
    }

    .slider {
        height: 350px;
        display: flex;
        position: absolute;
        left: 0;
        transition: all 1s ease;

        @include mobile {
            height: 100vh;
            flex-direction: column;
            justify-content: center;
        }

        .container {
            width: 100vw;
            display: flex;
            align-items: center;
            justify-content: center;
            
            .item {
                width: 700px;
                height: 100%;
                background-color: white;
                border-radius: 20px;
                display: flex;
                align-items: center;
                justify-content: center;

                @include mobile {
                    width: 80%;
                    height: 150px;
                    margin: 15px 0;
                }
                

                .left{
                    flex: 4;
                    height: 80%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    
                    .leftContainer{
                        width: 90%;
                        height: 90%;
                        display: flex;
                        flex-direction: column;
                        justify-content: space-between;
                        
                        .imgContainer{
                            width: 40px;
                            height: 40px;
                            border-radius: 40px;
                            background-color: rgb(245, 179, 155);
                            display: flex;
                            align-items: center;
                            justify-content: center;

                            @include mobile {
                                width: 20px;
                                height: 20px;
                            }

                            img {
                                width: 25px;

                                @include mobile {
                                    font-size: 15px;
                                }
                            }
                        }

                        h2{
                            font-size: 20px;
                            padding-top: 10%;
                            overflow-y: hidden;

                            @include mobile {
                                font-size: 13px;
                            }
                        }

                        p{
                            font-size: 13px;
                            padding-top: 5%;
                            overflow-y: hidden;

                            @include mobile {
                                display: none;
                            }
                        }

                        span {
                            padding-top: 5%;
                            font-size: 12;
                            font-weight: 600;
                            text-decoration: underline;
                            cursor: pointer;
                            overflow-y: hidden;
                        }
                    }
                }

                .right {
                    flex: 8;
                    height: 100%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    overflow: hidden;

                    img {
                        width: 400px;
                        transform: rotate(-10deg);
                       
                    }
                }
            }
        }
    }
}

4. Testimonials

Komponen testimonials ini menampilkan data testimonial yang di input dari admin panel

// src/components/testimonials/testimonials.jsx

import './testimonial.scss'
import axios from 'axios';
import React, { useState, useEffect } from 'react';


export default function Testimonials() {
    const regex = /(<([^>]+)>)/ig;
    const RemoveHtmlTag = (word) => {
        if (word)
            return word.replace(regex, '');
        return '';
    };
    const [testimony, setTestimony] = useState([]);

    useEffect(() => {
        const fetchData = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const resp = await axios.get(Config.API_URL+"/api/blog/testimonial/");
                setTestimony(resp.data);
            }
            catch (err) {

            }
        }
        fetchData();
    }, []);


    return (
        <div className="testimonials" id="testimonials">
            <h1>Testimonials</h1>
            <div className="container">
                {testimony.map((d) => ( 
                    <div key={d.id} className={d.testimonial_featured ? "card featured" : "card"}>
                        <div className="top">
                            <img src="assets/right-arrow.png" className="left" alt=""/>
                            <img className="user"
                                src={d.testimonial_img}
                                alt="" 
                            />
                            <img className="right" src={d.testimonial_icon} alt="" />
                        </div>
                        <div className="center">
                            {RemoveHtmlTag(d.testimonial_desc)}
                        </div>
                        <div className="bottom">
                            <h3>{d.testimonial_name}</h3>
                            <h3>{d.testimonial_title}</h3>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    )
}
/* src/components/testimonials/testimonials.scss */

@import "../../global.scss";

.testimonials{
    background-color: whitesmoke ;
    display: flex;
    flex-direction: column;
    align-items: center;

    @include mobile{
        justify-content: space-around;
    }   
    
    h1{
        font-size: 50px;
        overflow: hidden;
        margin-bottom: 20px;

        @include mobile {
            font-size: 20px;
        }
    }

    .container {
        width: 100%;
        height: 80%;
        display: flex;
        align-items: center;
        justify-content: center;

        @include mobile {
            flex-direction: column;
            height: 100%;
        }

        .card {
            width: 250px;
            height: 70%;
            border-radius: 10px;
            box-shadow: 0px 0px 15px -8px black;
            display:  flex;
            flex-direction: column;
            justify-content: space-around;
            padding: 20px;
            transition: all 1s ease;

            @include mobile {
                height: 180px;
                margin: 10px 0;
            }

            &.featured {
                width: 300px;
                height: 75%;
                margin: 0 30px;

                @include mobile {
                    width: 250px;
                    height: 180px;
                    margin: 1px;
                }

            }

            &:hover {
                transform: scale(1.1);
            }

            .top {
                display: flex;
                align-items: center;
                justify-content: center;
    
                img {
                    &.left,
                    &.right {
                        height: 25px;
                    }

                    &.user {
                        height: 60px;
                        width: 60px;
                        border-radius: 50%;
                        object-fit: cover;
                        margin: 0 30px;

                        @include mobile{
                            width: 30px;
                            height: 30px;
                        }
                    
                    }
                }
            }
    
            .center {
                padding: 10px;
                border-radius: 10px;
                background-color: rgb(250, 244, 245);

                @include mobile{
                    font-size: 12px;
                    padding: 5px;
                }
        
            }
              
            .bottom {
                display: flex;
                align-items: center;
                flex-direction: column;
                justify-content: center;
        
                h3 {
                  margin-bottom: 5px;


                  @include mobile{
                    font-size: 14px;
                  }
                }
            
                h4{
                    color: rgb(121, 115, 115);

                    @include mobile{
                        font-size: 13px;
                    }
                }
            }
        }
    }
}

5. Contact Form

Seperti contact form pada umumnya, contact form ini berfungsi sebagai user interface untuk pengguna mengirimkan pesan ke admin web. contact form ini dilengkapi dengan fitur send email ke email admin sebagai notifikasi adanya pesan masuk dari web.

// src/components/contact/Contact.jsx

import "./contact.scss";
import React, { useState, useEffect } from "react";
import axios from 'axios';
import { Oval } from 'react-loader-spinner';
import { FormGroup } from '@mui/material';
import { Input } from '@mui/material';
import TextField from '@mui/material/TextField'
import toast, { Toaster } from 'react-hot-toast';
import mailcontact_img from '../assets/mailcontact.png'


export default function Contact () {
    useEffect(() => {
        window.scrollTo(0, 0);
    }, []);
   
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        message: ''
    });

    const { name, email, message } = formData;
    const [loading, setLoading] = useState(false);
    const onChange = e => setFormData(
        { ...formData, [e.target.name]: e.target.value }
    );
    const onSubmit = async (e) => {
        const { default: Config } = require("../../utils/config")        
        e.preventDefault();

        try {
            const config = {
                headers: {
                    'Content-Type': 'application/json'
                }
            };
                
            setLoading(true);
            axios.post(Config.API_URL + `/api/blog/contact/`, {name, email, message }, config)
            .then(res => {

                if (res.status === 200) {
                    setFormData({
                        name: '',
                        email: '',
                        message: ''
                    });

                    toast.success('Thank You, your message was sent', {
                    
                        // Styling
                        className: 'Toast-Message',
                        icon: '',
                    })
                }
                setLoading(false);
                window.scrollTo(0, 0);
            })
            .catch(err => {
                toast.success('Error sending message', {
                    // Styling
                    className: 'Toast-Message',
                        icon: '',
                })
              
                setLoading(false);
                window.scrollTo(0, 0);
            })

        } 
        catch (err) {
            console.log(err);
        }
    };

    return (
        <div className="contact" id="contact">
            <div className="left">
                <div className="Container">
                    <img src={mailcontact_img} alt="" />
                </div>
            </div>
            <div className="right">
                <h2>Contact</h2><br/>
                <form className="contact__form" id="contact-form" onSubmit={e => onSubmit(e)}>
                    <FormGroup sx=> 
                        <Input 
                            className='contact__form__input' 
                            name='name' 
                            type='text' 
                            placeholder='Full Name' 
                            onChange={e => onChange(e)} 
                            value={name} 
                            required 
                        />
                        <br/>
                        <Input 
                            className='contact__form__input' 
                            name='email' 
                            type='email' 
                            placeholder='example@mail.com' 
                            onChange={e => onChange(e)} 
                            value={email} 
                            required 
                        />
                        <br/>
                        <TextField
                            className='contact__form__textarea'
                            name='message'
                            multiline
                            rows={10}
                            placeholder='Message'
                            onChange={e => onChange(e)} 
                            value={message} 
                        />
                    </FormGroup>
                    <br/>
                 
                    <button 
                        className='contact__form__button' 
                        htmltype='submit'
                        disabled={loading}
                        
                    >
                        {loading ? 
                            <div className="loader-message">
                                <Oval 
                                    color={'#424242'}
                                    height={20} 
                                    width={20}
                                /> Sending...
                            </div> 
                            : 'Send'
                        }
                    </button>
                </form>
            </div>

            <Toaster
                position="top-right" 
                reverseOrder={false} 
            />
        </div>
    );
};
/* src/components/contact/contact.scss */

@import "../../global.scss";

.contact {
    background-color: whitesmoke;
    display: flex;

    @include mobile {
        flex-direction: column;
    }

    .left{
        flex:1;
        overflow: hidden;
        flex-direction: column;

        .Container{
            width: 700px;
            height: 650px;
            border-radius: 30%;
            display: flex;
            align-items: flex-end;
            justify-content: center;
            float: right;
                    
            img {
                height: 100%;
            }
        }
    }

    .right {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    
        h2 {
            font-size: 30px;
            overflow: hidden;
            height: 15%;
        }

        .contact__form__button {
            outline: none;
            border: none;
            cursor: default;
            transition: box-shadow 300ms ease;
            letter-spacing: 2px;
            box-shadow: 0px 0px $btnHeight/2 20px $shadow;
            user-select: none;
  
            position: relative;
            box-sizing: border-box;
            width: $btnWidth;
            height: $btnHeight;
            border-radius: $borderRadius;
            border: $btnBorder solid;
            border-color: $accentColor;
            background: none;
            color: $accentColor;
            transition: all 300ms ease-out;
            
            &:hover {
                cursor: pointer;
                font-size: 1.05em;
                border-color: transparent;
                background: $accentColor;
                color: $bgColor;
            }

            .loader-message {
                font-size: 0.9em;
                display: flex;
                align-items: center;
                justify-content: center;
                
            } 
        }
    }
}

Blog Web Component

Komponen Blog web ini berfungsi sebagai halaman web blog.

// src/components/blog/blog.jsx

import React, { useState, useEffect } from 'react';
import { Route } from 'react-router-dom';
import axios from 'axios';
import PropTypes from 'prop-types';

import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Container from '@mui/material/Container';

import Blog from './blog/Blog'
import Category from './blog/Category';
import BlogDetail from './blog/BlogDetail';
import Header from './blog/Header';
import Footer from './blog/Footer';


const BlogWebComponent = (props) => {
    const [sectionBlog, setSectionBlog] = useState([]);

    useEffect(() => {
        const fetchSection = async () => {
            const { default: Config } = require("../utils/config")
            try {
                const res = await axios.get(Config.API_URL + "/api/blog/list_category/");
                setSectionBlog(res.data)
            }
            catch (err) {

            }
        }
        fetchSection();
    }, []);
    
    const theme = createTheme();

    if(props.HideBlog) {
        return null;
    }
    else {
        return (
            <div className="blog">
                <ThemeProvider theme={theme}>
                    <CssBaseline />
                    <Container maxWidth="lg" sx=>
                        <Header title="Blog" sections={sectionBlog}/>
                            
                        <Route exact path='/blog' component={Blog}/>
                        <Route exact path='/blog/category/:cat_title_id' component={Category}/>
                        <Route exact path='/blog/blogdetail/:id' component={BlogDetail}/>
                    </Container>
                    <Footer
                        title="Footer"
                        description="Something here to give the footer a purpose!"
                    />
                </ThemeProvider>          
            </div>
        );
    }
 
}

BlogWebComponent.propTypes = {
    HideBlog: PropTypes.bool
}

BlogWebComponent.defaultProps = {
    HideBlog: true
}

export default BlogWebComponent;

Komponen Web Blog terdiri dari beberapa komponen berikut :

1. Header

Komponen header ini menampilkan menu katergori.

// src/components/blog/Header.jsx

import React from 'react';
import PropTypes from 'prop-types';
import Toolbar from '@mui/material/Toolbar';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';

import { useHistory } from "react-router-dom";
import IconButton from '@mui/material/IconButton';
import SvgIcon from '@mui/material/SvgIcon';
import "./header.scss";


function HomeIcon(props) {
  return (
    <SvgIcon {...props}>
      <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
    </SvgIcon>
  );
}


const Header = (props) => {
    const { sections } = props;
    
    const history = useHistory();
    const handleClickBlogHome = () => {
        history.push('/blog')
    };

    const CapitalizeFirstLetter = (word) => {
        if (word) 
            return word.charAt(0).toUpperCase() + word.slice(1);
        return '';
    }; 
  
    return (
        <React.Fragment>
            <Box sx=>
                <Toolbar
                    component="nav"
                    variant="dense"
                    sx = 
                >
                    <IconButton
                        size="small"
                        color="inherit"
                        aria-label="menu"
                        onClick={handleClickBlogHome}
                    >
                        <HomeIcon />
                    </IconButton>
                    
                        {sections.map((section) => (
                            <Link
                                underline="none" 
                                color="inherit"
                                noWrap
                                key={section.id}
                                variant="body2"
                                href={`/blog/category/${section.id}`}
                                sx=
                                
                            >
                                {CapitalizeFirstLetter(section.cat_title)}
                            </Link>

                            
                        ))}
                    
                </Toolbar>
            </Box>
        </React.Fragment>
    );
}
  
Header.propTypes = {
    sections: PropTypes.arrayOf(
      PropTypes.shape({
        cat_title: PropTypes.string.isRequired,
        id: PropTypes.number.isRequired,
      }),
    ).isRequired,
    title: PropTypes.string.isRequired,
};
  
export default Header;
/* src/components/blog/header.scss */

@import "../../global.scss";


.MenuLink {
    color: inherits;
    &.clicked {
        cursor: default;
        pointer-events: none;        
        text-decoration: none;
        color: gey;
    }
}

2. Blog

Komponen Blog ini berfungsi untuk mengambil data blog post kemudian di tampilkan menggunakan komponen Main.

// src/components/blog/Blog.jsx

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Main from './Main';


export default function Blog() {
    const [blogs, setBlogs] = useState([]);

    useEffect(() => {
        const fetchBlogs = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const res = await axios.get(Config.API_URL + "/api/blog/");
                setBlogs(res.data);
            }
            catch (err) {

            }
        }
        fetchBlogs();
    }, []);

    return (
        <Main posts={blogs}/>
    );
}
// src/components/blog/Main.jsx

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Grid from '@mui/material/Grid';
import FeaturedPost from './FeaturedPost';
import MainFeaturedPost from './MainFeaturedPost';

export default function Main(props) {
    const { posts } = props;
    const [featuredBlog, setFeaturedBlog] = useState([]);

    const capitalizeFirstLetter = (word) => {
        if (word)
            return word.charAt(0).toUpperCase() + word.slice(1);
        return '';
    };

    useEffect(() => {
        const fetchData = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const res = await axios.get(Config.API_URL + `/api/blog/featured`);
                setFeaturedBlog(res.data[0]);
            }
            catch (err) {

            }
        }
        fetchData();
    }, []);

    return (
        <main>
            <MainFeaturedPost post={featuredBlog}/>
            <Grid container spacing={3}>
                {posts.map(post => (
                    <FeaturedPost key={capitalizeFirstLetter(post.title)} post={post} />
                ))}
            </Grid>

        </main>
        
      
    );
}

Didalam komponen Main terdapat komponen MainFeaturedPost yang berfungsi menampilkan post featured

// src/components/blog/MainFeaturedPost.jsx

import * as React from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Grid';
import Link from '@mui/material/Link';
import Box from '@mui/material/Box';


function MainFeaturedPost(props) {
    const { post } = props;

    return (
        <Paper
            sx={
            position: 'relative',
            backgroundColor: 'grey.800',
            color: '#fff',
            mb: 4,
            backgroundSize: 'cover',
            backgroundRepeat: 'no-repeat',
            backgroundPosition: 'center',
            backgroundImage: `url($)`,
            }
        >   
            {/* Increase the priority of the hero background image */}
            {<img style= src={post.thumbnail} alt={post.imageText} />}
            
            <Box
                sx={
                    position: 'absolute',
                    top: 0,
                    bottom: 0,
                    right: 0,
                    left: 0,
                    backgroundColor: 'rgba(0,0,0,.3)',
                }
            />
            <Grid container>
                <Grid item md={6}>
                    <Box
                        sx={
                            position: 'relative',
                            p: { xs: 3, md: 6},
                            pr: { md: 0},
                        }
                    >
                        <Typography component="h1" variant="h3" color="inherit" overflow="hidden" gutterBottom>
                            {post.title}
                        </Typography>
                        <Typography variant="h5" color="inherit" paragraph>
                            {post.excerpt}
                        </Typography>
                        <Link variant="subtitle1" href={`/blog/blogdetail/${post.slug}`}>
                            Continue reading...
                        </Link>
                    </Box>
                </Grid>
            </Grid>
        </Paper>
    );
}

export default MainFeaturedPost;

3. Blog Detail

Komponent detail berfungsi menampilkan detail post artikel.

// src/components/blog/BlogDetail.jsx

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import dateFormat from 'dateformat';
import axios from 'axios';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';


const BlogDetail = (props) => {
    const [PostDetail, setPostDetail] = useState({});

    useEffect(() => {
        const slug = props.match.params.id;
        
        const fetchData = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const res = await axios.get(Config.API_URL + `/api/blog/${slug}`);
                setPostDetail(res.data);
            }
            catch (err) {
                
            }
            
        };
        fetchData();
    }, [props.match.params.id]);

    const createBlog = () => {
        return {__html: PostDetail.content}
    };

    const CapitalizeFirstLetter = (word) => {
        if (word)
            return word.charAt(0).toUpperCase() + word.slice(1);
        return '';
    };

    return (
        <Box mt={4}>
            <Typography variant="h4">
                {CapitalizeFirstLetter(PostDetail.title)}
            </Typography>
            <Typography variant="subtitle1">
                {dateFormat(PostDetail.date_created, "mmm dS, yyyy")}
            </Typography>
            <Typography mt={3} paragraph gutterBottom>
                <span align='justify' dangerouslySetInnerHTML={createBlog()}/>
            </Typography>

            <Link to='/blog' color="primary" underline="none">Back to Blog</Link>
        </Box>
    );

};

export default BlogDetail;

4. Category

Komponen ini menampilkan artikel (post) berdasarkan kategori yang di pilih di menu kategori

// src/components/blog/Category.jsx

import React, { useState, useEffect} from 'react'

import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import { CardActionArea, CardActions } from '@mui/material';
import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';

import axios from 'axios';


const Category = (props) => {
    const [blogs, setBlogs] = useState([]);
    
    useEffect(() => {
        const cat_title_id = props.match.params.cat_title_id;
    
        const cfg = {
            headers: {
                'Content-Type': 'application/json'
            }
        };

        const fetchData = async () => {
            const { default: Config } = require("../../utils/config")
            try {
                const res = await axios.post(Config.API_URL + "/api/blog/category", { cat_title_id }, cfg);
                setBlogs(res.data);

            }
            catch (err) {

            }
        };
        fetchData();
    }, [props.match.params.cat_title_id]);

    const CapitalizeFirstLetter = (word) => {
        if (word)
            return word.charAt(0).toUpperCase() + word.slice(1);
        return '';
    };

    return (
        <Grid container spacing={3}>
            {blogs.map(post => (
                <Grid item xs={12} md={6} key={post.id}>
                    <CardActionArea>
                        <Card sx=>
                            <CardContent sx= >
                                <Typography variant="subtitle1" color="text.secondary">
                                    <Link href={`/blog/category/${post.cat_title_id}`} color="primary" underline="hover">{post.cat_title}</Link>
                                </Typography>
                                <Typography component="h2" variant="h5">
                                    {CapitalizeFirstLetter(post.title)}
                                </Typography>
                                <Typography variant="subtitle1" color="text.secondary">
                                    {post.month} {post.day}
                                </Typography>
                                <Typography variant="subtitle1" paragraph>
                                    {post.excerpt}
                                </Typography>
                                        
                                <CardActions>
                                    <Link href={`/blog/blogdetail/${post.slug}`} color="primary" underline="none">Continue reading...</Link>
                                </CardActions>
                            </CardContent>   
                            <CardMedia
                                component="img"
                                sx={ width: 200, heigth: 250, display: { xs: 'none', sm: 'block' } }
                                image= {post.thumbnail} 
                                alt={post.slug}
                            />
                        </Card>
                    </CardActionArea>
                </Grid>
            ))}
        </Grid>
    )
};

export default Category;

Setelah semua komponen selesai dibuat, selanjutnya lakukan build npm run build dari folder frontend dan copy folder hasil build ke dalam directory backend/nama aplikasi. setelah itu jalankan django dan yea.. aplikasi web portfolio dan blog dengan reactjs sudah bisa tampil di django service.