Долгое вступление и установка UI библиотеки + tailwind
Хороший дизайн сайта это уже половина дела. Ведь, если подумать, только его работа и видна. Вся та работа, которая совершается на сервере для конечного пользователя не важна. Важно то, что в итоге этой работы было совершенно нечто полезное для него и при этом функционал сайта легко читался и был не вырвиглазным.
После протчения данной статьи у тебя получится что-то вроде этого.
Десктопная версия
Я на своём опыте знаю, что дизайн это мастхев в наши дни. А для разработчика ещё важнее то, как этот дизайн делать и улучшать. И скажу я тебе мой дорогой читатель, заниматься этим на чистом JS и CSS это та ещё задачка.
Взять к примеру этот сайт. Он написан на чистом JS(ну окей, ещё и jQuery) и CSS. Поддерживать его тот ещё гемор, а добавить какой-нибудь новый компонент(типа приближение изображения, которое я до сих пор не сделал) вообще подвиг. Поэтому, в этом проекте (SearchResultParser) я буду использовать уже готовую UI библиотеку.
Я буду использовать MaterialUI + tailwindcss для более гибкой настройки стилей сайта, без необходимости залазить в CSS файлы. И будем использовать axios библиотеку для общения с сервером.
В нём описано, к каким файлам применять данное расширение.
Также не забудь создать входной и выходной файл. В моём случае первый называется index.css, а другой zero.css. Первый в директории src, другой в static/css.
Во входном файле достаточно вставить несколько директив:
И теперь, чтобы стили были применены необходимо запускать следующую команду каждый раз, когда изменяем файлы в которых используем tailwindcss, будь то html или js. Можно добавить —watch чтобы не перезапускать её каждый раз.
Скажу сразу, у меня не будет много маршрутов. Ибо, этот сайт это в первую очередь приложение, их ещё называют SAAS. У моего saas будут следующие маршруты/представления:
main: страница главного приложения, пользователь большую часть времени будет проводить здесь.
about: страница на которой я расскажу об этом проекте
contacts: страница с контактными данными, моими
Вот, собственно говоря, и всё, теперь добавим эти представления и маршруты к ним. Добавим маршруты, в Frontend/urls.py:
Это наша база. Больше, в этой статье, мы не вернёмся к 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
Здесь, мы запускаем ранее записанный скрипт в package.json. Можно конечно и без скрипта, вот так:
npm run webpack –mode development –watch
Остаётся только запустить Django-сервер, открыть вкладку и начать писать код.
./manage.py runserver
Работа с django шаблонами
Создадим базовый шаблон, base.html в templates/Frontend. Откроем его в текстовом редакторе и вставим следующий код:
Сначала, мы загружаем значение глобальной переменной static, чтобы иметь доступ к CSS, JS, JPEG, PNG, SVG и прочим медиафайлам на нашем сервере.
Этот файл, как я называю их, базовики, эти шаблоны не рендерятся на прямую, их основная роль это быть скелетом/основанием/базой для других шаблонов. Например, этот сайт, на котором размещается эта статья, имеет следующие базовики:
base.html (Базовый интерфейс)
base_post.html (Основа для любых постов)
base_article.html (Основа для статей, как этой)
base_post_list.html (Основа для пагинаторов )
И чтобы шаблон, который унаследует базовика, мог его модернизировать и добавлять что-то свою нужно добавить специальные блоки. В данном базовике их 4:
head (Для метатегов, стилей, и начальных скриптов)
header (Для модификации главного меню)
main (Для разного конента)
scripts (Только для скриптов)
Все эти блоки выглядят примерно так:
{% block main %}
{% endblock %}
Теперь, когда мы разобрались с тем как данный шаблон устроен нужно сделать так, чтобы этот шаблон унаследовали следующие шаблоны:
Шаблон для contacts.html идентичен выше написанному шаблону, с той лишь разницей, что у них отличаются тайтлы, канонический адрес и описание.
Одними шаблонами сыт не будешь, нужен React. Причём использовать его нужно окуратно. В чём дело? Ты мог заметить, что у меня есть специальные элементы с айдишниками header и footer и рядом с ними их аналоги, meta-header и meta-footer. Почему я так сделал? Почему бы не отрендерить всё в одном блоке через react ?
Причиной этого является то, как react и django рендерят страницы. Если react отдаёт рендеринг пользовательской машине CSR, то django занимается этим сам, на сервере SSR.
Ну и что? Какая разница, кто что рендерит. Главное рендерит.
Разница всё-таки есть. И она особенно заметна для поисковиков. Поисковой робот, краулер, зайдёт на страницу отрендеренной django и сможет увидеть все ссылки и контент сайта. Но если всё тот же краулер зайдёт на страницу отрендеренной React-ом, он ничего не увидит, посчитает страницу либо бесполезной, либо не доделанной и уйдёт.
То есть, для SEO это имеет критическое значение.
И поэтому у меня есть это meta-* элементы. Они отрисовываются django и доступны поисковикам. Реакт эти элементы подхватывает и обрабатывает.
Закончили с HTML перейдём к JS и React коду.
Работа с React элементами
Создадим необходимые элементы и файлы. Нам их потребуется 4:
Header.js (Шапка и меню нашего сайта)
Footer.js (Футер сайта)
MobileAppBar.js (Шапка и меню только для мобильной версии)
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()
}
else if (btn.dataset.type == 'tutorial' ){
btns.push()
}
else{
btns.push()
}
}
if (IS_MOBILE){
const [open, setOpen] = React.useState(false);
const toggleSideMenu = (newOpen) => () => {
setOpen(newOpen)
}
return (
В этом компоненте мы используем два других, это 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 (
);
}
Компонент 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 (
{text_in}
);
}
const container = document.getElementById('ref_to_place');
const root = createRoot(container);
root.render();
Изначально, я планировал добавить туда много-много ссылок, но мне стало лень, да и зачем там ссылки? Только лишняя нагрузка на восприятие пользователя. Поэтому оставил лишь одну ссылку на себя любимого)
Ну и конечно не забываем подключить наши компоненты Header и Footer в index.js:
import Header from './components/Header';
import Footer from './components/Footer';
Верстка главной страницы и её компонентов
Процесс работы пользователя с приложением
Итак, мы переходим к самой тяжёлой части этой статьи. По крайней мере, она самая большая. Я даже думал разделить эту статью, но не сделал этого по причине потери целостности. Как вообще будет выглядеть процесс работы пользователя с приложением?
Пользователь открывает сайт.
Нажимает добавить, плюсик.
Выбирает необходимые движки.
Заполняет поля.
Настраивает парсер.
Запускает его в работу.
В качестве результата работы, пользователь получит ссылку на скачиваемый файл.
Django-шаблон приложения, app.html
А теперь к приложению и коду. Давай немного изменим шаблон app.html чтобы можно было легко с ним работать из реакта.
Из шаблона можно заметить что моё приложение разбито на несколько независимых частей. Это настройки(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
AppUtils.js
AppActions.js
AppQueries.js
Waiter.js
Msg.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 (
Export as:{
var root = document.getElementById('export-as')
root.dataset.export = ev.target.value
setExport(ev.target.value)
}}/>} label="exel" />
{
var root = document.getElementById('export-as')
root.dataset.export = ev.target.value
setExport(ev.target.value)
}}/>} label="csv" />
{
var root = document.getElementById('export-as')
root.dataset.export = ev.target.value
setExport(ev.target.value)
}}/>} label="json" />
Save:{
setTitle(checked)
}} />} label="title" />
{
setUrl(checked)
}} />} label="url" />
{
setDesr(checked)
}} />} label="description" />
Other:{
setVerb(checked)
const console = document.getElementById('console-button')
console.classList.toggle('hidden')
}} />} label="verbose" />
)
}
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 (
{/* Icon button is gonna be changed */}
{ isSettings ?
: }
Данный компонент реализован при помощи связки, кнопка→нижний слайдер → модальное окно. И на каждом из этапов будут делаться запросы на сервер, чтобы получить необходимые пресеты.
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(
Preset info
);
}
// 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(
Deleting preset
Are you sure ?
);
}
// 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 (
{setModal(false)}}
>
Trending presets
{/* Template to be rendered by Django*/}
)
}
// 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 (
{setModal(false)}}
>
Your presets
{/* Template to be rendered by Django */}
Состоит только из двух кнопок, сохранения пресета и запуск парсинга. Здесь же, мы собираем данные из других приложений, здесь же проверяем их на правильность и здесь же отправляем их на сервер.
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(
Saving preset
)
}
}
//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 (
{setModal(true); waitTillModalIsUp(SaveRequest, req)}} className='w-fit'>{setModal(false)}}
>
{StartParsingRequest(req)}} className='w-fit'>
)
}
const actions_container = document.getElementById('app_actions');
const actions_root = createRoot(actions_container);
actions_root.render();
Получение доступных к парсингу движков и создание таблицы движок-запрос. Изначально я планировал его сделать таким образом, чтобы пользователь добавлял движок, потом добавлял к нему столько запросов сколько бы ему хотелось. Но потом я понял что можно это всё реализовывать гораздо проще и через одну кнопку.
И ещё, тебе потребуется иконки для всех движков. Нужно будет скачать этот архив и распаковать его в папке 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(
)
}
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(
{
onAddQuery(event)
}}>
Существует только для того, чтобы показывать пользователю, что сервер сейчас занят работай и нужно немного подождать. Он же управляет отображением сообщений об успехе или неудаче при работе сервера.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import CircularProgress from '@mui/material/CircularProgress';
import Backdrop from '@mui/material/Backdrop';
export function Msg(msg, status){
const msg_container = document.getElementById('msg');
msg_container.classList.remove('hidden')
const msgText_container = document.getElementById('msg-text');
msgText_container.querySelector('.MuiAlert-message').innerText = msg
}
export function CloseMsg(){
const msg_container = document.getElementById('msg');
msg_container.classList.add('hidden')
const msgText_container = document.getElementById('msg-text');
msgText_container.querySelector('.MuiAlert-message').innerText = ''
}
// Show up a waiter
export function Wait(){
const waiter_container = document.getElementById('waiter');
waiter_container.classList.remove('hidden')
const header = document.getElementById('header');
header.style.zIndex = 0
}
// Hide waiter and shop up a status message
export function StopWait(msg, status){
const waiter_container = document.getElementById('waiter');
waiter_container.classList.add('hidden')
const header = document.getElementById('header');
header.style.zIndex = 1100
Msg(msg, status)
}
export default function Waiter(){
return (
)
}
const waiter_container = document.getElementById('waiter');
const waiter_root = createRoot(waiter_container);
waiter_root.render()
В этом файле идёт подготовка(отрисовка) определённого блока к тому, чтобы быть заполненным информацией о результатах работы сервера. Управляется через 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 Msg(){
return (
)
}
const msg_container = document.getElementById('msg');
const msg_root = createRoot(msg_container);
msg_root.render()
Осталось только подключить все эти компоненты в 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 пакеты.