Создание базового макета сайта и его дизайна

Часы
16.08.2024
Глазик
215
Сердечки
0
Соединённые точки
0
Соединённые точки
0
Соединённые точки
0

Долгое вступление и установка UI библиотеки + tailwind

Хороший дизайн сайта это уже половина дела. Ведь, если подумать, только его работа и видна. Вся та работа, которая совершается на сервере для конечного пользователя не важна. Важно то, что в итоге этой работы было совершенно нечто полезное для него и при этом функционал сайта легко читался и был не вырвиглазным.
После прочтения данной статьи у тебя получится что-то вроде этого.
SearchResultParser десктопная версия
Десктопная версия
SearchResultParser мобильная версия
Мобильная версия
Я на своём опыте знаю, что дизайн это мастхев в наши дни. А для разработчика ещё важнее то, как этот дизайн делать и улучшать. И скажу я тебе мой дорогой читатель, заниматься этим на чистом JS и CSS это та ещё задачка.
Взять к примеру этот сайт. Он написан на чистом JS(ну окей, ещё и jQuery) и CSS. Поддерживать его тот ещё гемор, а добавить какой-нибудь новый компонент(типа приближение изображения, которое я до сих пор не сделал) вообще подвиг. Поэтому, в этом проекте (SearchResultParser) я буду использовать уже готовую UI библиотеку.
Я буду использовать MaterialUI + tailwindcss для более гибкой настройки стилей сайта, без необходимости залазить в CSS файлы. И будем использовать axios библиотеку для общения с сервером.
Итак, установим их:
npm install @mui/material @mui/icons-material/ @emotion/react @emotion/styled tailwindcss
TailewindCSS нужно ещё настроить для его работы. Это создаст настроечный файл для него:
npx tailwindcss init
В Frontend/tailwind.config.js вставьте следующий код.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./templates/**/*.html",
],
theme: {
extend: {},
},
plugins: [],
}
В нём описано, к каким файлам применять данное расширение.
Также не забудь создать входной и выходной файл. В моём случае первый называется index.css, а другой zero.css. Первый в директории Frontend/src, другой в Frontend/static/css. Во входном файле достаточно вставить несколько директив:
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
И теперь, чтобы стили были применены необходимо запускать следующую команду каждый раз, когда изменяем файлы в которых используем tailwindcss, будь то html или js. Можно добавить —watch чтобы не перезапускать её каждый раз.
npx tailwindcss -i .\src\index.css -o .\static\css\index.css --watch
TailwindCSS установлен и настроен. Теперь настроим axios. Тут всё довольно просто в файле package.json добавь следующую строку:
...
"keywords": [],
"author": "",
"proxy": "http://localhost:8000",
"license": "ISC",
"description": "",
...

Настройка маршрутов и представлений django

Скажу сразу, у меня не будет много маршрутов. Ибо, этот сайт это в первую очередь приложение, их ещё называют SAAS. У моего saas будут следующие маршруты/представления:
  1. main: страница главного приложения, пользователь большую часть времени будет проводить здесь.
  2. about: страница на которой я расскажу об этом проекте
  3. contacts: страница с контактными данными, моими
Вот, собственно говоря, и всё, теперь добавим эти представления и маршруты к ним. Добавим маршруты, в 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'),
]
Напишем представления( в файле views.py), пока просто для галочки, чтобы django не ругался:
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')
Это наша база. Больше, в этой статье, мы не вернёмся к django. Будем верстать, верстать и ещё раз верстать.

Верстка базового макета и его компонентов

Подготовка к работе, запуск сервера

Чтобы начать верстать и видеть результаты нашей работы, нужно будет запустить несколько команд в терминале. Во-первых, для того чтобы TailwindCSS мог сгенерировать для нас стили. Во-вторых, чтобы уже React успевал собирать компоненты.
Для генерации CSS стилей, tailwindcss:
npm run tailwind -i ./src/index.css -o ./static/css/zero.css –watch
Флаг -i для входного файла
Флаг -o для выходного файла
Также нужно указать аргумент —watch, чтобы не запускать данную команду каждый раз, когда начнём использовать новые стили. Для компиляции и генерации JS, React:
npm run dev
Здесь, мы запускаем ранее записанный скрипт в Frontend/package.json. Можно конечно и без скрипта, вот так:
npm run webpack –mode development –watch
Очень важно чтобы ты понимал, для выполнения работы этими скриптами требуется время. Поэтому, иногда при обновлении страницы стили и скрипты не обновятся, и будет возникать много вопросов. И это значит, сначала смотрим, была ли генерация успешна, а потом идём проверять сайт.
Остаётся только запустить Django-сервер, открыть вкладку и начать писать код.
python ./manage.py runserver

Работа с django шаблонами

Создадим базовый шаблон, base.html в Frontend/templates/Frontend. Откроем его в текстовом редакторе и вставим следующий код:

{% 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">
<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>
{% block header %}
{% endblock %}
<div data-type="profile"><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>
Сначала, мы загружаем значение глобальной переменной static, чтобы иметь доступ к CSS, JS, JPEG, PNG, SVG и прочим медиафайлам на нашем сервере.
Этот файл, как я называю их, базовики, эти шаблоны не рендерятся на прямую, их основная роль это быть скелетом/основанием/базой для других шаблонов. Например, этот сайт, на котором размещается эта статья, имеет следующие базовики:
  1. base.html (Базовый интерфейс)
  2. base_post.html (Основа для любых постов)
  3. base_article.html (Основа для статей, как этой)
  4. base_post_list.html (Основа для пагинаторов )
И чтобы шаблон, который унаследует базовика, мог его модернизировать и добавлять что-то свою нужно добавить специальные блоки. В данном базовике их 4:
  1. head (Для меты тегов, стилей, и начальных скриптов)
  2. header (Для модификации главного меню)
  3. main (Для разного конента)
  4. scripts (Только для скриптов)
Все эти блоки выглядят примерно так:
{% block main %}
{% endblock %}
Теперь, когда мы разобрались с тем как данный шаблон устроен нужно сделать так, чтобы этот шаблон унаследовали следующие шаблоны:
  1. about.html
  2. contacts.html
  3. app.html
Шаблон app.html:
{% 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 %}
{% endblock %}

{% block main %}
{% endblock %}

{% block scripts %}
{% endblock %}

Вот пример шаблона about.html

{% 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 %}

{% endblock %}

{% block main %}
{% endblock %}

{% block scripts %}
{% endblock %}
Шаблон для contacts.html идентичен выше написанному шаблону, с той лишь разницей, что у них отличаются тайтлы, канонический адрес и описание.
Функция url в шаблоне принимает значения переменной name. Которую, мы заполняли в Frontend/urls.py
Одними шаблонами сыт не будешь, нужен React. Причём использовать его нужно окуратно. В чём дело? Ты мог заметить, что у меня есть специальные элементы с айдишниками header и footer и рядом с ними их аналоги, meta-header и meta-footer. Почему я так сделал? Почему бы не отрендерить всё в одном блоке через react ?
Причиной этого является то, как react и django рендерят страницы. Если react отдаёт рендеринг пользовательской машине CSR, то django занимается этим сам, на сервере SSR. Ну и что? Какая разница, кто что рендерит. Главное рендерит.
Разница всё-таки есть. И она особенно заметна для поисковиков. Поисковой робот, краулер, зайдёт на страницу отрендеренной django и сможет увидеть все ссылки и контент сайта. Но если всё тот же краулер зайдёт на страницу отрендеренной React-ом, он ничего не увидит, посчитает страницу либо бесполезной, либо не доделанной и уйдёт. То есть, для SEO это имеет критическое значение.
Хоть гугл уже и может самостоятельно рендерить страницы с JS, я бы на это не расчитывал, и положился бы на статический контент.
И поэтому у меня есть это meta-* элементы. Они отрисовываются django и доступны поисковикам. Реакт эти элементы подхватывает и обрабатывает. Закончили с HTML перейдём к JS и React коду.

Работа с React элементами

Создадим необходимые элементы и файлы. Нам их потребуется 4:
  1. Header.js (Шапка и меню нашего сайта)
  2. Footer.js (Футер сайта)
  3. MobileAppBar.js (Шапка и меню только для мобильной версии)
  4. LangSwitch.js (Переключатель языка)
Начнём с самого сложного элемента нашего сайта, header.js, это его шапка. Код достаточно объёмен, но по сути своей он берёт отрендереную django информацию и формирует из неё либо горизонтальные (Десктопная версия), либо вертикальные (Мобильная версия) кнопки. Вот и всё.
Ну и если это мобильная версия, он эти кнопки оборачивает в боковое меню. Потому что мне оно больше всего нравится. А вот и код:
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');
if (container1){
const root1 = createRoot(container1);
root1.render(<Header></Header>);
}
В этом компоненте мы используем два других, это LangSwitch и MenuAppBar. В 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>
);
}

В 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>
);
}

Компонент LangSwitch, не такой уж и большой. Большую часть пространства занимает настройка SVG изображений. Сейчас он, правда, не рабочий, то есть языки не переключает. Но это потому что мы ещё не настроили django для этого. Это будет в другой раз. Ну а пока имеем просто рабочий переключатель.
Осталось лишь рассмотреть компонент Footer в 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');
if (container){
const root = createRoot(container);
root.render(<Footer></Footer>);
}
Изначально, я планировал добавить туда много-много ссылок, но мне стало лень, да и зачем там ссылки? Только лишняя нагрузка на восприятие пользователя. Поэтому оставил лишь одну ссылку на себя любимого)
Ну и конечно не забываем подключить наши компоненты Header и Footer в index.js:
import Header from './components/Header';
import Footer from './components/Footer';

Верстка главной страницы и её компонентов

Процесс работы пользователя с приложением

Итак, мы переходим к самой тяжёлой части этой статьи. По крайней мере, она самая большая. Я даже думал разделить эту статью, но не сделал этого по причине потери целостности. Как вообще будет выглядеть процесс работы пользователя с приложением?
Шаг 1: открыть вебсайт
Пользователь открывает сайт.
шаг 2: нажать кнопку добавить движок
Нажимает добавить, плюсик.
шаг 3: выбрать движок
Выбирает необходимые движки.
шаг 4: заполнить поля
Заполняет поля.
шаг 5: настройка парсера
Настраивает парсер.
шаг 6: начать парсинг
Запускает его в работу.
шаг 7: скачать файл
В качестве результата работы, пользователь получит ссылку на скачиваемый файл.

Django-шаблон приложения, app.html

А теперь к приложению и коду. Давай немного изменим шаблон app.html чтобы можно было легко с ним работать из реакта.
{% 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="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">
<div id="popover-tutorial"></div>
{# 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 %}
Из шаблона можно заметить что моё приложение разбито на несколько независимых частей. Это настройки(id=”app_settings”), таблица запросов и движков(id=”engines” + id=”queries”), утилиты(id=”app_utils”) и действия (id=”app_actions”).
Хочу отметить блок meta-engines. Здесь я написал вручную все движки которые собираюсь парсить, но в будущем этот блок будет заполняться django (джангом?). Просто в будущем я возможно захочу добавить другие движки или убрать старые и лучше делать это на сервере.

React компоненты приложения

Приложение разбито на 4 части + ещё два компонента:
  1. AppSettings.js
  2. AppUtils.js
  3. AppActions.js
  4. AppQueries.js
  5. Waiter.js
  6. Msg.js
Создай их в src/components и перейдём к их разбору.
Компонент AppSettings.js. Это просто группа переключателей с чекбоксами для настройки работы парсера. Нужно отметить, что выбранные данные сохраняются в так называемом атрибуте data. Дабы дальше можно было легко их достать из другого приложения, 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');
if (settings_container){
const settings_root = createRoot(settings_container);
settings_root.render(<AppSettings></AppSettings>);
}
Компонент AppUtils.js. Он реализован при помощи связки, кнопканижний слайдермодальное окно. И на каждом из этапов будут делаться запросы на сервер, чтобы получить необходимые пресеты.
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');
if (utils_container){
const utils_root = createRoot(utils_container);
utils_root.render(<AppUtils></AppUtils>);
}
Компонент AppActions.js. Состоит только из двух кнопок, сохранения пресета и запуск парсинга. Здесь же, мы собираем данные из других приложений, здесь же проверяем их на правильность и здесь же отправляем их на сервер.
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');
if (actions_container){
const actions_root = createRoot(actions_container);
actions_root.render(<AppActions></AppActions>);
}
Компонент AppQueries.js. Получение доступных к парсингу движков и создание таблицы движок-запрос. Изначально я планировал его сделать таким образом, чтобы пользователь добавлял движок, потом добавлял к нему столько запросов сколько бы ему хотелось. Но потом я понял что можно это всё реализовывать гораздо проще и через одну кнопку.
И ещё, тебе потребуется иконки для всех движков. Нужно будет скачать этот архив и распаковать его в папке Frontend/static/img
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');
if (engines_container){
const engines_root = createRoot(engines_container);
engines_root.render(<AppQueries></AppQueries>);
}
Компонент Waiter.js. Существует только для того, чтобы показывать пользователю, что сервер сейчас занят работай и нужно немного подождать. Он же управляет отображением сообщений об успехе или неудаче при работе сервера.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import CircularProgress from '@mui/material/CircularProgress';
import Backdrop from '@mui/material/Backdrop';
import Message from './Msg'

export function Msg(msg, status='info'){
var msg_container = document.getElementById('msg');
msg_container.dataset.status = status
const msg_root = createRoot(msg_container);
msg_root.render(<Message status={msg_container.dataset.status} message={msg}/>)
msg_container.classList.remove('hidden')
}

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>)
Компонент Msg.js. В этом файле идёт подготовка(отрисовка) определённого блока к тому, чтобы быть заполненным информацией о результатах работы сервера. Управляется через Waiter компонент.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import Alert from '@mui/material/Alert';
import { CloseMsg } from './Waiter';

export default function Message(props){
return (
<Alert
id="msg-text"
onClose={CloseMsg}
severity={props.status}
variant="filled"
sx={{ width: '100%' }}
>
{props.message}
</Alert>
)
}

const msg_container = document.getElementById('msg');
const msg_root = createRoot(msg_container);
msg_root.render(<Message status={msg_container.dataset.status} message="" />)
Осталось только подключить все эти компоненты в 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';

Другие страницы и разделы сайта.

Такие страницы как, about и contacts я не буду так же подробно освещать. Почему?
Это общие(я бы даже сказал, стандартные) страницы. И в основе своей они будут статичный, там не будет никакого реакта. Они никак не влияют на основной функционал сайта. И просто, смысл показывать что я там написал? Или что важнее, что рассказывать? То какой шрифт я использую, или какие отступы делаю?) Вот.

Выводы и заключение

В этой статье я рассказал и показал как можно сделать фронтенд часть сайта, используя React и Django + MaterialUI, чтобы не изобретать колёса заново. TailwindCSS, чтобы приобрести максимальную гибкость в стилизации элементов страницы (ну ладно, чтобы не лезть в css файлы :)).
Вообще, фронтенд для меня всегда был самой тяжёлой частью в разработке, ну не моё это. Сделать что-то функциональное и работающее это да, это я могу. Но сделать это красивым и стильным, тут я кончаюсь. Ты наверняка знаешь эту аналогию фронтенда и бэкенда.
PLACEHOLDER
Так вот, у меня наоборот. В любом случае, с самой тяжёлой частью мы покончили и дальше будет только легче. Добавим интерактивный туториал, поддержку нескольких языков, бэкенд в конце концов и аутентификацию пользователей.
Если ты пропустил всё что было выше и хочешь просто готовое-к-использованию решение, то ты можешь скачать его здесь. Это архив с настроенной структурой директорий и готовыми зависимостями. Всё что тебе остаётся сделать, так это скачать, создать виртуальное окружение, установить все необходимые python и npm пакеты.

Комментарии

(0)
captcha
Отправить
Сейчас тут пусто. Буть первым (o゚v゚)ノ

Использованные термины