Долгое вступление и установка UI библиотеки + tailwind Хороший дизайн сайта это уже половина дела. Ведь, если подумать, только его работа и видна. Вся та работа, которая совершается на сервере для конечного пользователя не важна. Важно то, что в итоге этой работы было совершенно нечто полезное для него и при этом функционал сайта легко читался и был не вырвиглазным.
После прочтения данной статьи у тебя получится что-то вроде этого.
Десктопная версия
Мобильная версия
Я на своём опыте знаю, что дизайн это мастхев в наши дни. А для разработчика ещё важнее то, как этот дизайн делать и улучшать. И скажу я тебе мой дорогой читатель, заниматься этим на чистом JS и CSS это та ещё задачка.
Взять к примеру этот сайт. Он написан на чистом JS(ну окей, ещё и jQuery) и CSS. Поддерживать его тот ещё гемор, а добавить какой-нибудь новый компонент(типа приближение изображения, которое я до сих пор не сделал) вообще подвиг. Поэтому, в этом проекте (SearchResultParser) я буду использовать уже готовую UI библиотеку.
Я буду использовать MaterialUI + tailwindcss для более гибкой настройки стилей сайта, без необходимости залазить в CSS файлы. И будем использовать axios библиотеку для общения с сервером.
Итак, установим их:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
npm install @mui/material @mui/icons-material/ @emotion/react @emotion/styled tailwindcss
TailewindCSS нужно ещё настроить для его работы. Это создаст настроечный файл для него:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
npx tailwindcss init
В Frontend/tailwind.config.js вставьте следующий код.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
module .exports = {
content : [
"./src/**/*.{js,jsx,ts,tsx}" ,
"./templates/**/*.html" ,
],
theme : {
extend : {},
},
plugins : [],
}
В нём описано, к каким файлам применять данное расширение.
Также не забудь создать входной и выходной файл. В моём случае первый называется index.css , а другой zero.css . Первый в директории Frontend/src , другой в Frontend/static/css . Во входном файле достаточно вставить несколько директив:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
И теперь, чтобы стили были применены необходимо запускать следующую команду каждый раз, когда изменяем файлы в которых используем tailwindcss, будь то html или js. Можно добавить —watch чтобы не перезапускать её каждый раз.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
npx tailwindcss -i .\src\index.css -o .\static\css\index.css --watch
TailwindCSS установлен и настроен. Теперь настроим axios . Тут всё довольно просто в файле package.json добавь следующую строку:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
...
"keywords" : [],
"author" : "" ,
"proxy" : "http://localhost:8000" ,
"license" : "ISC" ,
"description" : "" ,
...
Настройка маршрутов и представлений django Скажу сразу, у меня не будет много маршрутов. Ибо, этот сайт это в первую очередь приложение, их ещё называют
SAAS . У моего
saas будут следующие маршруты/представления:
main : страница главного приложения, пользователь большую часть времени будет проводить здесь.about : страница на которой я расскажу об этом проектеcontacts : страница с контактными данными, моимиВот, собственно говоря, и всё, теперь добавим эти представления и маршруты к ним. Добавим маршруты, в Frontend/urls.py :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 не ругался:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
npm run tailwind -i ./src/index.css -o ./static/css/zero.css –watch
Флаг -i для входного файла
Флаг -o для выходного файла
Также нужно указать аргумент —watch , чтобы не запускать данную команду каждый раз, когда начнём использовать новые стили. Для компиляции и генерации JS, React:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
npm run dev
Здесь, мы запускаем ранее записанный скрипт в Frontend/package.json . Можно конечно и без скрипта, вот так:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
npm run webpack –mode development –watch
Очень важно чтобы ты понимал, для выполнения работы этими скриптами требуется время. Поэтому, иногда при обновлении страницы стили и скрипты не обновятся, и будет возникать много вопросов. И это значит, сначала смотрим, была ли генерация успешна, а потом идём проверять сайт. Остаётся только запустить Django-сервер, открыть вкладку и начать писать код.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
python ./manage.py runserver
Работа с django шаблонами Создадим базовый шаблон, base.html в Frontend/templates/Frontend . Откроем его в текстовом редакторе и вставим следующий код:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
{% 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 и прочим медиафайлам на нашем сервере.
Этот файл, как я называю их, базовики, эти шаблоны не рендерятся на прямую, их основная роль это быть скелетом/основанием/базой для других шаблонов. Например, этот сайт, на котором размещается эта статья, имеет следующие базовики:
base.html (Базовый интерфейс) base_post.html (Основа для любых постов) base_article.html (Основа для статей, как этой) base_post_list.html (Основа для пагинаторов )И чтобы шаблон, который унаследует базовика, мог его модернизировать и добавлять что-то свою нужно добавить специальные блоки. В данном базовике их 4:
head (Для меты тегов, стилей, и начальных скриптов) header (Для модификации главного меню) main (Для разного конента) scripts (Только для скриптов)Все эти блоки выглядят примерно так:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
{% block main %}
{% endblock %}
Теперь, когда мы разобрались с тем как данный шаблон устроен нужно сделать так, чтобы этот шаблон унаследовали следующие шаблоны:
about.html contacts.html app.htmlШаблон app.html :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
{% 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
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
{% 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:
Header.js (Шапка и меню нашего сайта) Footer.js (Футер сайта) MobileAppBar.js (Шапка и меню только для мобильной версии) LangSwitch.js (Переключатель языка)Начнём с самого сложного элемента нашего сайта, header.js , это его шапка. Код достаточно объёмен, но по сути своей он берёт отрендереную django информацию и формирует из неё либо горизонтальные (Десктопная версия), либо вертикальные (Мобильная версия) кнопки. Вот и всё.
Ну и если это мобильная версия, он эти кнопки оборачивает в боковое меню. Потому что мне оно больше всего нравится. А вот и код:
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
import Header from './components/Header' ;
import Footer from './components/Footer' ;
Верстка главной страницы и её компонентов Процесс работы пользователя с приложением Итак, мы переходим к самой тяжёлой части этой статьи. По крайней мере, она самая большая. Я даже думал разделить эту статью, но не сделал этого по причине потери целостности. Как вообще будет выглядеть процесс работы пользователя с приложением?
Пользователь открывает сайт.
Нажимает добавить, плюсик.
Выбирает необходимые движки.
Заполняет поля.
Настраивает парсер.
Запускает его в работу.
В качестве результата работы, пользователь получит ссылку на скачиваемый файл.
Django-шаблон приложения, app.html А теперь к приложению и коду. Давай немного изменим шаблон app.html чтобы можно было легко с ним работать из реакта.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
{% 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 части + ещё два компонента:
AppSettings.js AppUtils.js AppActions.js AppQueries.js Waiter.js Msg.jsСоздай их в src/components и перейдём к их разбору.
Компонент AppSettings.js . Это просто группа переключателей с чекбоксами для настройки работы парсера. Нужно отметить, что выбранные данные сохраняются в так называемом атрибуте data . Дабы дальше можно было легко их достать из другого приложения, AppActions .
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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' ;
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 );
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 . Он реализован при помощи связки,
кнопка →
нижний слайдер →
модальное окно . И на каждом из этапов будут делаться запросы на сервер, чтобы получить необходимые пресеты.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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" ;
function onUtilsContentInfo (rec ){
var uid = 0
Wait ()
axios.get (`/api/presets/${uid}` )
.then (response => {
StopWait ('Successfully obtain info about preset.' , 'success' )
})
.catch (error => {
StopWait ('Cant obtain info about preset. ' + 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">
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>
);
}
function onUtilsContentUpload (rec ){
var uid = 0
Wait ()
axios.get (`/api/presets/${uid}` )
.then (response => {
StopWait ('Successfully apply preset for user.' , 'success' )
})
.catch (error => {
StopWait ('Cant apply preset for user. ' + error , 'error' )
});
}
function onUtilsContentDelete (rec ){
var uid = 0
axios.delete (`/api/presets/${uid}` )
.then (data => {
StopWait ('Successfully delete a preset.' , 'success' )
Wait ()
axios.get ('/api/presets/' )
.then (response => {
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>
);
}
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 });
}
function TrendingContent ( props ){
const [isModal, setModal] = React .useState (false );
Wait ()
axios.get ('/api/popular-presets/' )
.then (response => {
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>
)
}
function OwnSavesContent (props ){
const [isModal, setModal] = React .useState (false );
Wait ()
axios.get ('/api/presets/' )
.then (response => {
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>
)
}
function ConsoleContent (props ){
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 );
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 . Состоит только из двух кнопок, сохранения пресета и запуск парсинга. Здесь же, мы собираем данные из других приложений, здесь же проверяем их на правильность и здесь же отправляем их на сервер.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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" ;
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 });
}
function checkIfValidPreset (){
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
}
}
function SaveRequest (req ){
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>
)
}
}
function StartParsingRequest (req ){
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 => {
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
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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' ;
function onDeleteQuery (event ){
var id = event.currentTarget .dataset .id
var elsToDelete = document .querySelectorAll ('#' +id)
elsToDelete.forEach ((el)=> {
el.remove ()
})
}
function onAddQuery (event ){
var uid = 'uid_' +Math .random ().toString (16 ).slice (2 )
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
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++){
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)
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 . Существует только для того, чтобы показывать пользователю, что сервер сейчас занят работай и нужно немного подождать. Он же управляет отображением сообщений об успехе или неудаче при работе сервера.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 = ''
}
export function Wait (){
const waiter_container = document .getElementById ('waiter' );
waiter_container.classList .remove ('hidden' )
const header = document .getElementById ('header' );
header.style .zIndex = 0
}
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 компонент.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 :
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL
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 файлы :)).
Вообще, фронтенд для меня всегда был самой тяжёлой частью в разработке, ну не моё это. Сделать что-то функциональное и работающее это да, это я могу. Но сделать это красивым и стильным, тут я кончаюсь. Ты наверняка знаешь эту аналогию фронтенда и бэкенда.
Так вот, у меня наоборот. В любом случае, с самой тяжёлой частью мы покончили и дальше будет только легче. Добавим интерактивный туториал, поддержку нескольких языков, бэкенд в конце концов и аутентификацию пользователей.
Если ты пропустил всё что было выше и хочешь просто готовое-к-использованию решение, то ты можешь
скачать его здесь . Это архив с настроенной структурой директорий и готовыми зависимостями. Всё что тебе остаётся сделать, так это скачать, создать виртуальное окружение, установить все необходимые python и npm пакеты.