3 горизонтальные линии, бургер
3 горизонтальные линии, бургер
3 горизонтальные линии, бургер
3 горизонтальные линии, бургер

3 горизонтальные линии, бургер
Удалить все
ЗАГРУЗКА ...

Содержание



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

    Часы
    16.08.2024
    /
    Часы
    02.10.2025
    /
    Часы
    18 минут
    Глазик
    1339
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0

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

    Хороший дизайн сайта это уже половина дела. Ведь, если подумать, только его работа и видна. Вся та работа, которая совершается на сервере для конечного пользователя не важна. Важно то, что в итоге этой работы было совершенно нечто полезное для него и при этом функционал сайта легко читался и был не вырвиглазным.
    После прочтения данной статьи у тебя получится что-то вроде этого.
    Десктопная версия
    Мобильная версия
    Я на своём опыте знаю, что дизайн это мастхев в наши дни. А для разработчика ещё важнее то, как этот дизайн делать и улучшать. И скажу я тебе мой дорогой читатель, заниматься этим на чистом 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';

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

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

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

    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 файлы :)).
    Вообще, фронтенд для меня всегда был самой тяжёлой частью в разработке, ну не моё это. Сделать что-то функциональное и работающее это да, это я могу. Но сделать это красивым и стильным, тут я кончаюсь. Ты наверняка знаешь эту аналогию фронтенда и бэкенда.
    Так вот, у меня наоборот. В любом случае, с самой тяжёлой частью мы покончили и дальше будет только легче. Добавим интерактивный туториал, поддержку нескольких языков, бэкенд в конце концов и аутентификацию пользователей.
    Если ты пропустил всё что было выше и хочешь просто готовое-к-использованию решение, то ты можешь скачать его здесь. Это архив с настроенной структурой директорий и готовыми зависимостями. Всё что тебе остаётся сделать, так это скачать, создать виртуальное окружение, установить все необходимые python и npm пакеты.

    Не забудь поделиться, лайкнуть и оставить комментарий)

    Комментарии

    (0)

    captcha
    Отправить
    ЗАГРУЗКА ...
    Сейчас тут пусто. Буть первым (o゚v゚)ノ

    Другое

    Похожие статьи


    Как (и можно ли) объединить React JS с Django проектом ч. 1

    Часы
    03.08.2024
    /
    Часы
    02.10.2025
    Глазик
    1266
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    В этой статье ты узнаешь как создать Django приложение и настроить его для работы с React JS чтобы получилось полноценное фулстак приложение. Так же в статье приведено видео-туториал и скачиваемые …

    Интерактивный туториал для сайта | Серия SearchResultParser ч. 3

    Часы
    29.08.2024
    /
    Часы
    02.10.2025
    Глазик
    495
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    В этой статье ты узнаешь как добавить туториал для новоприбывших пользователей используя React, с возможностью указывать к каким элементам относятся подсказки и количество этих подсказок, последовательно

    Django allauth интеграция на реакте. То как реализовать регистрацию и авторизацию на allauth ч. 4

    Часы
    31.01.2025
    /
    Часы
    02.10.2025
    Глазик
    610
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    Соединённые точки
    0
    В этой статье я опишу процесс интеграции allauth к django проекту. Весь фронтендом на react. Я настрою регистрацию и авторизацию пользователей через пароль, плюс подтверждение почты и возможностью восстановления пароля. …

    Как разместить django сайт на хостинг(или VPS) от reg.ru Полная инструкция

    Часы
    16.03.2025
    /
    Часы
    02.10.2025
    Глазик
    3044
    Сердечки
    0
    Соединённые точки
    0
    Соединённые точки
    2
    Соединённые точки
    0
    Как развернуть django-сайт на хостинге (или VPS) от reg.ru. Так же, как создать и настроить БД(в том числе и используя кластер в рег облаке). Настроим переадресацию на HTTPS, будет показано …

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


    Релевантные вопросы