How to make a Kali Linux themed Terminal Website Portfolio using React JS

Hello Beautiful People,

You must have known about Kali Linux and most probably have used it. If not no problem then, we are going to create our own Kali Linux (oops, Sorry... Our Kali Linux themed portfolio.

For reference you people can see these terminal themed portfolio websites:

1. Heber Leonard's Portfolio

2. Lokesh Bachhu's Portfolio

3. ShellFolio

One day I was just chilling about it and somehow found a website in the theme of terminal. Then I thought why not make something like that? A terminal themed portfolio it isn't that bad...


Prerequisites

Previous knowloedge of React + JavaScript

No knowledge? No problem😥 Learn by building things just like I do...


Let's Begin

What else we will add?

We will make our own linux like login screen and will make a custom loader. Cool right? Let's start Coding.

Kali Linux Themed Portfolio Website in ReactJS - MrDevKnown


Create a React App

Go to your desired folder, open terminal and run this command to create a react app in that directory.


npx create-react-app ./

After the app gets created,

Consider the folder structure we are going to use in this project...

folder structure

Create the folders which are not available in your default app folder according to this structure.


After making the following file structure we are ready to start working with our Application.


Packages

Packages we are going to use in this project are as:

  react-icons: v4.7.1
  react-router-dom: v6.6.1

Run the below command to install all the packages, if you want to explore about these packages, click on the names to go to their respective pages.

npm i react-icons react-router-dom


Components

Now we are going to work with our components. Create these files in your components folder with the same names to use in your app.

There are total 7 components I've made.

1. TerminalLayout.js

This file contains the layout of our terminal.

Paste the below code into this file


import React from 'react'
import Contact from './Contact'
import Terminal from './Terminal'

export default function TerminalLayout(props) {
    return (
        <>
            <div className='kali_desktop'>
                <div className='kali_terminal'>

                    <Terminal loginname={props.loginname} />

                </div>

                <Contact />
                
            </div>
        </>
    )
}

2. Terminal.js

This file is our actual terminal


import React, { useState } from 'react'
import TerminalData from './TerminalData';

export default function Terminal(props) {

    let logo = `
       _______      __            __                ____                  _______        __            ___
     /    _____|     |     |____|     |            /          \\             /    _____|      |     |____|      |
   /    /_____       |     ____       |          /    /_\\    \\          /    /_____       |     _____      |
   \\_____      |     |     |        |      |        /    ___      \\         \\_____     |      |     |         |     |
    _____/    /      |     |        |      |_     |    |       |     |_      _____/    /       |     |         |     |
    \\______/        |__ |        |____|    |__|       |____|    \\______/         |__ |         |___|
    
    `

    const [history, setHistory] = useState([])
    const [command, setCommand] = useState('')
    const [dirname, setdirname] = useState('')

    const dirs_list = ["home", "about"]

    const handleAdd = () => {
        const newList = [...history];
        newList.push(command);
        setHistory(newList);
    }

    const handleKeyDown = event => {
        // what if ctrl + c is pressed break the command
        var key = event.which || event.keyCode;
        var ctrl = event.ctrlKey ? event.ctrlKey : ((key === 17)
            ? true : false);
        if (key == 67 && ctrl) {
            event.key = "Enter"
        }

        if (event.key === 'Enter') {
            // Enter key pressed ✅

            handleAdd()
            setCommand('')
            // console.log(history)


            // hide the logo terminal
            if (command === "hide") {
                document.querySelector(".terminal_logo").style.display = "none";
            }
            // show the logo terminal
            if (command === "show") {
                document.querySelector(".terminal_logo").style.display = "block";
            }

            // clear the terminal
            if (command === "clear") {
                setHistory([])
            }

            const contact = document.querySelector(".contact_container")
            if (command.substring(0, 5) === "mkmsg") {
                contact.style.display = "block"
            }

            if (command.substring(0, 2) === "cd") {
                if (command.includes("..")) {
                    setdirname('')
                } else {
                    if (dirs_list.includes(command.slice(3))) {
                        setdirname(command.slice(3))
                    }
                }
            }
        }
    };




    return (
        <>
            <div className='terminal_logo'>
                <pre>{logo}</pre>
                <div className='instructions'>
                    <ol type='number'>
                        <li>Type 'hide/show' to hide or show these instructions.</li>
                        <li>Type 'clear' to clear the terminal.</li>
                        <li>Type '-help' to get help instructions of the terminal.</li>
                        <li>Type 'whoishe' to know about me.</li>
                    </ol>
                </div>

                <br />
            </div>
            <div className='terminal_container'>

                <TerminalData loginname={props.loginname} array={history} dirname={dirname} />

                <div className='terminal_area'>
                    <div className='input_area'>
                        <div className='terminal_name'>
                            <font style={{ color: "lightgreen" }}>(</font>
                            {props.loginname}@shash
                            <font style={{ color: "lightgreen" }}>)
                                -[
                                <font style={{ color: "white" }}>~{dirname && <b>/{dirname}</b>}</font>]
                            </font>
                        </div>
                        <div className='input_box'>
                            $ <input
                                id='terminal_input'
                                value={command}
                                onKeyDown={handleKeyDown}
                                onBlur={({ target }) => target.focus()}
                                onChange={(e) => setCommand(e.target.value)}
                                type="text" autoFocus={true} />
                        </div>
                    </div>
                </div>
            </div>
        </>
    )
}


3. TerminalResult.js

This file gives the result of the terminal (what user inputs)


import React from 'react'
import parse from "html-react-parser";

export default function TerminalResult(props) {

    const projects = "https://mrdevknown.blogspot.com"

    const dirs_list = ["home", "about"]

    const keywords = [
        {
            "name": "help",
            "abbr": ["-help", "-h", "-H", "-Help", "-HELP", "--help"],
            "text": `
                    <h4>Shash Help</h4> 
                    <br/>
                    <p/>Shash is a linux terminal themed website.
                    <p/>This website is made just for fun.
                    <br/><br/>
                    Commands:
                    <br/><br/>
                    <div className="command_desc">
                        <div className="command_name">-h, -help, --help</div>
                        <div className="command_details">Get Help</div>
                    </div>
                    <div className="command_desc">
                        <div className="command_name">cd</div>
                        <div className="command_details">To get inside a directory</div>
                    </div>
                    <div className="command_desc">
                        <div className="command_name">mkmsg</div>
                        <div className="command_details">Make me a new message</div>
                    </div>
                    <div className="command_desc">
                        <div className="command_name">dir</div>
                        <div className="command_details">Show the directories</div>
                    </div>
                    `
        },
        {
            "name": "cd",
            "abbr": ["cd"]
        },
        {
            "name": "whoishe",
            "abbr": ["whoishe"],
            "text": `Hello,
                    <p/>I am <font className="primarybg">Shivesh</font>
                    <br/><br/>
                    <ul style="margin-left: 20px">
                        <li type="square">I started to code when I was 11.</li>
                    </ul>
                    <br/>
                    <ul style="margin-left: 20px">
                        <li type="square">I love to make <a href=${projects} target="_blank">stupid things</a> like this.</li>
                    </ul>`
        },
        {
            "name": "dir",
            "abbr": ["dir"],
            "text": `Directories:
                    <br/><br/>
                    <div className="dirs_name">
                        <div>home</div>
                        <div>about</div>
                    </div>
                    `
        },
        {
            "name": "mkmsg",
            "abbr": ["mkmsg"],
            "text": `send a message`
        },
        {
            "name": "hide",
            "abbr": ["hide"],
            "text": `hidden`
        },
        {
            "name": "show",
            "abbr": ["show"],
            "text": `showing`
        },
        {
            "name": "clear",
            "abbr": ["clear"],
            "text": `clear`
        }
    ]

    let dirname = ""
    let text = "Command Not Found";

    for (let i = 0; i < keywords.length; i++) {
        const keywordResult = (item) => {
            for (let j = 0; j < keywords[i].abbr.length; j++) {
                if (item === `${keywords[i].abbr[j]}`) {
                    if (keywords[i].name === "help") {
                        text = `${keywords[i].text}<br/><br/>`
                    }
                    if (keywords[i].name === "dir") {
                        text = `${keywords[i].text}<br/><br/>`
                    }
                    if (keywords[i].name === "mkmsg") {
                        text = `${keywords[i].text}<br/><br/>`
                    }
                    if (keywords[i].name === "hide") {
                        text = `${keywords[i].text}<br/><br/>`
                    }
                    if (keywords[i].name === "show") {
                        text = `${keywords[i].text}<br/><br/>`
                    }
                    if (keywords[i].name === "whoishe") {
                        text = `${keywords[i].text}<br/><br/>`
                    }
                }
            } if (props.command.substring(0, 2) === "cd") {
                dirname = props.command.slice(3)
                if (dirs_list.includes(dirname) || dirname === "..") {
                    text = ""
                } else {
                    text = "No Such Directory"
                }
            }
        }

        keywordResult(props.command)
    }

    return (
        <>
            <div className='terminal_result'>
                {parse(text)}
            </div>
        </>
    )
}

4. TerminalData.js

This file stores the terminal data


import React from 'react'
import TerminalResult from './TerminalResult';

export default function TerminalData(props) {
    let dirname = ''
    return (
        <>{props.array.map((com) => {
    
            if (com.substring(0,2) === "cd") {
                dirname = com.slice(3)
                if (dirname === "..") {
                    dirname = ""
                }
            }

            return (
                <div className='terminal_area'>
                    <div className='input_area'>
                        <div className='terminal_name'>
                            <font style={{ color: "lightgreen" }}>(</font>
                            {props.loginname}@shash
                            <font style={{ color: "lightgreen" }}>)
                                -[
                                <font style={{ color: "white" }}>~{dirname && <b>/{dirname}</b>}</font>]
                            </font>
                        </div>
                        <div className='input_box'>
                            $ {com}
                        </div>
                    </div>

                    <TerminalResult command={com} />

                </div>
            );
        })}
        </>
    )
}


Now our  Terminal is Ready.

But I would like to add a few more things...

Here comes our 5th component

5. Contact.js

This file will display a popup window when user types mkmsg command, it will open and display our contact links.

Paste the below code into this file

import React from 'react'
import { IoIosClose, IoIosMail, IoIosSquareOutline, IoLogoGithub, IoLogoLinkedin } from 'react-icons/io';
import { BiMessageAltDetail, BiMinus } from 'react-icons/bi';

export default function Contact() {

    const logo = `
                 .--------\\ /--------.
                 |            -            |
                 |                         |
                 |                         |
      _____|____________|_____
       =========.==========
             / ~~~~~     ~~~~~ \\
           /|                |                |\\
           W   ------  / \\  --------   W
           \\.             |o o|             ./
            |                                    |
             \\    #########   /
               \\  ## --------- ##  /
                 \\##              ##/
                   \\_____v____/
    `

    var isMaximised = false;

    const maximize = () => {
        if (!isMaximised) {
            document.querySelector(".contact_container").classList.add("fullscreen")

            isMaximised = true
        } else {
            document.querySelector(".contact_container").classList.remove("fullscreen")

            isMaximised = false
        }
    }

    const hide = () => {
        document.querySelector(".contact_container").classList.remove("fullscreen")
        document.querySelector(".contact_container").style.display = "none"
    }

    return (
        <div className='contact_container'>
            <div className='title_bar'>
                <div className='title'>
                    <BiMessageAltDetail />
                    Contact Me
                </div>
                <div className='buttons'>
                    <div className='button' onClick={hide}><BiMinus style={{ fontSize: "10px" }} /></div>
                    <div className='button' onClick={maximize}><IoIosSquareOutline style={{ fontSize: "10px" }} /></div>
                    <div className='button' onClick={hide}><IoIosClose /></div>
                </div>
            </div>
            <div className='contact_window'>

                <pre>{logo}</pre>

                <div className="container">
                    <h1>Contact Me</h1>
                    <div className='contact_links'>
                        <a href='https://mail.google.com/mail/?view=cm&to=YOUR_EMAIL' target='_blank' className='link'>
                            <IoIosMail />
                            "Your Email"
                        </a>
                        <a href='gmail.com' className='link'>
                            <IoLogoGithub />
                            TheShiveshNetwork
                        </a>
                        <a href='gmail.com' className='link'>
                            <IoLogoLinkedin />
                            WhoIsShivesh
                        </a>
                    </div>
                </div>
            </div>
        </div>
    )
}
6. Login.js

One more thing I would add is a login page, where user will enter their name or they can just enter as guest and we will display their entered name on the terminal. This is a little extra thing but I feel this adds to our portfolio and makes it a little more interactive.

import React from 'react'
import logo from '../kali-logo.png'
import { Link } from "react-router-dom";

export default function Login(props) {
    return (
        <>
            <div className='login_container'>
                <div className='login_box'>
                    <div className='login_area'>
                        {/* <h1>Login</h1> */}
                        <div className='logo_image'>
                            <img src={logo} alt="logo" />
                        </div>
                        <div className='input_login'>
                            Enter your name here
                            <input value={props.name} onChange={(e) => props.setName(e.target.value)} type="text" autoFocus={true} />
                        </div>
                    </div>
                    <div className='login_btns'>
                        <Link to="/" className='secondaryBtn' onClick={props.guestName}>Guest</Link>
                        <Link to="/" className='primaryBtn disabled-link' onClick={props.submitName}>Log In</Link>
                    </div>
                </div>
            </div>
        </>
    )
}



Now we are done with all our components. 

Now let's use all these components into our app.

App.js

import './App.css';
import Login from './components/Login';
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import TerminalLayout from './components/TerminalLayout';
import { useState } from 'react';
import Loader from './components/Loader';

function App() {
  const [name, setName] = useState("")
  const [loggedIn, setLoggedIn] = useState(false)
  // let loggedIn = false

  if (name !== "") { document.querySelector(".primaryBtn").classList.remove("disabled-link") }

  const submitName = () => {
    setName(name)
    // loggedIn = true;
    if (name !== "") { setLoggedIn(true); }
    // console.log(loggedIn)
  }

  const guestName = () => {
    setName("guest")
    setLoggedIn(true);
  }
  
  return (
    <>
          <BrowserRouter>
            <Routes>
              <Route path="/" element={loggedIn ? <TerminalLayout loginname={name} /> : <Navigate to="/login" />} />
              <Route path='*' element={<Navigate to="/" />} />
              <Route path="/login" element={<Login name={name} setName={setName} guestName={guestName} submitName={submitName} />} />
            </Routes>
          </BrowserRouter>

          <Loader />
    </>
  );
}

export default App;

  
  

With this app.js file, we are done with our project. Now start the app by running "npm start" command in the terminal.

npm start

The Output is going to look like this:



But the design will definitely look ugly since we haven't added any styles. So let's do so.

I am going to modify the index.css file.

Paste the below code to get the output like me, it's just simple styling so anyone can easily go through it and understand it.

Index.css

.kali_desktop {
  min-height: 100vh;
  width: 100vw;
  background: url('../public/assets/wallpaper3.jpg');
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
  background-attachment: fixed;
}

.kali_terminal {
  background: black;
  min-height: 100vh;
  width: 100%;
  opacity: 70%;
  font-family: 'Ubuntu', sans-serif;
  color: white;
}

.terminal_container {
  padding: 25px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

a {
  color: #fff;
}

.input_area {
  display: flex;
  /* gap: 8px; */
  flex-direction: column;
  letter-spacing: 2px;
}

.input_area::after {
  z-index: -1;
  content: '';
  position: absolute;
  /* background: red; */
  border-left: 2px solid lightgreen;
  border-top: 2px solid lightgreen;
  border-bottom: 2px solid lightgreen;
  padding: 0;
  height: 16px;
  width: 50px;
  transform: translate(-15px, 10px);
}

.kali_terminal input {
  width: 80%;
  height: auto;
  background: none;
  border: none;
  color: white;
  font-size: 16px;
  letter-spacing: 2px;
  font-family: 'Ubuntu', sans-serif;
}

input:focus {
  outline: none;
}

.input_box {
  background: black;
  width: 100%;
  padding-left: 8px;
}

.terminal_name {
  background: black;
  margin-left: 15px;
  padding-left: 5px;
  color: royalblue;
}

.dirs_name {
  display: grid;
  width: 350px;
  grid-template-columns: auto auto auto;
  gap: 25px;
}

pre {
  font-family: 'Ubuntu', sans-serif;
}

.command_desc {
  display: flex;
  gap: 105px;
  padding-bottom: 10px;
}

.command_name {
  width: 70px;
  font-style: italic;
  color: orange;
}

.terminal_result {
  margin: 10px 0 10px -10px;
}

.terminal_logo {
  padding: 10px;
}

.instructions {
  width: 100%;
  margin: 10px 20px 0 20px;
  font-size: 16px;
}

.login_container {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: url('../public/assets/wallpaper1.jpg');
  background-position: center;
  background-size: cover;
}

.login_box {
  backdrop-filter: blur(5px);
  color: white;
  height: 230px;
  width: 440px;
  border-radius: 5px;
  /* padding: 20px; */
  display: flex;
  flex-direction: column;
  align-items: center;
}

.login_area {
  font-size: 18px;
  width: 100%;
  height: 70%;
  gap: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0a0a0bed;
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
}

.input_login {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.input_login input {
  width: 250px;
  font-size: 16px;
}

.login_btns {
  width: calc(100% - 80px);
  height: 30%;
  padding: 0 40px 0 40px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #000000;
  border-bottom-left-radius: 5px;
  border-bottom-right-radius: 5px;
}

.login_area .logo_image {
  height: 100px;
  width: 100px;
  background: #071a54;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.logo_image img {
  height: 80%;
}

.primaryBtn,
.secondaryBtn {
  border: none;
  padding: 5px 15px 5px 15px;
  border-radius: 2px;
  cursor: pointer;
  font-size: 14px;
  text-decoration: none;
}

.primarybg {
  background: #071a54;
  padding: 2px 5px 2px 5px;
  border-radius: 2px;
}

.terminal_result li:before {
  content: '\26A1';
  margin-left: -20px;
  margin-right: 10px;
}

.primaryBtn:hover,
.secondaryBtn:hover {
  text-decoration: underline;
}

.disabled-link {
  pointer-events: none;
  background: #829ff7;
}

.primaryBtn {
  background: #28408b;
  color: white;
}

.secondaryBtn {
  background: #acacac;
  color: black;
}

.contact_container {
  display: none;
  position: fixed;
  top: 50%;
  right: 20%;
  transform: translate(0, -50%);
  width: 600px;
  background: rgba(10, 10, 10, 0.9);
  backdrop-filter: blur(1px);
  border-radius: 10px;
  box-shadow: 0 0 20px rgb(8, 8, 8);
  /* border: 5px solid lightseagreen; */
  color: #fff;
  font-family: 'Ubuntu', sans-serif;
  transition: .1s all;
}

.contact_container .container {
  margin: 20px;
}

.title_bar {
  width: 100%;
  min-height: 30px;
  background: rgb(16, 16, 16);
  border-radius: 10px 10px 0 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.title_bar .title {
  display: flex;
  align-items: center;
  margin-left: 10px;
  font-size: 14px;
  display: flex;
  gap: 10px;
}

.title_bar .buttons {
  margin-right: 10px;
  display: flex;
  gap: 10px;
}

.title_bar .button {
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background: royalblue;
  cursor: pointer;
  color: #000;
  font-size: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}

.title_bar .button:hover {
  background: rgb(32, 51, 109);
  color: white;
}

.fullscreen {
  border-radius: 0%;
  width: 100vw !important;
  height: 100vh;
  position: fixed;
  left: 50%;
  transform: translate(-50%, -50%);
}

.fullscreen .contact_window {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}

.contact_container .contact_links {
  margin-top: 25px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.contact_container .contact_links .link {
  background: black;
  padding: 10px 30px 10px 30px;
  border-radius: 15px;
  height: 50px;
  display: flex;
  align-items: center;
  gap: 30px;
  text-decoration: none;
  transition: .2s all;
}

.contact_container .contact_links .link:hover {
  color: rgb(223, 223, 223);
  background: rgb(19, 19, 19);
  text-decoration: underline;
}

.contact_window {
  display: flex;
  justify-content: space-evenly;
}


@media (max-width: 650px) {
  .terminal_logo {
    font-size: 10px !important;
  }

  .login_box {
    width: 70%;
    height: 400px;
  }

  .login_area {
    flex-direction: column;
    padding: 40px;
    width: calc(100% - 80px);
  }

  .login_area input {
    width: 100%;
  }

}

@media (max-width: 880px) {
  .contact_container {
    left: 50%;
    transform: translate(-50%, -50%);
    width: 80%;
  }

  .contact_window {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
}


.loader {
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 100vw;
  background: #000;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 74px;
}

.loader .icon {
  animation: loader-anime 1s infinite;
}

@keyframes loader-anime {
  0% {
    shadow: 0 0 20px #fff;
  }
  100% {
    shadow: 0 0 10px #fff;
  }
}


Adding these styles, our Kali Linux Terminal themed portfolio is ready.


Final Look


We have created an awesome login screen, a working terminal and a great loader. What else?

Go ahead and feel free to experiment with the code.

See ya in the next post, till then Bye Bye!

0 Comments