Making a base website’s layout and its design

A long introduction and installing an UI library with Tailwindcss

A good website design is already half the battle. After all, if you think about it, only its work is visible. All the work that happens on the screen is not important for the end user. What is important is that in the end this work turned out to be completely useful for him, and on this functional site it was easy to read and was not an eyesore.
After reading this article you will get something like this.
SearchResultParser desktop version
Desktop version
I know from my own experience that design is a must-have these days. And for a developer, it is also important how to do this design and improve it. And I will tell you, my dear reader, do it with pure JS and CSS; this is quite a task.
Take this site, for example. It is written with pure JS (okay, also jQuery) and CSS. Supporting it is a real pain; adding some new component (like image zoom; I still haven’t done it) is a real feat.
Therefore, in this project (SearchResultParser), I will use a ready-made user interface library. I will use MaterialUI + Tailwindcss for more flexible customization of the site style without the need to get into CSS files. And we will use the Axios library for communication with the server.
Let’s install them:
npm install @mui/material @mui/icons-material/	 @emotion/react @emotion/styled tailwindcss
The TailewindCSS library needs to be configured. This command will create a configuration file:
npx tailwindcss init
In a tailwind.config.js, insert the next lines of code.
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
  "./src/**/*.{js,jsx,ts,tsx}",
  "./templates/**/*.html",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
It describes which files to apply this extension to.
Also, don't forget to create an input and output file. In my case, the first one is called index.css, and the other one is zero.css. The first one is in the src directory, the other one is in static/css.
In the input file, it is enough to insert several directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
And now, for the styles to be applied, you need to run the following command every time you change the files in which you use tailwindcss, whether it's html or js. You can add --watch so you don't have to restart it every time.
npx tailwindcss -i .\src\index.css -o .\static\css\index.css  --watch
TailwindCSS is installed and configured. Now let's configure axios. Everything is pretty simple here; add the following line to the package.json file:
...
"keywords": [],
"author": "",
"proxy": "http://localhost:8000",
"license": "ISC",
"description": "",
...
Let's move on to setting up the server.

Setting up django routes and views

I'll say right away that I won't have many routes. Because this site is primarily an application, they are also called SAAS. My SAAS will have the following routes/views:
  • main: The main application page. The user will spend most of the time here.
  • about: The page where I will tell about this project
  • contacts: The page with contact information
That's all, actually. Now let's add these views and routes to them.Let's add routes to Frontend/urls.py:
from django.urls import path
from .views import main, about, contacts, articles

urlpatterns = [
    path('', main, name='main'),
    path('about/', about, name='about'),
    path('contacts/', contacts, name='contacts'),
]
Let's write views, just for the sake of it, so that Django doesn't complain:
from django.shortcuts import render

def main(request):
    return render(request, 'Frontend/app.html')

def about(request):
    return render(request, 'Frontend/about.html')

def contacts(request):
    return render(request, 'Frontend/contacts.html')
This is our base. We will not return to django in this article. We will layout, layout and layout again.

Developing a base website layout

Preparation for work, launching the server

To start developing and see the results of our work, you will need to run several commands in the terminal. Firstly, so that TailwindCSS can generate styles for us. Secondly, so that React has time to collect components and render them.
To generate CSS styles using tailwindcss:
npm run tailwind -i ./src/index.css -o ./static/css/zero.css –watch
Flag -i for an input file
Flat -o for an output file
You also need to specify the --watch argument so you don't have to run this command every time you change a template or script.
To compile and generate JS:
npm run dev
Here, we run the previously recorded script in package.json. Of course, you can do it without a script, like this:
npm run webpack –mode development –watch
Chevron It is very important that you understand that these scripts take time to do their job. Therefore, sometimes when you refresh the page, the styles and scripts will not be updated, and many questions will arise. And this means that first we look whether the generation was successful, and then we go check the site.
All that remains is to launch the Django server, open a tab and start writing code.
./manage.py runserver

Working with django templates

Let's create a base template, base.html in templates/Frontend. Open it in a text editor and paste the following code:

{% load static %}

<!DOCTYPE html>
<html class="h-full w-full" lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="{% static 'css/zero.css' %}">
        <link rel="icon" href="http://localhost:8000/favicon.svg" type="image/svg+xml">
        {% block head %}
        {% endblock %}
    </head>
    <body class="w-full h-full flex flex-col">
        <div id="waiter" class="flex fixed left-0 top-0 w-full h-full hidden items-center justify-center z-10"></div>
        <div id="msg" class="flex fixed left-0 bottom-0 m-4 hidden"></div>
        <header class="flex justify-center items-center flex-shrink-0 basis-12 ">
            <div id="meta-header" class="hidden">
                {% block header %}
                {% endblock %}
                <div data-type="login"><a href="#">login</a></div>
            </div>
            <div id="header" class="flex w-full justify-center"></div>
        </header>
        <hr>
        
        <main class="flex-auto pl-2 pr-2 pt-3 pb-3">
            {% block main %}
            {% endblock %}
        </main>
        <hr>
        <footer class="items-center text-center flex-shrink-0 ">
            <div id="meta-footer" class="hidden">
                <a id="footer-in" href="https://timthewebmaster.com/en/">TimTheWebmaster ➥</a>
            </div>
            <div id="footer" class="flex flex-row flex-wrap justify-center p-3">
                Made by <div id="ref_to_place" class="pl-1 pr-1"></div> Copyright © 2024
            </div>
        </footer>
        <script>
            var IS_MOBILE = function() {
                let check = false;
                (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
                return check;
            }();
        </script>
        <script src="{% static 'js/main.js' %}"></script>
        {% block scripts %}
        {% endblock %}
    </body>
</html>
First, we load the value of the global variable static to have access to CSS, JS, JPEG, PNG, SVG and other media files on our server.
This file, as I call them, bases, these templates are not rendered directly, their main role is to be a skeleton/foundation/base for other templates. For example, this site, where this article is hosted, has the following bases:
  • base.html (Basic/common interface)
  • base_post.html (A base for any posts on website)
  • base_article.html (A base for posts of article type)
  • base_post_list.html (A base for pagination of any posts)
And so that the template that inherits the base could modernize it and add something of its own, special blocks need to be added. In this base there are 4 of them:
  • head (For meta tags, styles and scripts to start in a beginning)
  • header (For modification of base menu)
  • main (For content)
  • scripts (Only for scripts in the end of document)
All of the above block looks and used in django templates like this:

{% block main %}
{% endblock %}
Now that we have figured out how this template works, we need to make sure that this template is inherited by the following templates:
  • about.html
  • contacts.html
  • app.html
The app.html template:

{% extends 'Frontend/base.html' %}
{% load static %}

{% block head %}
    <title>SearchResultParser - main</title>
    <meta name="description" content="">
    <link rel="canonical" href="http://localhost:8000/">
{% endblock %}

{% block header %}
    <div data-type="inner-link"><a href="{% url 'about' %}">about</a></div>
    <div data-type="inner-link"><a href="{% url 'contacts' %}">contacts</a></div>
    <div data-type="inner-link"><a href="https://timthewebmaster.com/en/articles/series-of-articles-about-search-result-parser-webtool/">articles</a></div>
    <div data-type="tutorial"><a href="#">tutorial</a></div>
{% endblock %}

{% block main %}
{% endblock %}

{% block scripts %}
{% endblock %}
The about.html template:

{% extends 'Frontend/base.html' %}
{% load static %}

{% block head %}
    <title>SearchResultParser - About this project</title>
    <meta name="description" content="">
    <link rel="canonical" href="{% url 'about' %}">
{% endblock %}

{% block header %}
    <div data-type="inner-link"><a href="{% url 'about' %}">about</a></div>
    <div data-type="inner-link"><a href="{% url 'contacts' %}">contacts</a></div>
    <div data-type="inner-link"><a href="https://timthewebmaster.com/en/articles/series-of-articles-about-search-result-parser-webtool/">articles</a></div>
    <div data-type="inner-link"><a href="{% url 'main' %}">main</a></div>
{% endblock %}

{% block main %}
{% endblock %}

{% block scripts %}
{% endblock %}
The template for contacts.html is identical to the template written above, with the only difference being that they have different titles, canonical address and description.
Chevron The url function in the template takes the value of the name variable, which we filled in Frontend/urls.py
You can't live on templates alone, you need React. And you need to use it carefully. What's the matter? You might have noticed that I have special elements with IDs header and footer and next to them their analogs, meta-header and meta-footer. Why did I do this? Why not render everything in one block via react?
The reason for this is how react and django render pages. If react gives rendering to the user's machine CSR, then django does it itself, on the server SSR.
So what? What difference does it make who renders what. The main thing is that they render.
There is a difference, after all. And it is especially noticeable for search engines. A search robot, a crawler, will go to a page rendered by Django and will be able to see all the links and content of the site. But if the same crawler goes to a page rendered by React, it will see nothing, will consider the page either useless or unfinished and will leave.
That is, for SEO this is critical.
Chevron Although Google can already render pages with JS on its own, I wouldn’t count on it and would rely on static content.
And that's why I have these meta-* elements. They are rendered by django and are available to search engines. React picks up and processes these elements.
Now that we're done with HTML, let's move on to JS and React code.

Working with React elements

Let's create the necessary elements and files. We'll need 4 of them:
  • Header.js (hat and website’s menu)
  • Footer.js
  • MobileAppBar.js (a menu and a hat only for the mobile version of the website)
  • LangSwitch.js (language switcher)
Let's start with the most complex element of our site, header.js, this is its header. The code is quite voluminous, but in essence it takes the information rendered by django and forms either horizontal (Desktop version) or vertical (Mobile version) buttons from it. That's all.
Well, if this is a mobile version, it wraps these buttons in a side menu. Because I like it the most. And here is the code:
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup';
import MenuAppBar from './MobileAppBar'
import LangSwitch from './LangSwitch';
import Drawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
import AppBar from '@mui/material/AppBar';
import LinkIcon from '@mui/icons-material/Link';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import LoginIcon from '@mui/icons-material/Login';

export default function Header() {
  const header_buttons = document.getElementById('meta-header')
  const btns = []
  for ( const btn of header_buttons.children){
    const ref = btn.firstElementChild.getAttribute('href')
    if (btn.dataset.type == 'inner-link'){
      btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LinkIcon className='mb-2' fontSize='small' /></Button>)
    }
    else if (btn.dataset.type == 'tutorial' ){
      btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><QuestionMarkIcon className='mb-2'  fontSize='small'/></Button>)
    }
    else{
      btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LoginIcon className='mb-2' fontSize='small' /></Button>)
    }
  }

  if (IS_MOBILE){
    const [open, setOpen] = React.useState(false);
    const toggleSideMenu = (newOpen) => () => {
        setOpen(newOpen)
    }

    return (
      <div>
        <MenuAppBar toggleSideMenu={toggleSideMenu} ></MenuAppBar>
        <Drawer open={open} onClose={toggleSideMenu(false)}>
          <Box className="flex flex-col justify-between h-full min-w-52">
            <ButtonGroup orientation='vertical'  color='primary' variant="text" aria-label="Basic button group">
                {btns}
            </ButtonGroup>
            <Box className="flex felx-row justify-between">
                <LangSwitch></LangSwitch>
            </Box>
          </Box>
        </Drawer>
      </div>
    );
  }else{
    return (
      <AppBar position="fixed" sx={{bgcolor: "white", paddingBottom: "10px", paddingTop: "10px" }}>
        <ButtonGroup  color='primary' className='flex flex-grow justify-center' variant="text" aria-label="Basic button group">
          {btns}
        </ButtonGroup>
        <Box  className="fixed top-0 right-0 flex felx-row justify-between">
            <LangSwitch></LangSwitch>
        </Box>
      </AppBar>
    );
  }
}

const container1 = document.getElementById('header');
const root1 = createRoot(container1);
root1.render(<Header></Header>);
In this component I’m using two more, LangSwitch and MenuAppBar.
In the MobileAppBar.js:
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';

export default function MenuAppBar({ toggleSideMenu }) {
    
    return (
        <Box sx={{ flexGrow: 1 }}>
            <AppBar sx={{bgcolor: "white"}}  position="fixed">
                <Toolbar color="error">
                    <IconButton
                        size="large"
                        edge="start"
                        color="black"
                        aria-label="menu"
                        sx={{ mr: 2 }}
                        onClick={toggleSideMenu(true)}
                    >
                        <MenuIcon />
                    </IconButton>
                </Toolbar>
            </AppBar>
        </Box>
    );
}
In the LangSwitch.js:
import * as React from 'react';
import { styled } from '@mui/material/styles';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';

const MaterialUISwitch = styled(Switch)(({ theme }) => ({
  width: 62,
  height: 34,
  padding: 7,
  '& .MuiSwitch-switchBase': {
    margin: 1,
    padding: 0,
    transform: 'translateX(6px)',
    '&.Mui-checked': {
      color: '#fff',
      transform: 'translateX(22px)',
      '& .MuiSwitch-thumb:before': {
        backgroundImage: `url('data:image/svg+xml,<svg width="512" height="512" viewBox="0 0 512 512" style="color:%231C2033" xmlns="http://www.w3.org/2000/svg" class="h-full w-full"><rect width="512" height="512" x="0" y="0" rx="30" fill="transparent" stroke="transparent" stroke-width="0" stroke-opacity="100%" paint-order="stroke"></rect><svg width="19px" height="19px" viewBox="0 0 512 512" fill="%231C2033" x="246.5" y="246.5" role="img" style="display:inline-block;vertical-align:middle" xmlns="http://www.w3.org/2000/svg"><g fill="%231C2033"><mask id="circleFlagsUm0"><circle cx="256" cy="256" r="256" fill="%23fff"/></mask><g mask="url(%23circleFlagsUm0)"><path fill="%23eee" d="M256 0h256v64l-32 32l32 32v64l-32 32l32 32v64l-32 32l32 32v64l-256 32L0 448v-64l32-32l-32-32v-64z"/><path fill="%23d80027" d="M224 64h288v64H224Zm0 128h288v64H256ZM0 320h512v64H0Zm0 128h512v64H0Z"/><path fill="%230052b4" d="M0 0h256v256H0Z"/><path fill="%23eee" d="m187 243l57-41h-70l57 41l-22-67zm-81 0l57-41H93l57 41l-22-67zm-81 0l57-41H12l57 41l-22-67zm162-81l57-41h-70l57 41l-22-67zm-81 0l57-41H93l57 41l-22-67zm-81 0l57-41H12l57 41l-22-67Zm162-82l57-41h-70l57 41l-22-67Zm-81 0l57-41H93l57 41l-22-67zm-81 0l57-41H12l57 41l-22-67Z"/></g></g></svg></svg>')`,
      },
      '& + .MuiSwitch-track': {
        opacity: 1,
        backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
      },
    },
  },
  '& .MuiSwitch-thumb': {
    backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
    width: 32,
    height: 32,
    '&::before': {
      content: "''",
      position: 'absolute',
      width: '100%',
      height: '100%',
      left: 0,
      top: 0,
      backgroundRepeat: 'no-repeat',
      backgroundPosition: 'center',
      backgroundImage: `url('data:image/svg+xml,<svg width="512" height="512" viewBox="0 0 512 512" style="color:%231C2033" xmlns="http://www.w3.org/2000/svg" class="h-full w-full"><rect width="512" height="512" x="0" y="0" rx="30" fill="transparent" stroke="transparent" stroke-width="0" stroke-opacity="100%" paint-order="stroke"></rect><svg width="19px" height="19px" viewBox="0 0 512 512" fill="%231C2033" x="246.5" y="246.5" role="img" style="display:inline-block;vertical-align:middle" xmlns="http://www.w3.org/2000/svg"><g fill="%231C2033"><mask id="circleFlagsRu0"><circle cx="256" cy="256" r="256" fill="%23fff"/></mask><g mask="url(%23circleFlagsRu0)"><path fill="%230052b4" d="M512 170v172l-256 32L0 342V170l256-32z"/><path fill="%23eee" d="M512 0v170H0V0Z"/><path fill="%23d80027" d="M512 342v170H0V342Z"/></g></g></svg></svg>')`,
    },
  },
  '& .MuiSwitch-track': {
    opacity: 1,
    backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
    borderRadius: 20 / 2,
  },
}));

export default function LangSwitch() {
  return (
    <FormGroup>
      <FormControlLabel
        control={<MaterialUISwitch sx={{ m: 1 }} defaultChecked />}
      />
    </FormGroup>
  );
}
The LangSwitch component is not that big. Most of the space is taken up by the SVG image settings. It is not working now, though, i.e. it does not switch languages. But that is because we have not yet configured django for this. This will be in the future. For now, we just have a working switch.
All that remains is to consider the Footer component in Footer.js:
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import Link from '@mui/material/Link'

export default function Footer() {
    const ref_blk = document.getElementById('footer-in')
    const ref = ref_blk.getAttribute('href')
    const text_in = ref_blk.innerText 

    return (
        <Link href={ref} underline="hover">{text_in}</Link>
    );
}

const container = document.getElementById('ref_to_place');
const root = createRoot(container);
root.render(<Footer></Footer>);
Initially, I planned to add lots and lots of links there, but I got lazy, and why would there be links there? Only an extra burden on the user's perception. So I left only one link to myself)
And of course, don't forget to connect our Header and Footer components to index.js:
import Header from './components/Header';
import Footer from './components/Footer';

Developing the main page and its components

The user experience with the application

So, we are moving on to the hardest part of this article. At least, it is the biggest one. I even thought about splitting this article, but I did not do it because of the loss of integrity of article. What will the user experience with the application look like?
  • A random user open the website.
    Step 1: open website
  • Click on an add button.
    step 2: push a button "add engine"
  • Selects the required engines.
    step 3: choose an engine
  • Fill the text fields.
    step 4: fill a text fields
  • Configures the parser.
    step 5: parser setup
  • Launches it.
    step 6: start parsing
  • As a result of the work, the user will receive a link to the downloadable file.
    step 7: download a file

Django application template, app.html

Now to the application and code. Let's change the app.html template a little bit so that it can be easily worked from a react component.
{% extends 'Frontend/base.html' %}
{% load static %}

{% block head %}
    <title>SearchResultParser - main</title>
    <meta name="description" content="">
    <link rel="canonical" href="http://localhost:8000/">
{% endblock %}

{% block header %}
    <div data-type="inner-link"><a href="{% url 'about' %}">about</a></div>
    <div data-type="inner-link"><a href="{% url 'contacts' %}">contacts</a></div>
    <div data-type="inner-link"><a href="{% url 'articles' %}">articles</a></div>
    <div data-type="tutorial"><a href="#">tutorial</a></div>
{% endblock %}

{% block main %}
<div id="app" class="flex flex-col h-full justify-center max-w-screen-md ml-auto mr-auto">
        {# Initially takeover by React #}
        <div id="app_settings" class="flex-grow-0 basis-10  content-center p-2"></div>
        <div id="app_body"  class="flex flex-grow">
            <div id="meata-engines">
                <div data-src="{% static '/img/Google.png' %}"></div>
                <div data-src="{% static '/img/Yahoo.png' %}"></div>
                <div data-src="{% static '/img/Bing.png' %}"></div>
                <div data-src="{% static '/img/DuckDuckGo.png' %}"></div>
                <div data-src="{% static '/img/Baidu.png' %}"></div>
                <div data-src="{% static '/img/Yandex.png' %}"></div>
                <div data-src="{% static '/img/Aol.png' %}"></div>
                <div data-src="{% static '/img/StackOverflow.png' %}"></div>
                <div data-src="{% static '/img/GitHub.png' %}"></div>
                <div data-src="{% static '/img/Ask.png' %}"></div>
                <div data-src="{% static '/img/YouTube.png' %}"></div>
                <div data-src="{% static '/img/MyAnimeList.png' %}"></div>
                <div data-src="{% static '/img/GoogleScholar.png' %}"></div>
                <div data-src="{% static '/img/GoogleNews.png' %}"></div>
                <div data-src="{% static '/img/Coursera.png' %}"></div>
            </div>
            <div id="engines" class="flex-grow-0  p-2 border-r">
                <h2 class="">Search Engines</h2>
                <hr>
                <div id="engines_list" class="flex gap-1 flex-col p-2">
                </div>
            </div>
            <div id="queries" class="flex-grow text-left  p-2">
                <h2>Queries</h2>
                <hr>
                <div id="queries_list"class="flex gap-1 flex-col p-2">
                </div>
            </div>
        </div>
        {# Initially takeover by React #}
        <hr>
        <div  class="flex justify-between flex-grow-0 basis-10 items-center">
            <div id="meta-app_utills" class="hidden">
                {# Saved presets #}
                <div id="utill_saved"></div>
                {# Popular presets #}
                <div id="utill_popular"></div>
            </div>
            <div id="app_utils" class="p-2"></div>
            <div id="app_actions" class="p-2"></div>
        </div>
    </div>
{% endblock %}

{% block scripts %}
{% endblock %}
From the template, you can see that my application is divided into several independent parts. These are settings (id = "app_settings"), a table of queries and engines (id = "engines" + id = "queries"), utilities (id = "app_utils") and actions (id = "app_actions").
I want to note the meta-engines block. Here I manually wrote all the engines that I am going to parse, but in the future this block will be filled by django. It's just that in the future I might want to add other engines or remove old ones and it is better to do this on the server.

React components of the application

The application is divided into 4 parts + two more components:
  • AppSettings.js
  • AppUtils.js
  • AppActions.js
  • AppQueries.js
  • Waiter.js
  • Msg.js
Create them in a src/components folder and let's move on to their analysis.
AppSettings.js
AppUtils.js
AppActions.js
AppQueries.js
Waiter.js
Msg.js
This is just a group of switches with check boxes for configuring the parser. It should be noted that the selected data is saved in the so-called data attribute. So that you can easily get them from another application, AppActions.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { IconButton, Box } from '@mui/material';
import SettingsApplicationsIcon from '@mui/icons-material/SettingsApplications';
import CloseIcon from '@mui/icons-material/Close';
import Paper from '@mui/material/Paper';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import FormGroup from '@mui/material/FormGroup';
import Checkbox from '@mui/material/Checkbox';

// To render an actual options to choose
function SettingsContent(){
    const [exportType, setExport] = React.useState('')
    const [title, setTitle] = React.useState(false)
    const [description, setDesr] = React.useState(false)
    const [url, setUrl] = React.useState(false)
    const [verbose, setVerb] = React.useState(false)

    return (
        <Box className="w-full flex flex-nowrap flex-row justify-between p-2">
            <Box>
                <FormLabel data-export='exel' id="export-as">Export as:</FormLabel>
                <RadioGroup
                    aria-labelledby="export-as"
                    defaultValue="exel"
                    name="export-as"
                >
                    <FormControlLabel value="exel" control={<Radio onChange={(ev,checked)=>{ 
                        var root = document.getElementById('export-as')
                        root.dataset.export = ev.target.value
                        setExport(ev.target.value)
                    }}/>} label="exel" />
                    <FormControlLabel value="csv" control={<Radio  onChange={(ev,checked)=>{ 
                        var root = document.getElementById('export-as')
                        root.dataset.export = ev.target.value
                        setExport(ev.target.value)
                    }}/>} label="csv" />
                    <FormControlLabel value="json" control={<Radio  onChange={(ev,checked)=>{ 
                        var root = document.getElementById('export-as')
                        root.dataset.export = ev.target.value
                        setExport(ev.target.value)
                    }}/>} label="json" />
                </RadioGroup>
            </Box>
            <Box>
                <FormGroup>
                    <FormLabel id="save">Save:</FormLabel>
                    <FormControlLabel id="dataTitle" value="title" control={<Checkbox checked={title} onChange={(ev, checked)=>{
                        setTitle(checked)
                    }} />} label="title" />
                    <FormControlLabel id="dataUrl" value="url" control={<Checkbox checked={url} onChange={(ev, checked)=>{
                        setUrl(checked)
                    }} />} label="url" />
                    <FormControlLabel id="dataDescription" value="description" control={<Checkbox checked={description} onChange={(ev, checked)=>{
                        setDesr(checked)
                    }} />} label="description" />
                </FormGroup>
            </Box>
            <Box>
                <FormGroup>
                    <FormLabel id="other">Other:</FormLabel>
                    <FormControlLabel value="verbose" control={<Checkbox onChange={(ev, checked)=>{
                        setVerb(checked)
                        const console = document.getElementById('console-button')
                        console.classList.toggle('hidden')
                    }} />} label="verbose" />
                </FormGroup>
            </Box>
        </Box>
    )
}
export default function AppSettings(){
    const [isSettings, setSettings] = React.useState(false);

    // To hide and show available choises for user
    const ToggleSettings = () => {
        if (isSettings == true){
            setSettings(false);
            const set_cont = document.getElementById('settings_content')
            set_cont.classList.add('hidden')
        }
        else{
            const set_cont = document.getElementById('settings_content')
            set_cont.classList.remove('hidden')
            setSettings(true)
        }
    }

    return (
        <div>
            <Paper elevation={2} className="z-10">
                <div id="settings_content" className='hidden'>
                    <SettingsContent />
                </div>
            </Paper>
            {/* Icon button is gonna be changed */}
            <IconButton onClick={ToggleSettings} className='w-fit'>
                { isSettings    ? <CloseIcon  className=" border-2 rounded-md"/>
                                : <SettingsApplicationsIcon className=" border-2 rounded-md"/>}
            </IconButton>
        </div>
    )
}

const settings_container = document.getElementById('app_settings');
const settings_root = createRoot(settings_container);
settings_root.render(<AppSettings></AppSettings>);
This component is implemented using this chain of actions/uis, buttonbottom slidermodal window. And at each stage, requests will be made to the server to get the necessary presets.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { IconButton, Box } from '@mui/material';
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
import FeaturedPlayListIcon from '@mui/icons-material/FeaturedPlayList';
import SavedSearchIcon from '@mui/icons-material/SavedSearch';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import UploadIcon from '@mui/icons-material/Upload';
import InfoIcon from '@mui/icons-material/Info';
import Drawer from '@mui/material/Drawer';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import {Wait, StopWait} from './Waiter';
import DownloadIcon from '@mui/icons-material/Download';
import axios from "axios";

// To get info about preset
function onUtilsContentInfo(rec){
    // Make a GET request for a specific preset
    var uid = 0
    Wait()
    axios.get(`/api/presets/${uid}`)
    .then(response => {
        // Here recieve a file 
        StopWait('Successfully obtain info about preset.', 'success')
    })
    .catch(error => {
        StopWait('Cant obtain info about preset. ' + error , 'error')
    });
    // Apply a recieved data to a poped up modal window
    const utils_modal = document.getElementById('utils-modal')
    const utils_modal_root = createRoot(utils_modal);
    utils_modal_root.render( 
        <div className="flex flex-col gap-3  min-w-52">
            <Typography id="modal-modal-title" variant="h6" component="h3">
            Preset info
            </Typography>
            <hr></hr>
            <List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
                <ListItemText primary="Export as:" />
                <List component="div" disablePadding>
                    <ListItemText sx={{ pl: 4 }} primary="JSON" />
                </List>
                <ListItemText primary="Save:" />
                <List component="div" disablePadding>
                    <ListItemText sx={{ pl: 4 }} primary="title" />
                    <ListItemText sx={{ pl: 4 }} primary="description" />
                    <ListItemText sx={{ pl: 4 }} primary="url" />
                </List>
                <ListItemText primary="Other:" />
                <List component="div" disablePadding>
                    <ListItemText sx={{ pl: 4 }} primary="verbose" />
                    <ListItemText sx={{ pl: 4 }} primary="send to email" />
                </List>
                <hr></hr>
                <ListItemText primary="Search engines:" />
                <List component="div" disablePadding>
                    <ListItemText sx={{ pl: 4 }} primary="Google" />
                    <List component="div" disablePadding>
                        <ListItemText sx={{ pl: 6 }} primary="'how to find a cats'" />
                        <ListItemText sx={{ pl: 6 }} primary="'how to find'" />
                    </List>
                    <ListItemText sx={{ pl: 4 }} primary="Yandex" />
                    <List component="div" disablePadding>
                        <ListItemText sx={{ pl: 6 }} primary="'how to get better'" />
                    </List>
                </List>
            </List>
        </div>
    );
}

// To apply preset for current user
function onUtilsContentUpload(rec){
    var uid = 0
    Wait()
    axios.get(`/api/presets/${uid}`)
    .then(response => {
        // Here recieve a file 
        StopWait('Successfully apply preset for user.', 'success')
    })
    .catch(error => {
        StopWait('Cant apply preset for user. ' + error , 'error')
    });
    // Make a GET request for a specific preset
    // Apply recieved preset for user 
}

// To delete preset of current user
function onUtilsContentDelete(rec){
    var uid = 0
    // Make a DEL request to remove a specific preset
    axios.delete(`/api/presets/${uid}`)
    .then(data => {
        // Here recieve a file 
        StopWait('Successfully delete a preset.', 'success')
        Wait()
        axios.get('/api/presets/')
        .then(response => {
            // Here recieve a file 
            StopWait('Successfully refreshed all users presets.', 'success')
        })
        .catch(error => {
            StopWait('Cant refresh user presets. ' + error , 'error')
        });
    })
    .catch(error => {
        StopWait('Cant delete a data. ' + error , 'error')
    });
    
    const utils_modal = document.getElementById('utils-modal')
    const utils_modal_root = createRoot(utils_modal);
    utils_modal_root.render( 
        <div className="flex flex-col gap-3  min-w-52">
            <Typography id="modal-modal-title" variant="h6" component="h3">
            Deleting preset
            </Typography>
            <hr></hr>
            <div className="p-1">
            Are you sure ?
            </div>
            <div className='flex flex-row justify-between items-center'>
                <Button className='p-2' variant="outlined" onClick={()=>{
                    // DEL request
                    rec.setModal(false)
                }}>Yes</Button>
                <Button className='p-2' variant="outlined" onClick={()=>{
                    rec.setModal(false)
                }}>No</Button>
            </div>
        </div>
    );
}

// Wait till modal is present and ready to be interactable
// Then launch callback with args
function waitTillModalIsUp(func, args){
    const observer = new MutationObserver((mutationList, observer) => {
        for (const mutation of mutationList) {
            if (mutation.addedNodes.length > 0){ 
                const utils_modal = mutation.target.querySelector("#utils-modal");
                if(utils_modal){
                    func(args)
                    observer.disconnect()
                }
            }
        }
    });
    observer.observe(document, {subtree: true, childList: true});
}

// Popular presets, I will create them by myself
function TrendingContent( props ){
    const [isModal, setModal] = React.useState(false);
    // Make a POST request to collect most popular presets
    Wait()
    axios.get('/api/popular-presets/')
    .then(response => {
        // Here recieve a file 
        StopWait('Successfully obtain popular presets.', 'success')
    })
    .catch(error => {
        StopWait('Cant get popular presets. ' + error , 'error')
    });
    const rec = {'setModal': setModal}
    return (
        <Box className="w-full min-h-32 max-h-96 flex flex-nowrap flex-col justify-between p-2">
            <Modal
                open={isModal}
                onClose={()=>{setModal(false)}}
                >
                <Box id='utils-modal' className="absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-2/4 shadow-md p-4 bg-white">
                </Box>
            </Modal>
            <Typography variant="h5" component='h2' gutterBottom className="p-2">Trending presets</Typography>
            <hr className='w-full'></hr>
            <Box className='w-full h-full'>
                {/* Template to be rendered by Django*/}
                <div>
                    <div className='flex justify-between items-center'>
                        <div className='flex flex-row gap-2 '>
                            <div>export_as_exel_save-title-description-verbose-show-final-result</div>
                        </div>
                        <div>
                            <IconButton onClick={()=>{setModal(true); waitTillModalIsUp(onUtilsContentInfo,rec)}}  className='w-fit'><InfoIcon/></IconButton>
                            <IconButton onClick={()=>onUtilsContentUpload(rec)} className='w-fit'><UploadIcon/></IconButton>
                        </div>
                    </div>
                    <hr className='w-full'></hr>
                </div>
            </Box>
        </Box>
    )
}

// Saved presets of user
function OwnSavesContent(props){
    const [isModal, setModal] = React.useState(false);
    // Make GET firts request to obtain all presets by user
    Wait()
    axios.get('/api/presets/')
    .then(response => {
        // Here recieve a file 
        StopWait('Successfully obtain all users presets.', 'success')
    })
    .catch(error => {
        StopWait('Cant get presets. ' + error , 'error')
    });
    const rec = {'setModal': setModal}
    return (
        <Box className="w-full min-h-32  max-h-96 flex flex-nowrap flex-col justify-between p-2">
            <Modal
                open={isModal}
                onClose={()=>{setModal(false)}}
                >
                <Box id='utils-modal' className="absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-2/4 shadow-md p-4 bg-white">
                </Box>
            </Modal>
            <Typography variant="h5" component='h2' gutterBottom className="p-2">Your presets</Typography>
            <hr className='w-full'></hr>
            <Box className='w-full h-full'>
            {/* Template to be rendered by Django */}
            <div>
                <div className='flex justify-between  items-center'>
                    <div className='flex flex-row gap-2 '>
                        <div>01.01.2000</div>
                        <div>Save name</div>
                    </div>
                    <div>
                        <IconButton onClick={()=>{setModal(true); waitTillModalIsUp(onUtilsContentInfo,rec)}}  className='w-fit'><InfoIcon/></IconButton>
                        <IconButton onClick={()=>{onUtilsContentUpload(rec); }} className='w-fit'><UploadIcon/></IconButton>
                        <IconButton onClick={()=>{setModal(true); waitTillModalIsUp(onUtilsContentDelete,rec)}} className='w-fit'><DeleteForeverIcon/></IconButton>
                    </div>
                </div>
                <hr className='w-full'></hr>
            </div>
            </Box>
        </Box>
    )
}

// Console for curious ones
function ConsoleContent(props){
    /* Make requests here */
    return (
        <Box className="w-full min-h-32  max-h-96 flex flex-nowrap flex-col justify-between p-2">
            <Typography variant="h5" component='h2' gutterBottom className="p-2">Console</Typography>
            <hr className='w-full'></hr>
            <Box className='w-full h-full '>
                {/* Paste response here */}
                <Box>user@localhost $: npm run test</Box>
            </Box>
        </Box>
    )
}

export default function AppUtils(){
    const [isTrending, setTrending] = React.useState(false);
    const [isOwnSaves, setOwnSaves] = React.useState(false);
    const [isConsole, setConsole] = React.useState(false);

    // To show or hide sticked to bottom side of screen the "Drawer"
    const ToggleTrending = (value) => (event) => {
        setTrending(value);
    }
    const ToggleOwnSaves = (value) => (event) => {
        setOwnSaves(value);
    }
    const ToggleConsole = (value) => (event) => {
        setConsole(value);
    }

    return (
        <Box className="flex flex-col">
            <Box className="flex gap-1">
                <IconButton onClick={ToggleTrending(true)} className='w-fit  border'><TrendingUpIcon className=" border-2 rounded-md"/></IconButton>
                <IconButton onClick={ToggleOwnSaves(true)} className='w-fit'><SavedSearchIcon className=" border-2 rounded-md"/></IconButton>
                <div id="console-button" className='hidden'><IconButton onClick={ToggleConsole(true)} className='w-fit'><FeaturedPlayListIcon  className=" border-2 rounded-md"/></IconButton></div>
                <div id="results-button" className='hidden'><IconButton id="results-button-ref" href='#' onClick={()=>{}} className='w-fit'><DownloadIcon color='warning' className=" border-2 rounded-md"></DownloadIcon></IconButton></div>
            </Box>
            <Drawer open={isTrending} anchor='bottom' onClose={ToggleTrending(false)}>
                <TrendingContent isActive={isTrending}/>
            </Drawer>
            <Drawer open={isOwnSaves} anchor='bottom' onClose={ToggleOwnSaves(false)}>
                <OwnSavesContent isActive={isOwnSaves}/>
            </Drawer>
            <Drawer open={isConsole} anchor='bottom' onClose={ToggleConsole(false)}>
                <ConsoleContent isActive={isConsole}/>
            </Drawer>
        </Box>
    )
}

const utils_container = document.getElementById('app_utils');
const utils_root = createRoot(utils_container);
utils_root.render(<AppUtils></AppUtils>);
Consists of only two buttons, saving the preset and starting the parsing. Here, we collect data from other applications, here we check them for correctness and here we send them to the server.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { IconButton, Box } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import NotStartedIcon from '@mui/icons-material/NotStarted';
import Typography from '@mui/material/Typography';
import Modal from '@mui/material/Modal';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import { Wait, StopWait, Msg } from './Waiter';
import axios from "axios";

// Wait till modal is present and ready to be interactable
// Then launch callback with args
function waitTillModalIsUp(func, args){
    const observer = new MutationObserver((mutationList, observer) => {
        for (const mutation of mutationList) {
            if (mutation.addedNodes.length > 0){ 
                const utils_modal = mutation.target.querySelector("#utils-modal");
                if(utils_modal){
                    func(args)
                    observer.disconnect()
                }
            }
        }
    });
    observer.observe(document, {subtree: true, childList: true});
}

// A set of checks to be checked if user make everything is in a right way
function checkIfValidPreset(){
    // Collect data
    var exportAs = document.getElementById('export-as').dataset.export
    var isTitle = document.getElementById('dataTitle').querySelector('input').checked
    var isUrl = document.getElementById('dataUrl').querySelector('input').checked
    var isDescription = document.getElementById('dataDescription').querySelector('input').checked
    if (!isTitle && !isUrl && !isDescription){
        Msg('You must "check" one of these: title, url, description','error')
        return false
    }
    var queries = []
    var queries_raw = document.querySelectorAll('.query')
    queries_raw.forEach((que) => {
        var engine = que.dataset.engine
        var query = que.querySelector('input').value
        queries.push({
            engine: engine,
            query: query
        })
    })
    if (queries.length >= 1){
        var isValid = true
        queries.forEach((que) => {
            if (que.query == ""){
                Msg('Query string cant be empty.','error')
                isValid = false
            }
        })
        return isValid
    }
    else{
        Msg('At least 1 query must be.','error')
        return false
    }
}

// Make a PUT request to save preset
function SaveRequest(req){
    // Collect data
    var exportAs = document.getElementById('export-as').dataset.export
    var isTitle = document.getElementById('dataTitle').querySelector('input').checked
    var isUrl = document.getElementById('dataUrl').querySelector('input').checked
    var isDescription = document.getElementById('dataDescription').querySelector('input').checked
    var queries = []
    var queries_raw = document.querySelectorAll('.query')
    queries_raw.forEach((que) => {
        var engine = que.dataset.engine
        var query = que.querySelector('input').value
        queries.push({
            engine: engine,
            query: query
        })
    })
    if (checkIfValidPreset()){
        const actions_modal = document.getElementById('utils-modal')
        const actions_modal_root = createRoot(actions_modal);
        actions_modal_root.render( 
            <div className="flex flex-col gap-3  min-w-72 ">
                <Typography variant="h6" component="h3">
                    Saving preset
                </Typography>
                <hr></hr>
                <Box className="flex flex-row gap-4 items-center justify-between">
                    <TextField id="preset-name" label="Preset name" variant="outlined" />
                    <Button onClick={()=>{
                        var preset_name = document.getElementById('preset-name').value
                        if (preset_name.length >= 3){
                            Wait()
                            var form_data = new FormData();
                            form_data.append("exportAs", exportAs)
                            form_data.append("isTitle", isTitle)
                            form_data.append("isUrl", isUrl)
                            form_data.append("isDescription", isDescription)
                            form_data.append("queries", JSON.stringify(queries))
                            axios.put(`/api/presets/${preset_name}`, form_data)
                            .then(data => {
                                // Here recieve a file 
                                StopWait('Successfully saved preset', 'success')
                            })
                            .catch(error => {
                                StopWait('Cant save a preset. ' + error , 'error')
                            });
                        }
                        else{
                            Msg('At least 3 character long.', 'error')
                            req.setModal(false)
                        }
                    }} variant='text'>Save</Button>
                </Box>
            </div>
        )
    }
    
}

//Make a POST request to server to get results
function StartParsingRequest(req){
    // Collect data
    var exportAs = document.getElementById('export-as').dataset.export
    var isTitle = document.getElementById('dataTitle').querySelector('input').checked
    var isUrl = document.getElementById('dataUrl').querySelector('input').checked
    var isDescription = document.getElementById('dataDescription').querySelector('input').checked
    var queries = []
    var queries_raw = document.querySelectorAll('.query')
    queries_raw.forEach((que) => {
        var engine = que.dataset.engine
        var query = que.querySelector('input').value
        queries.push({
            engine: engine,
            query: query
        })
    })
    if (checkIfValidPreset()){
        Wait()
        var form_data = new FormData();
        form_data.append("exportAs", exportAs)
        form_data.append("isTitle", isTitle)
        form_data.append("isUrl", isUrl)
        form_data.append("isDescription", isDescription)
        form_data.append("queries", JSON.stringify(queries))
        axios.post('/api/parse/', form_data)
        .then(data => {
            // Here recieve a file 
            StopWait('Successfully parsed a data.', 'success')
        })
        .catch(error => {
            StopWait('Cant parse data. ' + error , 'error')
        });
    }
}

export default function AppActions(){
    const [isModal, setModal] = React.useState(false);
    const req = {'setModal': setModal}
    return (
        <Box className="flex gap-1">
            <IconButton id='onSaveRequest'  onClick={()=>{setModal(true); waitTillModalIsUp(SaveRequest, req)}} className='w-fit'><SaveIcon className=" border-2 rounded-md"/></IconButton>
            <Modal
                open={isModal}
                onClose={()=>{setModal(false)}}
                >
                <Box id='utils-modal' className="absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-2/4 shadow-md p-4 bg-white">
                </Box>
            </Modal>
            <IconButton id='onStartParsing'  onClick={()=>{StartParsingRequest(req)}} className='w-fit'><NotStartedIcon className=" border-2 rounded-md"/></IconButton>
        </Box>
    )
}

const actions_container = document.getElementById('app_actions');
const actions_root = createRoot(actions_container);
actions_root.render(<AppActions></AppActions>);
Getting engines available for parsing and creating an engine-query table. Initially, I planned to do it in such a way that the user would add an engine, then add as many queries to it as he wanted. But then I realized that all this can be implemented much more simply and through one button.
And also, you will need icons for all engines. You will need to download this archive and unpack it in the Frontend/static/img folder
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import AddBoxIcon from '@mui/icons-material/AddBox';
import { IconButton, Box } from '@mui/material';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import TextField from '@mui/material/TextField';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import {Wait, StopWait} from './Waiter';
import Popover from '@mui/material/Popover';
import Divider from '@mui/material/Divider';

// Remove a line of query from UI
function onDeleteQuery(event){
    var id = event.currentTarget.dataset.id
    var elsToDelete = document.querySelectorAll('#'+id)
    elsToDelete.forEach((el)=>{
        el.remove()
    })
}

// Compiling and inserting a choosen engine with text field
function onAddQuery(event){

    var uid = 'uid_'+Math.random().toString(16).slice(2)
    // Inserting engine before + button and showing up all activity buttons
    var child = event.currentTarget.children[0].cloneNode(true)
    child.id = uid
    child.querySelectorAll('#toRemoveEngineStuff').forEach( (el) => {
        el.classList.remove('hidden')
    })

    var deleteButton = child.querySelector('#onDeleteQuery')
    deleteButton.dataset.id = uid
    deleteButton.addEventListener('click', onDeleteQuery)

    var parent = document.getElementById('engines_list')
    parent.insertBefore(child, parent.lastChild);
    var engine_name = event.currentTarget.dataset.engine_name
    // Activate 'save' and 'parse' buttons

    // Inserting query text field
    var querCont = document.getElementById('queries_list');
    var query = document.createElement("div");
    query.id = uid
    querCont.insertBefore(query, querCont.lastChild);
    const querRoot = createRoot(query);

    querRoot.render(
        <div className='flex flex-col gap-2'>
            <div className='flex flex-row items-center'>
                <TextField required data-engine={engine_name} id="outlined-basic" label="query" variant="outlined"  className="query items-center"/>
            </div>
            <Divider className="w-full self-center" component='div' variant="middle"/>
        </div>
    )
            
}
export default function AppQueries(){
    const [anchorEngine, setAnchoreEngine] = React.useState(null)
    var engine_list = []
    const list = document.getElementById('meata-engines').children
    for (var i = 0; i < list.length; i++){
        // Find name of engine
        var end = list[i].dataset.src.indexOf('.')
        var start = list[i].dataset.src.lastIndexOf('/') + 1
        var name = String(list[i].dataset.src).substring(start, end)
        // Push engines to pop up window, for to be selected later
        engine_list.push(
            <ListItem>
            <ListItemButton data-engine_name={name} onClick={(event)=>{
                onAddQuery(event)
            }}>
                <div className="flex flex-col gap-6">
                    <div className="flex flex-row gap-2 items-center justify-between" >
                        <div className='flex flex-row gap-1 items-center'>
                            <img className='w-4 h-4' src={list[i].dataset.src}></img>
                            <div>{name}</div>
                        </div>
                        <div id='toRemoveEngineStuff' className='hidden flex flex-row gap-1 items-center w-fit'>
                            <IconButton id='onDeleteQuery' className='w-fit'><DeleteForeverIcon/></IconButton>
                        </div>
                    </div>
                    <Divider id='toRemoveEngineStuff' className="hidden w-full self-center" component='div' variant="middle"/>
                </div>
            </ListItemButton>
            </ListItem>
        )
    }
    
    const handleClick = (event, engine_list) => {
        setAnchoreEngine(event.currentTarget);    
    };

    const handleClose = () => {
        setAnchoreEngine(null);
    };

    const open = Boolean(anchorEngine);
    const id = open ? 'simple-popover_addengine' : undefined;
    return (
        <div>
            <Popover
                id={id}
                open={open}
                anchorEl={anchorEngine}
                onClose={handleClose}
                anchorOrigin={{
                    vertical: 'bottom',
                    horizontal: 'left',
                }}
                >
                <List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
                    {engine_list}
                </List>
            </Popover>
            <IconButton onClick={(ev)=>handleClick(ev,engine_list)} className='w-fit'><AddBoxIcon/></IconButton>
        </div>
    )
}

const engines_container = document.getElementById('engines_list');
const engines_root = createRoot(engines_container);
engines_root.render(<AppQueries></AppQueries>);
It exists only to show the user that the server is currently busy and needs to wait a bit. It also controls the display of messages about success or failure when the server is running.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import CircularProgress from '@mui/material/CircularProgress';
import Backdrop from '@mui/material/Backdrop';

export function Msg(msg, status){
    const msg_container = document.getElementById('msg');
    msg_container.classList.remove('hidden')
    const msgText_container = document.getElementById('msg-text');
    msgText_container.querySelector('.MuiAlert-message').innerText = msg
}

export function CloseMsg(){
    const msg_container = document.getElementById('msg');
    msg_container.classList.add('hidden')
    const msgText_container = document.getElementById('msg-text');
    msgText_container.querySelector('.MuiAlert-message').innerText = ''
}

// Show up a waiter
export function Wait(){
    const waiter_container = document.getElementById('waiter');
    waiter_container.classList.remove('hidden')
    const header = document.getElementById('header');
    header.style.zIndex = 0
}

// Hide waiter and shop up a status message
export function StopWait(msg, status){
    const waiter_container = document.getElementById('waiter');
    waiter_container.classList.add('hidden')
    const header = document.getElementById('header');
    header.style.zIndex = 1100

    Msg(msg, status)
}

export default function Waiter(){
    return (
        <Backdrop
        sx={{ color: '#fff', zIndex: 2000 }}
        open={true}
        >
            <CircularProgress color='inherit' />
        </Backdrop>
    )
}

const waiter_container = document.getElementById('waiter');
const waiter_root = createRoot(waiter_container);
waiter_root.render(<Waiter></Waiter>)
This file is used to prepare (draw) a certain block to be filled with information about the results of the server's work. It is controlled via the Waiter component.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import Alert from '@mui/material/Alert';
import { CloseMsg } from './Waiter';

export default function Msg(){
    return (
        <Alert
        id="msg-text"
        onClose={CloseMsg}
        severity='error'
        variant="outlined"
        sx={{ width: '100%' }}
        >
        </Alert>
    )
}

const msg_container = document.getElementById('msg');
const msg_root = createRoot(msg_container);
msg_root.render(<Msg/>)
All that remains is to connect all these components to index.js:
import Header from './components/Header';
import Footer from './components/Footer';
import AppSettings from './components/AppSettings';
import AppUtils from './components/AppUtils';
import AppActions from './components/AppActions';
import AppQueries from './components/AppQueries';
import Msg from './components/Msg';

Other pages and website’s sections.

I won't cover pages like about and contacts in such detail. Why?
These are general (I would even say standard) pages. And basically they will be static, there will be no react. They do not affect the main functionality of the site in any way. And simply, what's the point of showing what I wrote there? Or what's more important, what to tell? What font do I use, or what indents do I make?) That's it.

Conclusion

In this article, I told and showed how you can make the frontend part of the site using React and Django + MaterialUI, so as not to reinvent the wheel. TailwindCSS, to gain maximum flexibility in styling page elements (okay, so as not to get into CSS files :)).
In general, the frontend has always been the hardest part of development for me, well, it's not my thing. Making something functional and working - yes, I can do that. But making it beautiful and stylish - that's where I end.
You probably know this analogy of the frontend and the backend.
PLACEHOLDER
Well, for me, it's the opposite. In any case, we're done with the hardest part, and it will only get easier. We'll add an interactive tutorial, support for several languages, a backend in the end, and user authentication.
If you skipped all of the above and just want a ready-to-use solution, here it is. An archive with preconfigured folder structure and precalculated dependencies. All you need to do is set up a virtual environment (install all required Python packages) for the downloaded folder and install the required NPM packages in the Frontend app.

heart
0
3 connected dots
0

Used termins

Related questions

Similar articles

Interactive web development tutorial on website | Series SearchResultParser p. 3

Clock
29.08.2024
An eye
192
Hearts
0
3 connected dots
0
In this article you will understand how to add a web tutorial on a website for guests using React components. With the ability to define to which elements hints will …

A new project, codename: SearchResultParser | Tim the Webmaster

Clock
16.07.2024
An eye
215
Hearts
4
3 connected dots
0
I will be busy developing a new project. His name is SearchResultParser. Its essence is to parse data from the search results of various search engines, such as google, youtube, …