Карта сайта и микроразметка

تم الإنشاء في: ٢١ نوفمبر ٢٠٢٤

تمت الإجابة باستخدام OpenAI 01 Preview بواسطة OpenAI01

سؤال

Мне нужно для своего проекта сделать карту сайта (sitemap) и микроразметку. Сделай это. Вот мой код:
import React, { useEffect } from "react"
import "../index.css"
import { Layout, theme } from "antd"
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"
import Preloader from "../Components/common/Preloader"
import ProtectedRoute from "../Components/hoc/ProtectedRoute"
import stores from "../stores/stores"
import { observer } from "mobx-react-lite" // Or "mobx-react"
import { withSuspenseLazy } from "../Components/hoc/withSuspenseLazy"
import AppHeader from "./AppHeader"
import AppSider from "./AppSider"
import logo from "../Image/logo_2.png"

// Ленивые загрузки компонентов
const MainPage = React.lazy(() => import("../Components/MainPage/MainPage"))
const CreatDB = React.lazy(() => import("../Components/CreatDB/CreatDB"))
const DB_get = React.lazy(() => import("../Components/DB_get/DB_get"))
const KAC = React.lazy(() => import("../Components/KAC/KAC"))
const Template = React.lazy(() => import("../Components/Templates/Template"))
const Codes = React.lazy(() => import("../Components/Codes/Codes"))
const Request = React.lazy(() => import("../Components/Request/Request"))
const Reviews = React.lazy(() => import("../Components/Reviews/Reviews"))
const Contacts = React.lazy(() => import("../Components/Contacts/Contacts"))
const SignUp = React.lazy(() => import("../Components/Auth/SignUp"))
const SignIn = React.lazy(() => import("../Components/Auth/SignIn"))
const Profile = React.lazy(() => import("../Components/Profile/Profile"))
const Search = React.lazy(() => import("../Components/Search/Search"))
const Exist = React.lazy(() => import("../Components/Auth/Exist"))
const Code_region = React.lazy(() => import("../Components/Codes/Code_region"))
const Notfound = React.lazy(() => import("../Components/Notfound/Notfound"))
const DeleteProfileConfirmation = React.lazy(() =>
import("../Components/Profile/DeleteProfileConfirmation")
)

const { Content, Footer } = Layout

const App = ({ router: Router = BrowserRouter }) => {
const { authStore } = stores

useEffect(() => {
const checkAuthAndMe = async () => {
if (localStorage.getItem("access_token")) {
await authStore.checkAuth()
await authStore.checkAuthMe()
}
}
checkAuthAndMe()
}, [authStore])

const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken()

return (
<Router>
<Layout className="layout">
<AppHeader />
<Content className="content">
<Layout className="layout">
<AppSider />
<Content className="content">
{authStore.isLoading && <Preloader />}
<Routes>
<Route
path="/"
element={<Navigate to="/main" />}
/>
<Route
path="/main"
element={withSuspenseLazy(MainPage)}
/>
<Route
path="/creat-db"
element={
<ProtectedRoute>{withSuspenseLazy(CreatDB)}</ProtectedRoute>
}
/>
<Route
path="/your_db"
element={
<ProtectedRoute>{withSuspenseLazy(DB_get)}</ProtectedRoute>
}
/>
<Route
path="/KAC"
element={
<ProtectedRoute>{withSuspenseLazy(KAC)}</ProtectedRoute>
}
/>
<Route
path="/template"
element={
<ProtectedRoute>
{withSuspenseLazy(Template)}
</ProtectedRoute>
}
/>
<Route
path="/codes"
element={
<ProtectedRoute>{withSuspenseLazy(Codes)}</ProtectedRoute>
}
/>
<Route
path="/request"
element={
<ProtectedRoute>{withSuspenseLazy(Request)}</ProtectedRoute>
}
/>
<Route
path="/reviews"
element={withSuspenseLazy(Reviews)}
/>
<Route
path="/contacts"
element={withSuspenseLazy(Contacts)}
/>
<Route
path="/search"
element={
<ProtectedRoute>{withSuspenseLazy(Search)}</ProtectedRoute>
}
/>
<Route
path="/register"
element={withSuspenseLazy(SignUp)}
/>
<Route
path="/login"
element={withSuspenseLazy(SignIn)}
/>
<Route
path="/logout"
element={withSuspenseLazy(Exist)}
/>
<Route
path="/profile"
element={
<ProtectedRoute>{withSuspenseLazy(Profile)}</ProtectedRoute>
}
/>
<Route
path="/code_region"
element={
<ProtectedRoute>
{withSuspenseLazy(Code_region)}
</ProtectedRoute>
}
/>
<Route
path="/confirm-delete"
element={
<ProtectedRoute>
{withSuspenseLazy(DeleteProfileConfirmation)}
</ProtectedRoute>
}
/>
<Route
path="*"
element={withSuspenseLazy(Notfound)}
/>
</Routes>
</Content>
</Layout>
</Content>

<Footer
className="layout"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<img
src={logo}
alt="Логотип"
style={{
height: "30px",
marginRight: "10px",
}}
/>
<h3 style={{ margin: 0, color: "white" }}>КОНАН24</h3>
</Footer>
</Layout>
</Router>
)
}
// }

export default observer(App)
import React from "react"
import "../index.css"
import { Header } from "antd/lib/layout/layout"
import { Menu, Dropdown, Space } from "antd"
import { NavLink } from "react-router-dom"
import { UserOutlined, SearchOutlined } from "@ant-design/icons"
import { useLocation } from "react-router-dom"
import stores from "../stores/stores"
import { observer } from "mobx-react-lite"
import logo from "../Image/logo_2.png"
import menuItems from "./menuItems"

const AppHeader = observer(() => {
const { authStore } = stores
const location = useLocation()

const items = authStore.isAuth
? [
{
key: "/profile",
label: <NavLink to="/profile">Личный кабинет</NavLink>,
},
{ key: "/logout", label: <NavLink to="/logout">Выход</NavLink> },
{ type: "divider" },
]
: [
{
key: "/register",
label: <NavLink to="/register">Регистрация</NavLink>,
},
{ key: "login", label: <NavLink to="/login">Вход</NavLink> },
{ type: "divider" },
]

const topMenuItems = [
{
key: "main",
label: (
<NavLink
to="/main"
style={{ display: "flex" }}
>
<img
src={logo}
alt="Логотип"
style={{
height: "40px",
marginRight: "10px",
marginTop: "10px",
}}
/>
<h1 style={{ margin: 0, color: "white" }}>КОНАН24</h1>
</NavLink>
),
},
{ key: "creat-db", label: <NavLink to="/creat-db">Создать БД</NavLink> },
{
key: "search",
label: <NavLink to="/search">Поиск материала в БД</NavLink>,

icon: <SearchOutlined />,
},
{
key: "code_region",
label: <NavLink to="/code_region">Поиск кода региона</NavLink>,

icon: <SearchOutlined />,
},

{
key: "email",
label: <a href="mailto:[email protected]">[email protected]</a>,
},

{
key: "dropdown",
style: { marginLeft: "auto" },
label: (
<Dropdown
menu={{ items }}
trigger={["click"]}
>
<a onClick={(e) => e.preventDefault()}>
<Space>
{authStore.user.username ? (
authStore.user.username ? (
<span>{authStore.user.username}</span>
) : (
<span>Загрузка...</span>
)
) : (
<UserOutlined />
)}
</Space>
</a>
</Dropdown>
),
},
]
return (
<>
<Header
className="header-custom"
style={{
display: "flex",
alignItems: "center",
background: "transparent",
}}
>
<Menu
className="header-custom"
theme="dark"
mode="horizontal"
selectedKeys={[location.pathname]}
items={topMenuItems}
style={{ flex: 1, minWidth: 0 }}
/>
</Header>
<div className="not_more_1200">
<Header
className="header-custom "
style={{
display: "flex",
alignItems: "center",
background: "transparent",
}}
>
<Menu
className="header-custom"
theme="dark"
mode="horizontal"
selectedKeys={[location.pathname]}
items={menuItems}
style={{ flex: 1, minWidth: 0 }}
/>
</Header>
</div>
</>
)
})

export default AppHeader
import React, { useState } from "react"
import "../index.css"
import { Layout, Menu } from "antd"
import { NavLink } from "react-router-dom"
import { useLocation } from "react-router-dom"
import { observer } from "mobx-react-lite"
import menuItems from "./menuItems"

const { Sider } = Layout

const AppSider = observer(() => {
const location = useLocation()

return (
<div className="not_1200">
<Sider>
<Menu
mode="inline"
selectedKeys={[location.pathname]}
items={menuItems}
style={{ height: "100%" }}
className="sider white-text"
/>
</Sider>
</div>
)
})

export default AppSider
import { NavLink } from "react-router-dom"

const menuItems = [
{ key: "/main", label: <NavLink to="/main">О проекте</NavLink> },
{ key: "/your_db", label: <NavLink to="/your_db">Ваша БАЗА</NavLink> },
{ key: "/KAC", label: <NavLink to="/KAC">КАЦ по КП</NavLink> },
{
key: "/template",
label: <NavLink to="/template">Заполнение шаблона</NavLink>,
},
{ key: "/codes", label: <NavLink to="/codes">Поиск кодов</NavLink> },
{ key: "/request", label: <NavLink to="/request">Запрос для КП</NavLink> },
{ key: "/reviews", label: <NavLink to="/reviews">Отзывы</NavLink> },
{ key: "/contacts", label: <NavLink to="/contacts">Контакты</NavLink> },
]

export default menuItems
import React, { useState } from "react"
import { Form, message } from "antd"
import stores from "../../stores/stores"
import SignIn from "./SignIn"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_message from "../../config_text_message"
import config_text_button from "../../config_text_button"

const Exist = () => {
const { authStore } = stores
const [isLoading, setIsLoading] = useState(false)

const onFinish = async () => {
setIsLoading(true)
try {
await authStore.handleLogout()
} catch (error) {
message.error(config_text_message.TEXT_ERROR_EXIST)
} finally {
setIsLoading(false)
}
}

const onFinishFailed = (errorInfo) => {
message.error(errorInfo || config_text_message.TEXT_ERROR_EXIST)
}

return (
<>
{authStore.isAuth ? (
<div style={{ textAlign: "center", marginTop: "50px" }}>
<h4>Если вы хотите выйти, нажмите кнопку ниже.</h4>
<Form
name="basic"
labelCol={{
span: 8,
}}
wrapperCol={{
span: 16,
}}
style={{
maxWidth: 600,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
wrapperCol={{
offset: 8,
span: 16,
}}
>
<ButtonsSpin
state_isLoading={isLoading}
htmlType_button={config_text_button.SUBMIT}
name_button={config_text_button.EXIST}
danger_button={true}
></ButtonsSpin>
</Form.Item>
</Form>
</div>
) : (
<SignIn />
)}
</>
)
}

export default Exist
import React, { useState } from "react"
import "../../index.css"
import { useNavigate } from "react-router-dom"
import { handleAuth } from "../../utils/authUtils"
import stores from "../../stores/stores"
import AuthForm from "./AuthForm"
import config_text_message from "../../config_text_message"
import config_text_button from "../../config_text_button"

const SignIn = () => {
const { authStore } = stores

const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false)

const onFinish = async (data) => {
setIsLoading(true)
try {
await authStore.handleSignIn(data)
await handleAuth(authStore, navigate)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_SIGNIN)
} finally {
setIsLoading(false)
}
}

return (
<div className="centr">
<h2>Для входа введите учетные данные!</h2>

<AuthForm
name_button={config_text_button.SIGNIN}
state_isLoading={isLoading}
onFinish_func={onFinish}
/>
</div>
)
}

export default SignIn
import React, { useState } from "react"
import { message, Typography } from "antd"
import { useNavigate } from "react-router-dom"
import { handleAuth } from "../../utils/authUtils"
import stores from "../../stores/stores"
import AuthForm from "./AuthForm"
import config_text_message from "../../config_text_message"
import config_text_button from "../../config_text_button"

const SignUp = () => {
const { authStore } = stores
const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false)

const onFinish = async (data) => {
setIsLoading(true)
try {
await authStore.handleSignUp(data)
await handleAuth(authStore, navigate)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_REGISTR)
} finally {
setIsLoading(false)
}
}

return (
<div className="centr">
<h2>Для регистрации введите учетные данные!</h2>
<AuthForm
name_button={config_text_button.SIGNUP}
state_isLoading={isLoading}
onFinish_func={onFinish}
/>
</div>
)
}

export default SignUp
import React from "react"
import { Collapse } from "antd"
import Code_region from "./Code_region"
import Code_constraction from "./Code_constraction"

const text_Code_region = <Code_region />
const text_Code_constraction = <Code_constraction />

const Codes = () => {
const items_Code_region = [
{
key: "1",
label: "Поиск кода региона",
children: text_Code_region,
},
]
const items_Code_constraction = [
{
key: "2",
label: "Поиск шифра строительного ресурса",
children: text_Code_constraction,
},
]

return (
<div>
<h2>Поиск кодов для КАЦ</h2>
<Collapse items={items_Code_region} />
<Collapse items={items_Code_constraction} />
</div>
)
}

export default Codes
import React, { useState } from "react"
import { Input, message, Spin, Descriptions } from "antd"
import stores from "../../stores/stores"
import config_text_message from "../../config_text_message"
import config_text_H from "../../config_text_H"

const Code_region = () => {
const { codes_store } = stores
const [city, setCity] = useState("")
const [result, setResult] = useState(null)
const [loading, setLoading] = useState(false)

const handleSearch = async () => {
if (!city) {
message.error(config_text_message.TEXT_ERROR_REGION_INPUT)
return
}
setLoading(true)
try {
const data = await codes_store.getRegionCode(city)
setResult(data)
message.success(config_text_message.TEXT_SUCCESS_REGION)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_REGION)
} finally {
setLoading(false)
}
}

return (
<div>
<h2>{config_text_H.CODES_H}</h2>
<Input.Search
placeholder="Введите название города"
enterButton="Поиск"
value={city}
onChange={(e) => setCity(e.target.value)}
onSearch={handleSearch}
loading={loading}
style={{ maxWidth: "400px" }}
/>
{loading && (
<div style={{ marginTop: "10px" }}>
<Spin tip="Поиск кода региона...">
<div style={{ minHeight: "50px" }} />
</Spin>
</div>
)}
{result && (
<div style={{ marginTop: "20px" }}>
<div className="not_1200">
<Descriptions
title="Результат поиска"
bordered
>
<Descriptions.Item label="Запрошенный город">
{result.city_prompt}
</Descriptions.Item>
<Descriptions.Item label="Город из базы">
{result.city_db}
</Descriptions.Item>
<Descriptions.Item label="Код региона">
{result.region_code}
</Descriptions.Item>

<Descriptions.Item label="Район подчинение">
{result.district_subordination}
</Descriptions.Item>

<Descriptions.Item label="Регион">
{result.area}
</Descriptions.Item>
</Descriptions>
</div>
<div className="not_more_1200">
<Descriptions
title="Результат поиска"
bordered
>
<Descriptions.Item label="Город из базы">
{result.city_db}
</Descriptions.Item>
<Descriptions.Item label="Код региона">
{result.region_code}
</Descriptions.Item>
</Descriptions>
</div>
</div>
)}
</div>
)
}

export default Code_region
import React from "react"

const Code_constraction = () => {
return (
<div style={{ padding: "20px" }}>
<h2>Поиск шифра строительного ресурса</h2>
<p>В разработке...</p>
</div>
)
}

export default Code_constraction
import { Button } from "antd"
import React from "react"

const Contacts = () => {
return (
<div className="Contacts">
<h2>ЧEM МЫ МОЖЕМ ПОМОЧЬ?!</h2>
<h4>
Ищете помощь и поддержку для улучшения работы с КОНАН24? Если вы хотите
узнать, как решить проблему в автоматизированной работе по формированию
конъюнктурного анализа цен или дать полезные идеи для дальнейшего роста
КОНАН24, пишите.
</h4>
<Button type="link">
<a href="mailto:[email protected]">[email protected]</a>
</Button>
</div>
)
}

export default Contacts
import React, { useState } from "react"
import { Collapse, Input } from "antd"
import { message } from "antd"
import Drag from "../common/Drag/Drag"
import stores from "../../stores/stores"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_button from "../../config_text_button"
import CreatDBInstructions from "./CreatDBInstructions"
import config_text_message from "../../config_text_message"
import config_text_H from "../../config_text_H"
import { EXAMPLE_URL } from "../../http/apiClients"

const text = <CreatDBInstructions URL={EXAMPLE_URL} />

const instructions = [
{
key: "1",
label: "Инструкция",
children: text,
className: "white-label",
},
]

function CreatDB() {
const { db_ka_store } = stores
const [dataFiles, setDataFiles] = useState([])
const [startRow, setStartRow] = useState(13)
const [isLoading, setLoading] = useState(false)
const [isLoadingReset, setLoadingReset] = useState(false)

const handleDataChange = (e) => {
try {
e.preventDefault()
const selectedFiles = e.target.files
if (selectedFiles && selectedFiles.length > 0) {
const filesArray = Array.from(selectedFiles)
setDataFiles(filesArray)
db_ka_store.setDataFiles_KA(filesArray)
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_DOWN_FILES)
}
}

const handleReset = () => {
setLoadingReset(true)
try {
setDataFiles([])
db_ka_store.setDataFiles_KA(null)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
} finally {
setLoadingReset(false)
}
}

const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
try {
const response = await db_ka_store.uploadDataToDatabase(
dataFiles,
startRow
)

if (response.total_rows === 0) {
message.warning(
`Файлы обработаны(${response.total_files_KA} шт.). В базу новых записей не добавлено.`,
10
)
} else {
message.success(
`Файлы обработаны(${response.total_files_KA} шт.). Добавлено в базу ${response.total_rows} строк`,
10
)
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_NOT_PUSH_DB)
} finally {
setLoading(false)
}
}

const handleDataDrop = (e) => {
try {
e.preventDefault()
setDragActiveData(false)
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const droppedFiles = Array.from(e.dataTransfer.files)
setDataFiles(droppedFiles)
db_ka_store.setDataFiles_KA(droppedFiles)
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_DOWN_FILES)
}
}

return (
<div>
<h2>{config_text_H.CREAT_DB_H}</h2>

<Collapse items={instructions} />
<div className="wrapper">
<div className="form-container">
<Drag
dataFiles={dataFiles}
handleReset={handleReset}
handleSubmit={handleSubmit}
handleDataDrop={handleDataDrop}
handleDataChange={handleDataChange}
nameForm={config_text_button.DOWNLOADS_KAC}
/>
</div>

<div className="buttons">
<div className="start_row">
<div className="start_row_info ">Строка:</div>
<Input
className="start_row_input"
placeholder=""
type="number"
value={startRow}
onChange={(e) => setStartRow(Number(e.target.value))}
/>
</div>

<ButtonsSpin
handle_func={handleSubmit}
name_button={config_text_button.PUSHDB}
state_isLoading={isLoading}
style={{ marginTop: "10px", display: "block" }}
/>

<ButtonsSpin
handle_func={handleReset}
name_button={config_text_button.CLEAR}
state_isLoading={isLoadingReset}
style={{ marginTop: "10px", display: "block" }}
type_button={config_text_button.DEFAULTBUTTON}
size_button={config_text_button.DEFAULTBUTTON}
/>
</div>
</div>
</div>
)
}

export default CreatDB
import React from "react"
import config_text_button from "../../config_text_button"

const CreatDBInstructions = ({ URL }) => {
return (
<>
<ol>
<li>
Загрузите файлы КАЦ в формате .xlsx. КАЦ должен состоять из 26 колонок
(приказ № 421 от 4 августа 2020 г. Минстроя РФ в редакции приказа № 55
от 30 января 2024 ) (
<a
href={`${URL}/3`}
download
>
скачать пример
</a>
).
</li>
<li>
Выбери номер стартовой строки для копирования строк с материалами
(если файл имеет шапку, как в примере, то можно оставить стартовую
строку 13).
</li>
<li>Нажми на кнопку "{config_text_button.PUSHDB}"</li>
<li>Получи положительный ответ!</li>
</ol>
</>
)
}

export default CreatDBInstructions
import React, { useState } from "react"
import { Collapse, message } from "antd"
import Inputs_filter from "./Inputs_filter"
import Table_KAC from "./Table_KAC"
import stores from "../../stores/stores"
import config_text_H from "../../config_text_H"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_button from "../../config_text_button"
import DB_getInstructions from "./DB_getInstructions"
import { EXAMPLE_URL } from "../../http/apiClients"
import config_text_message from "../../config_text_message"

const text = <DB_getInstructions URL={EXAMPLE_URL} />

const instructions = [
{
key: "1",
label: "Инструкция",
children: text,
},
]

function DB_get() {
const { db_ka_store } = stores

const [dataList, setDataList] = useState([])

const [supplierName, setSupplierName] = useState("")
const [location, setLocation] = useState("")
const [objectName, setObjectName] = useState("")
const [nameFile, setNameFile] = useState("")

const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [isLoading, setLoading] = useState(false)
const [isLoadingExport, setLoadingExpor] = useState(false)

const handleFetchData = async (
page = currentPage,
pageSizeValue = pageSize
) => {
setLoading(true)
try {
const params = {
supplier_name: supplierName || null,
location: location || null,
object_name: objectName || null,
name_file: nameFile || null,
page: page,
page_size: pageSizeValue,
}
await db_ka_store.fetchDataFromDatabase(params)
setDataList(db_ka_store.dataListKA)
if (db_ka_store.dataListKA.length) {
message.success(config_text_message.TEXT_SUCCESS_INFO)
} else {
message.success(config_text_message.TEXT_SUCCESS_INFO_NOT)
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_INFO)
} finally {
setLoading(false)
}
}

// Function to export data to Excel
const handleExportData = async () => {
setLoadingExpor(true)
try {
await db_ka_store.exportDataToExcel()

message.success(config_text_message.TEXT_SUCCESS_EXPORT)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_EXPORT)
} finally {
setLoadingExpor(false)
}
}

const items_filter = [
{
key: "1",
label: "Фильтры",
children: (
<Inputs_filter
supplierName={supplierName}
setSupplierName={setSupplierName}
location={location}
setLocation={setLocation}
objectName={objectName}
setObjectName={setObjectName}
nameFile={nameFile}
setNameFile={setNameFile}
setCurrentPage={setCurrentPage}
handleFetchData={handleFetchData}
/>
),
},
]

return (
<div>
<h2>{config_text_H.DB_GET_H}</h2>

<Collapse items={instructions} />

<div className="wrapper">
<div className="button_get_export">
<div className="not_mobile">
<ButtonsSpin
handle_func={() => handleFetchData()}
name_button={config_text_button.GET_LIST_DB}
state_isLoading={isLoading}
style={{
marginTop: "10px",
marginBottom: "10px",
display: "block",
}}
/>
</div>

<ButtonsSpin
handle_func={handleExportData}
name_button={config_text_button.SAVE_IN_COMP}
state_isLoading={isLoadingExport}
style={{
marginTop: "10px",
marginBottom: "10px",
display: "block",
}}
/>
</div>
<div className="not_mobile">
<Collapse items={items_filter} />

<Table_KAC
dataList={dataList}
store={db_ka_store}
currentPage={currentPage}
setCurrentPage={setCurrentPage}
pageSize={pageSize}
setPageSize={setPageSize}
handleFetchData={handleFetchData}
/>
</div>
</div>
</div>
)
}

export default DB_get
import React from "react"
import config_text_button from "../../config_text_button"

const DB_getInstructions = () => {
return (
<>
<div className="not_more_1200">
К сожалению, полная функциональность сервиса недоступна на мобильных
устройствах. Рекомендуем использовать компьютер для полного доступа.
</div>
<ul>
<li>
Кнопка "{config_text_button.GET_LIST_DB}" покажит таблицу с
материалами из загруженных КАЦ"
</li>
<li>Фильтрами можно скорректировать данные для вывода в таблицу</li>
<li>
Кнопка "{config_text_button.SAVE_IN_COMP}" загрузит таблицу с данными
в формате .xlsx
</li>

<li>Удачи!</li>
</ul>
</>
)
}

export default DB_getInstructions
import React from "react"
import { Table } from "antd"
import Quantity from "../common/Quantity"

const Table_KAC = ({
dataList,
store,
currentPage,
setCurrentPage,
pageSize,
setPageSize,
handleFetchData,
}) => {
// Определение колонок для таблицы
const columns = [
{
title: "№ п.п.",
dataIndex: "number_pozition",
key: "number_pozition",
width: 100,
fixed: "left",
},
{
title: "Код ресурса, затрат",
dataIndex: "resource_code",
key: "resource_code",
width: 150,
fixed: "left",
},
{
title: "Наименование ресурса, затрат",
dataIndex: "resource_name",
key: "resource_name",
width: 200,
fixed: "left",
},
{
title: "Полное наименование ресурса, затрат в обосновывающем документе",
dataIndex: "full_resource_name",
key: "full_resource_name",
},
{
title: "Единица измерения ресурса, затрат",
dataIndex: "unit_measure",
key: "unit_measure",
},
{
title: "Единица измерения ресурса, затрат в обосновывающем документе",
dataIndex: "unit_measure_doc",
key: "unit_measure_doc",
},
{
title:
"Текущая отпускная цена за единицу измерения в обосновывающем документе с НДС, руб.",
dataIndex: "current_price_with_vat",
key: "current_price_with_vat",
},
{
title:
"Текущая отпускная цена за единицу измерения в обосновывающем документе без НДС, руб.",
dataIndex: "current_price_without_vat",
key: "current_price_without_vat",
},
{
title:
"Текущая отпускная цена за единицу измерения без НДС, руб. в соответствии с графой 5",
dataIndex: "current_price_without_vat_unit",
key: "current_price_without_vat_unit",
},
{
title: "Затраты на перевозку, %",
dataIndex: "transport_cost_percent",
key: "transport_cost_percent",
},
{
title: "Затраты на перевозку, руб. за единицу измерения без НДС",
dataIndex: "transport_cost_unit",
key: "transport_cost_unit",
},
{
title: "Затраты на заготовительно-складские расходы, %",
dataIndex: "storage_cost_percent",
key: "storage_cost_percent",
},
{
title: "Заготовительно-складские расходы, руб.",
dataIndex: "storage_cost_unit",
key: "storage_cost_unit",
},
{
title:
"Дополнительные затраты, предусмотренные пунктами 88, 117, 119–121 Методики, Наименование затрат",
dataIndex: "additional_costs_name",
key: "additional_costs_name",
},
{
title:
"Дополнительные затраты, предусмотренные пунктами 88, 117, 119–121 Методики, %",
dataIndex: "additional_costs_percent",
key: "additional_costs_percent",
},
{
title:
"Дополнительные затраты, предусмотренные пунктами 88, 117, 119–121 Методики, руб.",
dataIndex: "additional_costs_unit",
key: "additional_costs_unit",
},
{
title: "Сметная цена без НДС, руб. за единицу измерения",
dataIndex: "estimate_price_without_vat",
key: "estimate_price_without_vat",
},
{
title: "Год",
dataIndex: "year",
key: "year",
},
{
title: "Квартал",
dataIndex: "quarter",
key: "quarter",
},
{
title:
"Полное и (или) сокращенное (при наличии) наименования производителя / поставщика",
dataIndex: "supplier_name",
key: "supplier_name",
},
{
title:
"Страна производителя оборудования, производственного и хозяйственного инвентаря",
dataIndex: "country_producer",
key: "country_producer",
},
{
title: "КПП организации",
dataIndex: "kpp",
key: "kpp",
},
{
title: "ИНН организации",
dataIndex: "inn",
key: "inn",
},
{
title: "Гиперссылка на веб-сайт производителя/поставщика",
dataIndex: "hyperlink",
key: "hyperlink",
},
{
title: "Населенный пункт расположения склада производителя/поставщика",
dataIndex: "location",
key: "location",
},
{
title: "Статус организации - производитель (1) / поставщик (2)",
dataIndex: "organization_status",
key: "organization_status",
},
{
title: "Раздел КА",
dataIndex: "section_value",
key: "section_value",
},
{
title: "Наименование строительного объекта",
dataIndex: "object_name",
key: "object_name",
},
{
title: "Приказ",
dataIndex: "order_number",
key: "order_number",
},
{
title: "Имя файла",
dataIndex: "name_file_KA",
key: "name_file_KA",
},
{
title: "Дата создания",
dataIndex: "created_at",
key: "created_at",
},
]

return (
<div className="table-container">
{dataList.length > 0 && (
<>
<Quantity
nameQuantity="Количество строк"
count={store.totalRows}
/>

<Table
className="table"
dataSource={dataList}
columns={columns}
rowKey="id"
pagination={{
total: store.totalRows,
pageSize: pageSize,
current: currentPage,
onChange: (page, pageSizeValue) => {
setCurrentPage(page)
setPageSize(pageSizeValue)
handleFetchData(page, pageSizeValue)
},
}}
scroll={{
x: 3000,
y: 400,
}}
/>
</>
)}
</div>
)
}

export default Table_KAC
import React, { useState } from "react"
import { message, Typography, Collapse, Card } from "antd"
import { FileExcelOutlined } from "@ant-design/icons"
import { observer } from "mobx-react-lite"
import stores from "../../stores/stores"
import KACInstructions from "./KACInstructions"
import ButtonsSpin from "../common/ButtonsSpin"
import FileUpload from "../common/FileUpload"
import { resetParams } from "../../utils/resetParams"
import SupplierMaterialsTable from "./SupplierMaterialsTable"
import SupplierInfoTable from "./SupplierInfoTable"
import config_text_message from "../../config_text_message"
import config_text_button from "../../config_text_button"
import config_text_H from "../../config_text_H"

const text = <KACInstructions />

const instructions = [
{
key: "1",
label: "Инструкция",
children: text,
},
]

const KAC = observer(() => {
const { kac_store } = stores

const [fileList, setFileList] = useState([])
const [fileTask, setFileTask] = useState(null)

const initialSupplierInfoParams = {
name_magaz: "",
address: "",
page: 1,
page_size: 10,
}

const initialSupplierMaterialsParams = {
name_magaz: "",
product_name: "",
number_zakaz: "",
file_name: "",
page: 1,
page_size: 10,
}

const [supplierInfoParams, setSupplierInfoParams] = useState(
initialSupplierInfoParams
)

const [supplierMaterialsParams, setSupplierMaterialsParams] = useState(
initialSupplierMaterialsParams
)

const [isUploading, setIsUploading] = useState(false)
const [isExporting, setIsExporting] = useState(false)
const [
isLoading_handleShowInfoSupplier,
setIsLoading_handleShowInfoSupplier,
] = useState(false)
const [
isLoading_handleShowInfoMaterials,
setIsLoading_handleShowInfoMaterials,
] = useState(false)
const [
isLoading_handleHideInfoSupplier,
setIsLoading_handleHideInfoSupplier,
] = useState(false)
const [
isLoading_handleHideInfoMaterials,
setIsLoading_handleHideInfoMaterials,
] = useState(false)

const [showSupplierInfo, setShowSupplierInfo] = useState(false)
const [showSupplierMaterials, setShowSupplierMaterials] = useState(false)

const handleUploadFiles = async () => {
if (fileList.length === 0) {
message.error(config_text_message.TEXT_ERROR_CHOOSE_FILES)
return
}

setIsUploading(true)
try {
await kac_store.uploadFilesToDBFromKP(fileList)
message.success(config_text_message.TEXT_SUCCESS_DOWN_FILES)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_DOWN_FILES)
} finally {
setIsUploading(false)
}
}

const handleExportExcel = async () => {
setIsExporting(true)
try {
await kac_store.exportSupplierMaterialsSessionToExcel(fileTask)
message.success(config_text_message.TEXT_SUCCESS_EXPORT)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_EXPORT)
} finally {
setIsExporting(false)
}
}

const fetchSupplierInfo = async (params = supplierInfoParams) => {
try {
await kac_store.getSupplierInfo(params)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_INFO)
}
}

const fetchSupplierMaterials = async (params = supplierMaterialsParams) => {
try {
await kac_store.getSupplierMaterials(params)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_INFO)
}
}

const handleShowInfoSupplier = async () => {
setIsLoading_handleShowInfoSupplier(true)
try {
await fetchSupplierInfo()
setShowSupplierInfo(true)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_INFO)
} finally {
setIsLoading_handleShowInfoSupplier(false)
}
}

const handleShowInfoMaterials = async () => {
setIsLoading_handleShowInfoMaterials(true)
try {
await fetchSupplierMaterials()
setShowSupplierMaterials(true)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_INFO)
} finally {
setIsLoading_handleShowInfoMaterials(false)
}
}

const handleHideInfoSupplier = async () => {
setIsLoading_handleHideInfoSupplier(true)
try {
setShowSupplierInfo(false)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_HIDE)
} finally {
setIsLoading_handleHideInfoSupplier(false)
}
}

const handleHideInfoMaterials = async () => {
setIsLoading_handleHideInfoMaterials(true)
try {
setShowSupplierMaterials(false)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_HIDE)
} finally {
setIsLoading_handleHideInfoMaterials(false)
}
}

const resetSupplierInfoParams = () => {
try {
resetParams(
setSupplierInfoParams,
fetchSupplierInfo,
initialSupplierInfoParams
)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_COMMON)
}
}

const resetSupplierMaterialsParams = () => {
try {
resetParams(
setSupplierMaterialsParams,
fetchSupplierMaterials,
initialSupplierMaterialsParams
)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_COMMON)
}
}

const handleFileChange = (file) => {
try {
setFileList((prevList) => [...prevList, file])
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
}
}

const handleTaskFileChange = (file) => {
try {
setFileTask(file)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
}
}

const handleRemoveFile = (file) => {
setFileList((prevList) => prevList.filter((f) => f.uid !== file.uid))
}

const handleRemoveTaskFile = () => {
setFileTask(null)
}

return (
<div>
<h2>{config_text_H.KAC_H}</h2>
<Collapse
items={instructions}
style={{ marginBottom: "20px" }}
/>
<div className="cards_kac">
<Card
title={config_text_H.KAC_H_DOWN_FILE}
bordered={false}
>
<div>
<FileUpload
multiple={true}
onFileChange={handleFileChange}
onRemove={handleRemoveFile}
fileList={fileList}
buttonText={config_text_button.CHOOSE_FILES}
accept=".xlsx,.xls"
/>

<ButtonsSpin
handle_func={handleUploadFiles}
name_button={config_text_button.DOWN_UPDATE}
icon_button={<FileExcelOutlined />}
state_isLoading={isUploading}
style={{ marginTop: "10px" }}
dataTestId="file-update-download"
/>
</div>
</Card>
<Card
title={config_text_H.KAC_H_EXPORT_FILE}
bordered={false}
>
<div>
<FileUpload
onFileChange={handleTaskFileChange}
onRemove={handleRemoveTaskFile}
fileList={fileTask ? [fileTask] : []}
buttonText={config_text_button.CHOOSE_FILE_TASK}
accept=".xlsx,.xls"
dataTestId="file-upload-input"
/>

<ButtonsSpin
handle_func={handleExportExcel}
name_button={config_text_button.EXPORT_IN_EXCEL}
icon_button={<FileExcelOutlined />}
state_isLoading={isExporting}
style={{ marginTop: "10px" }}
dataTestId="file-excel_download"
/>
</div>
</Card>
</div>
<div className="not_mobile">
<h2>Ваши данные</h2>
<div className="form-item-buttons">
<ButtonsSpin
handle_func={handleShowInfoSupplier}
name_button={config_text_button.MY_SUPPLIERS}
state_isLoading={isLoading_handleShowInfoSupplier}
style={{ marginTop: "10px" }}
/>
<ButtonsSpin
handle_func={handleShowInfoMaterials}
name_button={config_text_button.MY_MATERIALS}
state_isLoading={isLoading_handleShowInfoMaterials}
style={{ marginTop: "10px" }}
/>
</div>

{showSupplierInfo && (
<SupplierInfoTable
supplierInfoParams={supplierInfoParams}
setSupplierInfoParams={setSupplierInfoParams}
fetchSupplierInfo={fetchSupplierInfo}
kac_store={kac_store}
resetSupplierInfoParams={resetSupplierInfoParams}
handleHideInfoSupplier={handleHideInfoSupplier}
state_isLoading={isLoading_handleHideInfoSupplier}
/>
)}

{showSupplierMaterials && (
<SupplierMaterialsTable
supplierMaterialsParams={supplierMaterialsParams}
setSupplierMaterialsParams={setSupplierMaterialsParams}
fetchSupplierMaterials={fetchSupplierMaterials}
kac_store={kac_store}
setShowSupplierMaterials={setShowSupplierMaterials}
resetSupplierMaterialsParams={resetSupplierMaterialsParams}
handleHideInfoMaterials={handleHideInfoMaterials}
state_isLoading={isLoading_handleHideInfoMaterials}
/>
)}
</div>
</div>
)
})

export default KAC
import React from "react"
import config_text_button from "../../config_text_button"

const KACInstructions = () => {
return (
<>
<div className="not_more_1200">
К сожалению, полная функциональность сервиса недоступна на мобильных
устройствах. Рекомендуем использовать компьютер для полного доступа.
</div>
<ol>
<li>
Подготовьте файлы КП для загрузки в формате .xlsx (можно
воспользоваться онлайн инструментами извлечения данных из pdf в
таблицы Excel). Желательно, чтобы в файлах точно было наименование
организации поставщика (если нет в оригинале-файле, добавьте
наименование в файл в любую ячейку в формате ООО "Пример")
</li>
<li>Добавьте файлы в формате .xlsx</li>
<li>
Нажми на кнопку "{config_text_button.DOWN_UPDATE}" для добавления
данных в базы данных поставщиков и материалов
</li>
<li>
Если вы загрузите файл с искомыми материалами (формат .xlsx), то файл
КАЦ сформируется на основании этих материалов (наиболее подходящий
материал из КП добавится к материалу из задания). Excel файл должен
содержать как минимум 2 столбца (в А - номер по порядку, в столбце B
должен быть список материалов). Если файла с заданием не будет,
нумерация начнется с 1 и материалы будут просто по прядку.
</li>
<li>
Нажми на кнопку "{config_text_button.EXPORT_IN_EXCEL}" и получи почти
готовый КАЦ
</li>
<li>
Нажми на кнопку "{config_text_button.MY_SUPPLIERS}" и посмотри свою
базу поставщиков"
</li>
<li>
Нажми на кнопку "{config_text_button.MY_MATERIALS}" и посмотри свою
базу материалов"
</li>
<li>Удачи! Не забудь оценить идею, перейдя на страницу "Отзывы"</li>
</ol>
</>
)
}

export default KACInstructions
import React from "react"
import Common_KAC_table from "./Common_KAC_table"
import config_text_H from "../../config_text_H"

const SupplierInfoTable = ({
supplierInfoParams,
setSupplierInfoParams,
fetchSupplierInfo,
kac_store,
resetSupplierInfoParams,
handleHideInfoSupplier,
state_isLoading,
}) => {
const columns = [
{ title: "Имя магазина", dataIndex: "name_magaz", key: "name_magaz" },
{ title: "Адрес", dataIndex: "address", key: "address" },
{ title: "ИНН", dataIndex: "inn", key: "inn" },
{ title: "КПП", dataIndex: "kpp", key: "kpp" },
{ title: "Телефон", dataIndex: "phone", key: "phone" },
{ title: "Email", dataIndex: "email", key: "email" },
{
title: "URL",
dataIndex: "url",
key: "url",
render: (text) => (
<a
href={text}
target="_blank"
rel="noopener noreferrer"
>
{text}
</a>
),
},
{
title: "Дата создания",
dataIndex: "created_at",
key: "created_at",
render: (text) => text.substring(0, 10),
width: 200,
},
]

const formFields = [
{ label: "Имя магазина", key: "name_magaz" },
{ label: "Адрес", key: "address" },
]

return (
<Common_KAC_table
title={config_text_H.KAC_H_INFO_SUPPLIER}
columns={columns}
dataSource={kac_store.supplierInfo.slice()}
params={supplierInfoParams}
setParams={setSupplierInfoParams}
fetchData={fetchSupplierInfo}
totalRows={kac_store.totalRowsSupplierInfo}
formFields={formFields}
resetParams={resetSupplierInfoParams}
handleHide={handleHideInfoSupplier}
isLoading={state_isLoading}
quantityText={config_text_H.KAC_H_INFO_SUPPLIER_QUANTITY}
/>
)
}

export default SupplierInfoTable
import React from "react"
import Common_KAC_table from "./Common_KAC_table"
import config_text_H from "../../config_text_H"

const SupplierMaterialsTable = ({
supplierMaterialsParams,
setSupplierMaterialsParams,
fetchSupplierMaterials,
kac_store,
resetSupplierMaterialsParams,
handleHideInfoMaterials,
state_isLoading,
}) => {
const columns = [
{ title: "Имя магазина", dataIndex: "name_magaz", key: "name_magaz" },
{ title: "Номер заказа", dataIndex: "number_zakaz", key: "number_zakaz" },
{
title: "Название продукта",
dataIndex: "product_name",
key: "product_name",
},
{ title: "Цена", dataIndex: "price", key: "price" },
{
title: "Единица измерения",
dataIndex: "unit_without_digits",
key: "unit_without_digits",
},
{ title: "Артикул", dataIndex: "articul", key: "articul" },
{ title: "Дата", dataIndex: "date", key: "date" },
{ title: "Имя файла", dataIndex: "file_name", key: "file_name" },
{
title: "Дата создания",
dataIndex: "created_at",
key: "created_at",
render: (text) => text.substring(0, 10),
width: 200,
},
]

const formFields = [
{ label: "Имя магазина", key: "name_magaz" },
{ label: "Название продукта", key: "product_name" },
{ label: "Номер заказа", key: "number_zakaz" },
{ label: "Имя файла", key: "file_name" },
]

return (
<Common_KAC_table
title={config_text_H.KAC_H_INFO_MATERIALS}
columns={columns}
dataSource={kac_store.supplierMaterials.slice()}
params={supplierMaterialsParams}
setParams={setSupplierMaterialsParams}
fetchData={fetchSupplierMaterials}
totalRows={kac_store.totalRowsSupplierMaterials}
formFields={formFields}
resetParams={resetSupplierMaterialsParams}
handleHide={handleHideInfoMaterials}
isLoading={state_isLoading}
quantityText={config_text_H.KAC_H_INFO_MATERIALS_QUANTITY}
/>
)
}

export default SupplierMaterialsTable
import { Carousel, Typography } from "antd"
import React from "react"
import Cards_main from "../common/Cards_main"

const MainPage = () => {
return (
<>
<div className="Contacts">
<h2>КОРОТКО О КОНAН24</h2>
<h4>
Представляем вам КОНАН24 — ваш персональный онлайн-инструмент для
автоматизированного формирования конъюнктурного анализа цен (КАЦ). На
каждом этапе сбора информации наш сервис станет вашим незаменимым
помощником, предоставляя все необходимые инструменты и поддержку.
</h4>
<h4>
С каждым обновлением КОНАН24 будет становиться еще более
функциональным! Мы непрерывно внедряем новые возможности, чтобы
сделать ваш рабочий процесс быстрее, эффективнее и более удобным.
Доверьтесь КОНАН24 и откройте для себя мир простоты и умных решений в
анализе цен! Присоединяйтесь к нам сегодня и сделайте ваш бизнес более
эффективным и прибыльным!
</h4>
</div>

<Carousel
arrows
infinite={false}
>
<Cards_main
text_title="Создайте свою базу данных"
text_info_cards={
<>
Это быстро, просто и удобно!
<br />В дальнейшем Вы сможете быстро находить поставщиков для
новых материалов в нужном районе.
<p>Преимущества:</p>
1. Экономия времени: создание базы данных в считанные минуты!
<br />
2. Простота использования: интуитивно понятный интерфейс, не
требующий специальных знаний.
<br />
<br />
Не тратьте время на рутину! <br />
Автоматизируйте создание базы данных с нашим сервисом!
</>
}
redirectTo="/creat-db"
/>

<Cards_main
text_title="Создайте Конъюнктурный Анализ Цен (КАЦ) с легкостью!"
text_info_cards={
<>
Готовы преобразовать свои данные в мощные инструменты анализа?
<br />
Наш онлайн-сервис предлагает вам уникальную возможность
формировать КАЦ на основе коммерческих предложений (КП) всего за
несколько простых шагов!
<p>Преимущества:</p>
1. Экономия времени: мы обработали сотни КП, чтобы Вы могли
создать почти готовый КАЦ за считанные минуты!
<br />
2. Простота использования: интуитивно понятный интерфейс, не
требующий специальных знаний.
<br />
<br />
Не тратьте время на рутину! <br />
Автоматизируйте создание КАЦ с нашим сервисом! <br />
</>
}
redirectTo="/KAC"
/>

<Cards_main
text_title="Создайте документ по шаблону в 3 шага!"
text_info_cards={
<>
Ты устал тратить время создание книги прайсов? С помощью этого
инструмента ты сможешь автоматизировать создание документа!
Настрой шаблон, напиши под нее информацию и ты получишь готовые
листы, в которые останется добавить только скриншоты.
<br />
<p>Преимущества:</p>
1. Экономия времени: создание документов в считанные минуты!
<br />
2. Простота использования: интуитивно понятный интерфейс, не
требующий специальных знаний.
<br />
3. История: Обновляйте список файлов и скачивайте их в любое
время.
<br />
<br />
Не тратьте время на рутину! <br />
Автоматизируйте создание документов с нашим сервисом! Этот
инструмент в дальнейшем будет доработан. Дай обратную связь
только.
</>
}
redirectTo="/template"
/>

<Cards_main
text_title="Найдите код региона за секунды!"
text_info_cards={
<>
Нужно быстро найти информацию о регионе? <br />
Введите название города и получите всю необходимую информацию!
<p>Преимущества:</p>
1. Быстрый поиск: получите информацию о регионе за секунды!
<br />
2. Точность: данные актуальны и достоверны.
<br />
3. Простота использования: удобный и интуитивно понятный
интерфейс.
<br />
<br />
Не теряйте время на поиск информации! <br />
Используйте наш сервис и получайте нужные данные мгновенно!
</>
}
redirectTo="/code_region"
/>

<Cards_main
text_title="Найдите материал в своей базе данных за секунды!"
text_info_cards={
<>
Ты устал тратить время на поиск материалов вручную? <br />
Ты хочешь автоматизировать этот процесс? Тогда тебе нужен наш
сервис!
<p>Преимущества:</p>
1. Экономия времени: поиск материалов в считанные секунды!
<br />
2. Простота использования: интуитивно понятный интерфейс.
<br />
<br />
Не теряйте время на поиск информации! <br />
Используйте собственную базу и получайте нужные данные мгновенно!
</>
}
redirectTo="/search"
/>

<Cards_main
text_title="Сформируйте письмо-запрос за секунды!"
text_info_cards={
<>
Ты устал тратить время на создание писем для рассылки запросов по
КП? <br />
Ты хочешь автоматизировать этот процесс? <br />
Тогда тебе нужен наш сервис!
<p>Преимущества:</p>
1. Экономия времени: создание письма-запроса в считанные секунды!
<br />
2. Простота использования: интуитивно понятный интерфейс, не
требующий специальных знаний.
<br />
<br />
Не теряйте время на рутину! <br />
Автоматизируйте создание письма-запроса с нашим сервисом!
</>
}
redirectTo="/request"
/>
</Carousel>
</>
)
}

export default MainPage
import React, { useState } from "react"
import stores from "../../stores/stores"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_button from "../../config_text_button"
import Cards_main from "../common/Cards_main"
import { Link } from "react-router-dom"

const Profile = () => {
const { authStore } = stores

return (
<div>
<h2>Добро пожаловать, {authStore.user.username}!</h2>

<Cards_main
text_title="Ваша база КАЦ"
text_info_cards={
<>Пройдите, посмотреть базу КАЦ, которую Вы сами создали!</>
}
redirectTo="/your_db"
/>

<Cards_main
text_title="Посмотрите в базе поставщиков и материалы!"
text_info_cards={
<>
Не тратьте время на рутину! <br />
Автоматизируйте создание КАЦ с нашим сервисом! <br />
</>
}
redirectTo="/KAC"
/>
<Link to="/confirm-delete">
<ButtonsSpin
danger_button={true}
name_button={config_text_button.DELETE}
></ButtonsSpin>
</Link>
</div>
)
}

export default Profile
import React from "react"
import { Button, Typography, message, Modal } from "antd"
import { ExclamationCircleOutlined } from "@ant-design/icons"
import config_text_message from "../../config_text_message"
import stores from "../../stores/stores"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_button from "../../config_text_button"

const { Paragraph } = Typography
const { confirm } = Modal

const DeleteProfileConfirmation = () => {
const { authStore } = stores
const onFinish = async () => {
try {
await authStore.deleteUser()
message.success(config_text_message.TEXT_SUCCESS_DELETE)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_DELETE)
}
}

const onFinishFailed = (errorInfo) => {
message.warning(config_text_message.TEXT_WARNING_DELETE)
}

const showDeleteConfirm = () => {
confirm({
title: "Вы уверены, что хотите удалить профиль?",
icon: <ExclamationCircleOutlined />,
content: (
<div>
<Paragraph>
При удалении профиля все данные будут удалены безвозвратно.
</Paragraph>
<Paragraph>
Это действие не может быть отменено. Пожалуйста, убедитесь, что вы
действительно хотите это сделать.
</Paragraph>
</div>
),
okText: "Удалить",
okType: "danger",
cancelText: "Отмена",
onOk() {
onFinish()
},
onCancel() {
onFinishFailed()
},
})
}

return (
<div style={{ textAlign: "center", marginTop: "50px" }}>
<h2>Удаление профиля</h2>
<h4>Если вы хотите удалить свой профиль, нажмите кнопку ниже.</h4>
<ButtonsSpin
handle_func={showDeleteConfirm}
name_button={config_text_button.DELETE}
danger_button={true}
></ButtonsSpin>
</div>
)
}

export default DeleteProfileConfirmation
import React, { useState } from "react"
import { Button, Form, Input, Upload, message, Collapse } from "antd"
import { observer } from "mobx-react-lite"
import stores from "../../stores/stores"
import ButtonsSpin from "../common/ButtonsSpin"
import { downloadFile } from "../../utils/exportUtils"
import config_text_H from "../../config_text_H"
import config_text_button from "../../config_text_button"
import config_text_message from "../../config_text_message"
import RequestInstructions from "./RequestInstructions"
import { EXAMPLE_URL } from "../../http/apiClients"

const text = <RequestInstructions URL={EXAMPLE_URL} />

const instructions = [
{
key: "1",
label: "Инструкция",
children: text,
},
]

const Request = observer(() => {
const { request_store } = stores
const [form] = Form.useForm()
const [file, setFile] = useState(null)
const [fileName, setFileName] = useState("")

const [isloading, setIsLoading] = useState(false)
const [isloading_handleExportLetter, setIsloading_handleExportLetter] =
useState(false)

const [messageText, setmessageText] = useState("")

const handleFileChange = (info) => {
if (info.fileList.length > 0) {
setFile(info.fileList[0].originFileObj)
setFileName(info.fileList[0].name)
}
}

const handleCreatLetter = async (values) => {
setIsLoading(true)
try {
const text = await request_store.creatLetterForRequest(file, values.tasks)
message.success(config_text_message.TEXT_SUCCESS_REQUEST)
setmessageText(text)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_REQUEST)
} finally {
setIsLoading(false)
}
}

const handleClear = () => {
form.resetFields()
setFile(null)
setFileName("")
setmessageText("")
}

const handleExportLetter = async () => {
setIsloading_handleExportLetter(true)
try {
const response = await request_store.exportDocxLetterForRequest(
file,
form.getFieldValue("tasks")
)

downloadFile(
response.data,
"letter.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)

message.success(config_text_message.TEXT_SUCCESS_REQUEST_EXPORT)
} catch (error) {
message.error(
error.message || config_text_message.TEXT_ERROR_REQUEST_EXPORT
)
} finally {
setIsloading_handleExportLetter(false)
}
}

return (
<div style={{ padding: "20px" }}>
<h2>{config_text_H.REQUEST_H}</h2>
<Collapse items={instructions} />

<Form
form={form}
layout="vertical"
onFinish={handleCreatLetter}
>
<Form.Item
label="1. Впишите позиции (по необходимости):"
name="tasks"
rules={[{ required: false }]}
>
<Input.TextArea rows={4} />
</Form.Item>
<ButtonsSpin
handle_func={handleClear}
name_button={config_text_button.CLEAR}
type_button={config_text_button.DEFAULTBUTTON}
style={{ marginBottom: "10px" }}
size_button={config_text_button.DEFAULTBUTTON}
/>

<Form.Item label="2. Загрузите файл с материалами в формате .xlsx (по необходимости)">
<Upload
beforeUpload={() => false}
onChange={handleFileChange}
>
<Button data-testid="download_file_task">
{config_text_button.DOWNLOAD}
</Button>
</Upload>
</Form.Item>
<Form.Item>
<div className="buttons">
<ButtonsSpin
name_button={config_text_button.LETTER_FROM_KP}
htmlType_button={config_text_button.SUBMIT}
state_isLoading={isloading}
dataTestId="creat_letter"
/>

<ButtonsSpin
name_button={config_text_button.SAVE_IN_COMP}
state_isLoading={isloading_handleExportLetter}
handle_func={handleExportLetter}
/>
</div>
</Form.Item>

{messageText && (
<div className="messageText_request">{messageText}</div>
)}
</Form>
</div>
)
})

export default Request
import React from "react"
import config_text_button from "../../config_text_button"

const RequestInstructions = ({ URL }) => {
return (
<>
<ol>
<li>Если нужно, впиши позиции вручную</li>
<li>
Если нужно, загpузи файл с материалами, которые нужно найти (материалы
должны стоять в столбце В), формат файла .xlsx (
<a
href={`${URL}/4`}
download
style={{ marginLeft: "10px" }}
>
{config_text_button.DOWNLOAD_EX}
</a>
).
</li>
<li>
Нажми на кнопку "{config_text_button.LETTER_FROM_KP}" и посмотри
результат ниже.
</li>
<li>
Нажми на кнопку "{config_text_button.SAVE_IN_COMP}" и скачай письмо в
отформатированном виде к себе.
</li>
<li>Удачи!</li>
</ol>
</>
)
}

export default RequestInstructions
import React, { useEffect } from "react"
import { observer } from "mobx-react-lite"
import { Tabs, Typography } from "antd"
import stores from "../../stores/stores"
import AllCommentsTab from "./AllCommentsTab"
import MyCommentsTab from "./MyCommentsTab"
import CommentForm from "./CommentForm"

const Reviews = observer(() => {
const { reviews_store, authStore } = stores

useEffect(() => {
reviews_store.getListMessagesAll()
}, [reviews_store])

const handleTabChange = async (key) => {
if (key === "1") {
await reviews_store.getListMessagesAll()
} else if (key === "2") {
await reviews_store.getListMessagesId()
}
}

const refreshComments = async () => {
await reviews_store.getListMessagesAll()
if (authStore.isAuth) {
await reviews_store.getListMessagesId()
}
}

const tabItems = [
{
key: "1",
label: "Все отзывы",
children: <AllCommentsTab />,
},
...(authStore.isAuth
? [
{
key: "2",
label: "Мои отзывы",
children: <MyCommentsTab />,
},
]
: []),
]

return (
<div style={{ padding: "20px" }}>
<h2>О нас пишут</h2>
{authStore.isAuth ? (
<CommentForm onCommentSubmitted={refreshComments} />
) : (
<h4>Пожалуйста, войдите в систему для оставления комментария.</h4>
)}
<div style={{ maxHeight: "1000px", overflowY: "auto" }}>
<Tabs
defaultActiveKey="1"
items={tabItems}
onChange={handleTabChange}
/>
</div>
</div>
)
})

export default Reviews
import React, { useState } from "react"
import { List, Input, Spin, message } from "antd"
import {
EditOutlined,
DeleteOutlined,
CheckOutlined,
CloseOutlined,
} from "@ant-design/icons"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_button from "../../config_text_button"
import config_text_message from "../../config_text_message"
import Quantity from "../common/Quantity"
import { observer } from "mobx-react-lite"
import stores from "../../stores/stores"

const MyCommentsTab = observer(() => {
const { reviews_store } = stores
const isLoading = reviews_store.isLoading
const totalComments = reviews_store.total_messages_id
const comments = reviews_store.userMessages

const [editingCommentId, setEditingCommentId] = useState(null)
const [editingCommentContent, setEditingCommentContent] = useState("")
const [isLoadingDelete, setLoadingDelete] = useState(false)
const [isLoadingSaveEdit, setLoadingSaveEdit] = useState(false)

const handleEdit = (commentId, currentComment) => {
setEditingCommentId(commentId)
setEditingCommentContent(currentComment)
}

const handleSaveEdit = async (commentId) => {
setLoadingSaveEdit(true)
try {
await reviews_store.editComment(commentId, editingCommentContent)
setEditingCommentId(null) // Reset editing
message.success(config_text_message.TEXT_SUCCESS_EDIT_COMMENT)
} catch (error) {
message.error(
error.message || config_text_message.TEXT_ERROR_EDIT_COMMENT
)
} finally {
setLoadingSaveEdit(false)
}
}

const handleCancelEdit = () => {
setEditingCommentId(null)
setEditingCommentContent("")
}

const handleDelete = async (commentId) => {
setLoadingDelete(true)
try {
await reviews_store.deleteComment(commentId)
message.success(config_text_message.TEXT_SUCCESS_DELETE_COMMENT)
} catch (error) {
message.error(
error.message || config_text_message.TEXT_ERROR_DELETE_COMMENT
)
} finally {
setLoadingDelete(false)
}
}

return (
<>
{isLoading ? (
<Spin />
) : (
<>
<Quantity
nameQuantity="Количество отзывов"
count={totalComments}
/>
<List
itemLayout="horizontal"
dataSource={comments}
renderItem={(item) => {
const isEditing = editingCommentId === item.message_id
return (
<List.Item>
<List.Item.Meta
title={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
{isEditing ? (
<>
<Input
value={editingCommentContent}
onChange={(e) =>
setEditingCommentContent(e.target.value)
}
onPressEnter={() =>
handleSaveEdit(item.message_id)
}
style={{ flexGrow: 1 }}
data-testid={`comment-input_my_component${item.message_id}`}
/>
<ButtonsSpin
handle_func={() =>
handleSaveEdit(item.message_id)
}
icon_button={<CheckOutlined />}
state_isLoading={isLoadingSaveEdit}
style={{ marginLeft: "10px" }}
dataTestId={`save-button_my_component${item.message_id}`}
/>
<ButtonsSpin
handle_func={handleCancelEdit}
icon_button={<CloseOutlined />}
style={{ marginLeft: "10px" }}
/>
</>
) : (
<>
<div style={{ flexGrow: 1 }}>{item.content}</div>
<div className="buttons">
<ButtonsSpin
handle_func={() =>
handleEdit(item.message_id, item.content)
}
icon_button={<EditOutlined />}
state_isLoading={isLoadingSaveEdit}
size_button={config_text_button.DEFAULTBUTTON}
type_button={config_text_button.DEFAULTBUTTON}
dataTestId={`edit-icon_my_component${item.message_id}`}
/>
<ButtonsSpin
handle_func={() =>
handleDelete(item.message_id)
}
icon_button={<DeleteOutlined />}
state_isLoading={isLoadingDelete}
type_button={config_text_button.DEFAULTBUTTON}
size_button={config_text_button.DEFAULTBUTTON}
danger_button="true"
dataTestId={`delete-icon_my_component${item.message_id}`}
/>
</div>
</>
)}
</div>
}
description={
<span
style={{
marginLeft: "30px",
fontSize: "0.8em",
color: "gray",
}}
>
{item.created_at.slice(0, 10)}
</span>
}
/>
</List.Item>
)
}}
/>
</>
)}
</>
)
})

export default MyCommentsTab
import React, { useState } from "react"
import { Form, Input, message } from "antd"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_button from "../../config_text_button"
import config_text_message from "../../config_text_message"
import stores from "../../stores/stores"

const CommentForm = ({ onCommentSubmitted }) => {
const { reviews_store } = stores
const [form] = Form.useForm()
const [isLoadingSubmit, setLoadingSubmit] = useState(false)

const handleSubmit = async (values) => {
setLoadingSubmit(true)
try {
await reviews_store.creatComment(values.comment)
message.success(config_text_message.TEXT_SUCCESS_COMMENT)
form.resetFields()

onCommentSubmitted()
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_Reviews)
} finally {
setLoadingSubmit(false)
}
}

return (
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="comment"
label="Оставьте свой отзыв"
rules={[{ required: true, message: "Пожалуйста, введите отзыв" }]}
>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item>
<ButtonsSpin
htmlType_button={config_text_button.SUBMIT}
name_button={config_text_button.SEND}
state_isLoading={isLoadingSubmit}
style={{ marginTop: "10px" }}
/>
</Form.Item>
</Form>
)
}

export default CommentForm
import React from "react"
import { List, Spin } from "antd"
import Quantity from "../common/Quantity"
import { observer } from "mobx-react-lite"
import stores from "../../stores/stores"

const AllCommentsTab = observer(() => {
const { reviews_store } = stores
const isLoading = reviews_store.isLoading
const totalComments = reviews_store.total_messages_all
const comments = reviews_store.messages

return (
<>
{isLoading ? (
<Spin />
) : (
<>
<Quantity
nameQuantity="Количество отзывов"
count={totalComments}
/>
<List
itemLayout="horizontal"
dataSource={comments}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
title={item.content}
description={
<>
{`Пользователь ${item.user_id}`}
<span
style={{
marginLeft: "30px",
fontSize: "0.8em",
color: "gray",
}}
>
{item.created_at.slice(0, 10)}
</span>
</>
}
/>
</List.Item>
)}
/>
</>
)}
</>
)
})

export default AllCommentsTab
import React, { useState } from "react"
import {
Upload,
Button,
Input,
message,
Spin,
Typography,
Divider,
Collapse,
} from "antd"
import {
UploadOutlined,
SearchOutlined,
FileExcelOutlined,
} from "@ant-design/icons"
import SearchResultsTable from "./SearchResultsTable"
import stores from "../../stores/stores"
import config_text_button from "../../config_text_button"
import SearchInstructions from "./SearchInstructions"
import config_text_message from "../../config_text_message"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_H from "../../config_text_H"
import { EXAMPLE_URL } from "../../http/apiClients"

const { Title } = Typography

const text = <SearchInstructions URL={EXAMPLE_URL} />

const Search = () => {
const { db_ka_store } = stores
const [fileList, setFileList] = useState([])
const [materialSearch, setMaterialSearch] = useState("")
const [searchResults, setSearchResults] = useState([])
const [isUploading, setIsUploading] = useState(false)
const [isSearching, setIsSearching] = useState(false)

const handleFileUpload = async () => {
if (fileList.length === 0) {
message.error(config_text_message.TEXT_ERROR_CHOOSE_FILES)
return
}

const file = fileList[0]
setIsUploading(true)
try {
await db_ka_store.uploadFileTaskWithMaterials(file)
message.success(config_text_message.TEXT_SUCCESS_DOWN_FILES)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_DOWN_FILES)
} finally {
setIsUploading(false)
}
}

const handleMaterialSearch = async () => {
if (!materialSearch) {
message.error(config_text_message.TEXT_ERROR_INPUT)
return
}

setIsSearching(true)
try {
const results = await db_ka_store.findMaterials(materialSearch)
setSearchResults(results.matched_data)
if (results.matched_data.length === 0) {
message.warning(config_text_message.TEXT_ERROR_NOT_FOUND)
} else {
message.success(config_text_message.TEXT_SUCCESS_SEARCH)
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_SEARCH)
} finally {
setIsSearching(false)
}
}

const items = [
{
key: "1",
label: "Инструкция",
children: text,
className: "white-label",
},
]

return (
<div>
<h2>{config_text_H.SEARCH_H}</h2>
<Collapse items={items} />
<Upload
beforeUpload={(file) => {
setFileList([file])
return false
}}
onRemove={() => setFileList([])}
fileList={fileList}
accept=".xlsx,.xls"
>
<Button
icon={<UploadOutlined />}
style={{
marginTop: "10px",
marginBottom: "10px",
}}
>
{config_text_button.CHOOSE_FILES}
</Button>
</Upload>
<ButtonsSpin
handle_func={handleFileUpload}
name_button={config_text_button.DOWN_UPDATE}
icon_button={<FileExcelOutlined />}
state_isLoading={isUploading}
style={{ marginTop: "10px" }}
/>
<div className="not_mobile">
<h2>{config_text_H.SEARCH_H_MATERIALS}</h2>
<Input.Search
placeholder="Введите название материала"
enterButton={<SearchOutlined data-testid="search_input_button" />}
value={materialSearch}
onChange={(e) => setMaterialSearch(e.target.value)}
onSearch={handleMaterialSearch}
loading={isSearching}
style={{ maxWidth: "400px" }}
data-testid="search_input"
/>

{isSearching && (
<div style={{ marginTop: "10px" }}>
<Spin />
</div>
)}

{searchResults && searchResults.length > 0 && (
<div style={{ marginTop: "20px" }}>
<h3>Результаты поиска:</h3>
<SearchResultsTable data={searchResults} />
</div>
)}
</div>
</div>
)
}

export default Search
import React from "react"
import config_text_button from "../../config_text_button"

const SearchInstructions = ({ URL }) => {
return (
<>
<div className="not_more_1200">
К сожалению, полная функциональность сервиса недоступна на мобильных
устройствах. Рекомендуем использовать компьютер для полного доступа.
</div>
<ol>
<li>
Загрузите файл в формате .xlsx для поиска материалов в своей базе.
Материалы должны находиться в столбце В (
<a
href={`${URL}/4`}
download
>
скачать пример
</a>
).
</li>

<li>
Нажми на кнопку "{config_text_button.DOWN_UPDATE}" для скачивания
файла с результатом поиска.
</li>

<li>Получи положительный ответ!</li>
<li>Можно найти материал, вписав его в строку поиска ниже</li>
</ol>
</>
)
}

export default SearchInstructions
import React from "react"
import { Table } from "antd"

const SearchResultsTable = ({ data }) => {
const columns = [
{
title: "№ п.п.",
dataIndex: "number_pozition",
key: "number_pozition",
width: 100,
fixed: "left",
},
{
title: "Код ресурса, затрат",
dataIndex: "resource_code",
key: "resource_code",
width: 150,
fixed: "left",
},
{
title: "Наименование ресурса, затрат",
dataIndex: "resource_name",
key: "resource_name",
width: 200,
fixed: "left",
},
{
title: "Полное наименование ресурса, затрат в обосновывающем документе",
dataIndex: "full_resource_name",
key: "full_resource_name",
width: 200,
},
{
title: "Единица измерения ресурса, затрат",
dataIndex: "unit_measure",
key: "unit_measure",
},
{
title: "Единица измерения ресурса, затрат в обосновывающем документе",
dataIndex: "unit_measure_doc",
key: "unit_measure_doc",
},
{
title:
"Текущая отпускная цена за единицу измерения в обосновывающем документе с НДС, руб.",
dataIndex: "current_price_with_vat",
key: "current_price_with_vat",
},
{
title:
"Текущая отпускная цена за единицу измерения в обосновывающем документе без НДС, руб.",
dataIndex: "current_price_without_vat",
key: "current_price_without_vat",
},
{
title:
"Текущая отпускная цена за единицу измерения без НДС, руб. в соответствии с графой 5",
dataIndex: "current_price_without_vat_unit",
key: "current_price_without_vat_unit",
},
{
title: "Затраты на перевозку, %",
dataIndex: "transport_cost_percent",
key: "transport_cost_percent",
},
{
title: "Затраты на перевозку, руб. за единицу измерения без НДС",
dataIndex: "transport_cost_unit",
key: "transport_cost_unit",
},
{
title: "Затраты на заготовительно-складские расходы, %",
dataIndex: "storage_cost_percent",
key: "storage_cost_percent",
},
{
title: "Заготовительно-складские расходы, руб.",
dataIndex: "storage_cost_unit",
key: "storage_cost_unit",
},
{
title:
"Дополнительные затраты, предусмотренные пунктами 88, 117, 119–121 Методики, Наименование затрат",
dataIndex: "additional_costs_name",
key: "additional_costs_name",
},
{
title:
"Дополнительные затраты, предусмотренные пунктами 88, 117, 119–121 Методики, %",
dataIndex: "additional_costs_percent",
key: "additional_costs_percent",
},
{
title:
"Дополнительные затраты, предусмотренные пунктами 88, 117, 119–121 Методики, руб.",
dataIndex: "additional_costs_unit",
key: "additional_costs_unit",
},
{
title: "Сметная цена без НДС, руб. за единицу измерения",
dataIndex: "estimate_price_without_vat",
key: "estimate_price_without_vat",
},
{
title: "Год",
dataIndex: "year",
key: "year",
},
{
title: "Квартал",
dataIndex: "quarter",
key: "quarter",
},
{
title:
"Полное и (или) сокращенное (при наличии) наименования производителя / поставщика",
dataIndex: "supplier_name",
key: "supplier_name",
width: 200,
},
{
title:
"Страна производителя оборудования, производственного и хозяйственного инвентаря",
dataIndex: "country_producer",
key: "country_producer",
},
{
title: "КПП организации",
dataIndex: "kpp",
key: "kpp",
width: 200,
},
{
title: "ИНН организации",
dataIndex: "inn",
key: "inn",
width: 200,
},
{
title: "Гиперссылка на веб-сайт производителя/поставщика",
dataIndex: "hyperlink",
key: "hyperlink",
width: 200,
},
{
title: "Населенный пункт расположения склада производителя/поставщика",
dataIndex: "location",
key: "location",
},
{
title: "Статус организации - производитель (1) / поставщик (2)",
dataIndex: "organization_status",
key: "organization_status",
},
{
title: "Раздел КА",
dataIndex: "section_value",
key: "section_value",
width: 200,
},
{
title: "Наименование строительного объекта",
dataIndex: "object_name",
key: "object_name",
width: 200,
},
{
title: "Приказ",
dataIndex: "order_number",
key: "order_number",
width: 200,
},
{
title: "Имя файла",
dataIndex: "name_file_KA",
key: "name_file_KA",
width: 200,
},
{
title: "Дата создания",
dataIndex: "created_at",
key: "created_at",
},

{
title: "ID",
dataIndex: "id",
key: "id",
width: 80,
fixed: "left",
},
]

return (
<Table
dataSource={data}
columns={columns}
rowKey="id"
pagination={false}
scroll={{
x: 5000,
y: 400,
}}
/>
)
}

export default SearchResultsTable
import React, { useState } from "react"
import { Table, message } from "antd"
import { DownloadOutlined, DeleteOutlined } from "@ant-design/icons"
import ButtonsSpin from "../common/ButtonsSpin"
import "../../index.css"
import Quantity from "../common/Quantity"
import config_text_button from "../../config_text_button"
import config_text_message from "../../config_text_message"

const ReadyFilesTable = ({ templStore }) => {
const [isLoading, setIsLoading] = useState(false)
const [isLoadingDelete, setIsLoadingDelete] = useState(false)

// Получаем уникальные значения(нужно для фильтрации)
const uniqueFilenames = [
...new Set(templStore.readyfiles_template.map((item) => item.filename)),
]

// Создаем массив фильтров
const filters = uniqueFilenames.map((filename) => ({
text: filename,
value: filename,
}))

// Обработка скачивания
const handleDownload = async (id) => {
setIsLoading(true)
try {
await templStore.downloadFileReadyTemplate(id)
message.success(config_text_message.TEXT_SUCCESS_EXPORT)
} catch (error) {
message.error(error.message || config_text_message.TEXT_ERROR_EXPORT)
} finally {
setIsLoading(false)
}
}

// Обработка удаления
const handleDelete = async (id) => {
setIsLoadingDelete(true)
try {
await templStore.deleteFile_ready_template(id)
message.success(config_text_message.TEXT_SUCCESS_DELETE_COMMENT)
} catch (error) {
message.error(
error.message || config_text_message.TEXT_ERROR_DELETE_COMMENT
)
} finally {
setIsLoadingDelete(false)
}
}

// Определение колонок для таблицы
const columns = [
{
title: "Действия",
key: "actions",
render: (text, record) => (
<span>
<div className="buttons">
<ButtonsSpin
state_isLoading={isLoading}
handle_func={() => handleDownload(record.id)}
icon_button={<DownloadOutlined />}
type_button={config_text_button.DEFAULTBUTTON}
size_button={config_text_button.DEFAULTBUTTON}
/>
<ButtonsSpin
state_isLoading={isLoadingDelete}
handle_func={() => handleDelete(record.id)}
danger_button={true}
icon_button={<DeleteOutlined />}
type_button={config_text_button.DEFAULTBUTTON}
size_button={config_text_button.DEFAULTBUTTON}
/>
</div>
</span>
),
width: 130,
},
{
title: "Имя файла",
dataIndex: "filename",
key: "filename",
filters: filters,
onFilter: (value, record) => record.filename.includes(value),
},
{
title: "Дата создания",
dataIndex: "created_at",
key: "created_at",
render: (text) => text.substring(0, 10),
width: 200,
},
{
title: "Шаблон",
dataIndex: "template",
key: "template",
},
{
title: "Файл Excel",
dataIndex: "file_excel",
key: "file_excel",
},
]

return (
<>
<div className="ready-files">
<Quantity
nameQuantity={config_text_button.QuantityREADYFILES}
count={templStore.readyfiles_template.length}
/>

<Table
className="table"
dataSource={templStore.readyfiles_template}
columns={columns}
rowKey="id"
scroll={{ y: 400 }}
/>
</div>
</>
)
}

export default ReadyFilesTable
import React, { useState } from "react"
import "../../index.css"
import { DownloadOutlined, EyeOutlined } from "@ant-design/icons"
import { Collapse, message } from "antd" // Импортируем Spin
import { EXAMPLE_URL } from "../../http/apiClients"
import ReadyFilesTable from "./ReadyFilesTable"
import Drag from "../common/Drag/Drag" // Импортируем новый компонент
import stores from "../../stores/stores"
import TemplateInstructions from "./TemplateInstructions"
import ButtonsSpin from "../common/ButtonsSpin"
import config_text_H from "../../config_text_H"
import config_text_button from "../../config_text_button"
import config_text_message from "../../config_text_message"

const text = <TemplateInstructions URL={EXAMPLE_URL} />

const instructions = [
{
key: "1",
label: "Инструкция",
children: text,
},
]

function Template() {
const { templStore } = stores

const [templateFiles, setTemplateFiles] = useState([])
const [dataFiles, setDataFiles] = useState([])

const [isLoadingDownload, setIsLoadingDownload] = useState(false)
const [isLoadingShow, setIsLoadingShow] = useState(false)

const handleTemplateChange = (e) => {
try {
e.preventDefault()
const selectedFiles = e.target.files
if (selectedFiles && selectedFiles.length > 0) {
setTemplateFiles([...selectedFiles])
templStore.setTemplateFile(selectedFiles[0])
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
}
}

const handleDataChange = (e) => {
try {
e.preventDefault()
const selectedFiles = e.target.files
if (selectedFiles && selectedFiles.length > 0) {
setDataFiles([...selectedFiles])
templStore.setDataFile(selectedFiles[0])
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
}
}

const handleReset = () => {
try {
setTemplateFiles([])
setDataFiles([])
templStore.setTemplateFile(null)
templStore.setDataFile(null)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
}
}

const handleTemplateDrop = (e) => {
try {
e.preventDefault()
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const droppedFiles = [...e.dataTransfer.files]
setTemplateFiles(droppedFiles)
templStore.setTemplateFile(droppedFiles[0])
}
} catch (error) {
message.error(config_text_message.TEXT_ERROR_COMMON)
}
}

const handleSubmit = async (e) => {
e.preventDefault()
setIsLoadingDownload(true)
try {
await templStore.uploadFiles()

message.success(config_text_message.TEXT_SUCCESS_DOWN_FILES)
} catch (error) {
message.error(config_text_message.TEXT_ERROR_DOWN_FILES)
} finally {
setIsLoadingDownload(false)
}
}

const handleSubmitInfoFilesReady = async (e) => {
e.preventDefault()
setIsLoadingShow(true)
try {
await templStore.checkReadyfiles_template()

message.success(config_text_message.TEXT_SUCCESS_INFO)
} catch (error) {
message.success(config_text_message.TEXT_ERROR_INFO)
} finally {
setIsLoadingShow(false)
}
}

return (
<div>
<h2>{config_text_H.TEMPLATE_H}</h2>

<Collapse items={instructions} />
<div className="wrapper">
<div className="form-container_two">
<Drag
dataFiles={templateFiles}
handleReset={handleReset}
handleSubmit={handleSubmit}
handleDataDrop={handleTemplateDrop}
handleDataChange={handleTemplateChange}
nameForm={config_text_button.DOWNLOADSTEMPLATES}
/>
<Drag
dataFiles={dataFiles}
handleReset={handleReset}
handleSubmit={handleSubmit}
handleDataDrop={handleTemplateDrop}
handleDataChange={handleDataChange}
nameForm={config_text_button.DOWNLOADDATASTEMPLATES}
/>
</div>
<div className="buttons">
<ButtonsSpin
handle_func={handleReset}
name_button={config_text_button.RESET}
type_button={config_text_button.DEFAULTBUTTON}
size_button={config_text_button.DEFAULTBUTTON}
/>

<ButtonsSpin
state_isLoading={isLoadingDownload}
handle_func={handleSubmit}
name_button={config_text_button.DOWNLOADSFILESREADY}
icon_button={<DownloadOutlined />}
/>
<div className="not_mobile">
<ButtonsSpin
state_isLoading={isLoadingShow}
handle_func={handleSubmitInfoFilesReady}
name_button={config_text_button.UPDATELISTFILES}
icon_button={<EyeOutlined />}
/>
</div>
</div>
<div className="not_mobile">
{templStore.readyfiles_template && (
<ReadyFilesTable templStore={templStore} />
)}
</div>
</div>
</div>
)
}

export default Template
import React from "react"
import config_text_button from "../../config_text_button"

const TemplateInstructions = ({ URL }) => {
return (
<>
<div className="not_more_1200">
К сожалению, полная функциональность сервиса недоступна на мобильных
устройствах. Рекомендуем использовать компьютер для полного доступа.
</div>
<ol>
<li>
Создай и загрузи собственный шаблон в формате .docx (
<a
href={`${URL}/1`}
download
>
скачать пример и инструкцию
</a>
).
</li>
<li>
Создай и загрузи данные для заполнения шаблона в формате .xlsx (
<a
href={`${URL}/2`}
download
>
скачать пример
</a>
).
</li>
<li>
Нажми на кнопку "{config_text_button.DOWNLOADSFILESREADY}" и получи
результат у себя в загрузках.
</li>
<li>
Нажав на кнопку "{config_text_button.UPDATELISTFILES}", можно увидеть
и скачать файлы, подготовленные за всё время. Есть фильтр по
наименованию файла. Поэтому рекомендуется в файл .xlsx в первый
столбец добавлять ту переменную, которую хотелось бы увидеть в
названии файла.
</li>
<li>Удачи!</li>
</ol>
</>
)
}

export default TemplateInstructions
Сделай за меня или напиши пошаговую инструкцию, как это сделать

الفكرة

Формулирую запрос

Пользователь хочет добавить карту сайта и микроразметку к своему React-проекту, предлагая предложенный код.

Создание карты

Я разбираюсь в структуре React-приложения, содержащего компоненты с React Router, Ant Design и MobX. Сейчас я занимаюсь созданием sitemap и добавлением microdata.

Создание карты сайта для SPA

В React SPA создание карты сайта не так просто, но можно использовать средства ползания или React Router.

Перечисляю маршруты

Я начинаю с перечня всех маршрутов в конфигурации React Router, которые нужно учесть для создания карты сайта.

Создаю карту сайта

Я генерирую карту сайта вручную, создавая файл sitemap.xml и добавляя теги <url> для каждой маршрутизации. Также рассмотрен вариант с библиотекой для динамической генерации.

Создание карты сайта

Итак, я работаю над генерацией карты сайта с использованием пакета sitemap. Устанавливаю его через npm и создаю скрипт для чтения маршрутов и формирования sitemap.xml, доступного по ссылке.

Создаю карту сайта

Проверяю наличие файла robots.txt в каталоге public. Это помогает поисковым системам понять структуру сайта и улучшает индексацию страниц.

Добавляю карту сайта

Включаю ссылку на карту сайта в файл robots.txt. Это важно для улучшения индексации страниц.

Добавляем структурированные данные

Я изучаю добавление структурированных данных к компонентам React, используя JSON-LD для внедрения схемы продукта. Это помогает четче понять предлагаемый продукт.

Внедрение и проверка

Ок, я сейчас думаю о внедрении Helmet для структурированных данных и проверяю их в Google Structured Data Testing Tool.

Создание и редактирование

Устанавливаю пакет react-router-sitemap и создаю sitemap-generator.js с настройками маршрутов.

Создание карты сайта

Рассматриваю пути маршрутизации для генерации карты сайта. Можно определить маршруты в отдельном файле или массиве, чтобы легко интегрировать их в генератор карты.

Создание карты сайта

Использую подходы вручную и через react-router-sitemap, чтобы сгенерировать файл sitemap.xml в public директории.

Создание маршрутов

В этом шаге я описываю процесс создания маршрутов в файле src/routes.js с использованием React lazy и withSuspenseLazy. Затем, в App.js, импортирую маршруты и отображаю их через Routes компонент.

Thought Process: Я осознаю важность добавления карты сайта и robots.txt для индексации поисковыми системами.

Внедряю микроданные

Планирую подключить микроданные к компонентам в React, представим, як страница "О нас" содержит схему организации, чтобы улучшить структурированные данные.

Устанавливаю React Helmet

Установил пакет React Helmet и импортировал его в компонент MainPage.js. Определяю схему организации с помощью JSON-ld для улучшенного SEO.

Проверяю результаты

Окей, давайте посмотрим. Я теперь проверяю структуру данных каждой страницы, используя инструмент проверки от Google. Если нужно, добавляю подходящие схемы для каждой страницы, например, ContactPage в разделе Contacts.js.

Внедряю мета-теги

Добавляю structured data для Contacts, чтобы улучшить SEO. В Reviews можно осуществить аналогичное для Review.

Создание карты сайта

Пытаюсь добавить карту сайта и микроразметку в свой проект React. Устанавливаю пакет react-router-sitemap, создаю файл src/routes.js для маршрутов и обновляю существующие маршруты.

Обновляю маршруты

Я вношу изменения в App.js, чтобы использовать маршруты из routes.js, а затем создаю скрипт генерации карты сайта в корне проекта.

Создаю карты сайта

Добавляю скрипт в package.json, запускаю его для генерации карты сайта с файлом sitemap.xml. обновляю robots.txt.

Управление приложениям

Я подключаю react-helmet для управления заголовками, чтобы улучшить SEO.

Добавляю схему

Я добавляю схему организации и контактной страницы в компоненты MainPage и Contacts для улучшения SEO.

Добавляю схемы

Я добавляю микроразметку к рецензиям, продукциям или услугам, и статьям, используя схемы Review, Product, Service, и Article соответственно.

الإجابة

Создание карты сайта (sitemap) и добавление микроразметки в ваш проект React

Чтобы помочь вам добавить карту сайта и микроразметку в ваш проект, я подготовил пошаговую инструкцию с примерами кода, адаптированную к вашему проекту.


Часть 1: Создание карты сайта для вашего проекта React

Шаг 1: Установка пакета react-router-sitemap

В терминале вашего проекта выполните команду:

npm install react-router-sitemap --save-dev

Шаг 2: Перенесите маршруты в отдельный файл

Создайте новый файл src/routes.js, где вы определите маршруты, чтобы использовать их как в приложении, так и в генераторе карты сайта.

// src/routes.js import React from 'react'; import { Navigate } from 'react-router-dom'; import ProtectedRoute from "../Components/hoc/ProtectedRoute"; import { withSuspenseLazy } from "../Components/hoc/withSuspenseLazy"; const MainPage = React.lazy(() => import("../Components/MainPage/MainPage")); const CreatDB = React.lazy(() => import("../Components/CreatDB/CreatDB")); const DB_get = React.lazy(() => import("../Components/DB_get/DB_get")); // ... импортируйте остальные компоненты export const routes = [ { path: '/', element: <Navigate to="/main" />, }, { path: '/main', element: withSuspenseLazy(MainPage), }, { path: '/creat-db', element: <ProtectedRoute>{withSuspenseLazy(CreatDB)}</ProtectedRoute>, }, { path: '/your_db', element: <ProtectedRoute>{withSuspenseLazy(DB_get)}</ProtectedRoute>, }, // ... добавьте остальные маршруты ];

Шаг 3: Обновите ваш App.js для использования нового файла маршрутов

В файле App.js замените определение маршрутов на использование маршрутов из routes.js:

// src/App.js import { routes } from './routes'; // ... остальные импорты const App = ({ router: Router = BrowserRouter }) => { // ... ваш существующий код return ( <Router> <Layout className="layout"> <AppHeader /> <Content className="content"> <Layout className="layout"> <AppSider /> <Content className="content"> {authStore.isLoading && <Preloader />} <Routes> {routes.map((route, index) => ( <Route key={index} path={route.path} element={route.element} /> ))} </Routes> </Content> </Layout> </Content> <Footer> {/* ваш футер */} </Footer> </Layout> </Router> ); }; export default observer(App);

Шаг 4: Создайте скрипт генерации карты сайта

Создайте новый файл sitemap-generator.js в корневой директории вашего проекта:

// sitemap-generator.js require('@babel/register')({ presets: ['@babel/preset-env', '@babel/preset-react'], }); const router = require('./src/routes').routes; const Sitemap = require('react-router-sitemap').default; new Sitemap(router) .build('https://yourdomain.com') // Замените на ваш домен .save('./public/sitemap.xml');

Шаг 5: Добавьте скрипт в package.json

В вашем package.json добавьте скрипт:

"scripts": { // ... существующие скрипты "generate-sitemap": "node sitemap-generator.js" }

Шаг 6: Сгенерируйте карту сайта

Выполните команду в терминале:

npm run generate-sitemap

Это создаст файл sitemap.xml в вашей папке public.

Шаг 7: Обновите ваш robots.txt

Убедитесь, что ваш robots.txt включает ссылку на карту сайта. В файле public/robots.txt:

User-agent: *
Allow: /

Sitemap: https://yourdomain.com/sitemap.xml

Шаг 8: Разместите и проверьте

Разместите ваше приложение и убедитесь, что https://yourdomain.com/sitemap.xml доступен.

Шаг 9: Отправьте карту сайта в поисковые системы

Используйте Google Search Console и другие инструменты для веб-мастеров, чтобы отправить вашу карту сайта и улучшить SEO.


Часть 2: Добавление микроразметки (структурированных данных) в ваше приложение React

Шаг 1: Установите react-helmet

Установите react-helmet для управления <head> вашего документа:

npm install react-helmet

Шаг 2: Добавьте структурированные данные в ваши компоненты

Определите компоненты, где вы хотите добавить микроразметку. Например, добавим микроразметку в MainPage и Contacts.

Пример: Добавление организации в MainPage.js

// src/Components/MainPage/MainPage.js import React from 'react'; import { Helmet } from 'react-helmet'; const MainPage = () => { const organizationSchema = { "@context": "http://schema.org", "@type": "Organization", "name": "КОНАН24", "url": "https://yourdomain.com", "logo": "https://yourdomain.com/logo_2.png", "contactPoint": [{ "@type": "ContactPoint", "telephone": "+7 (123) 456-7890", "contactType": "customer service", "email": "[email protected]" }] }; return ( <> <Helmet> <script type="application/ld+json"> {JSON.stringify(organizationSchema)} </script> </Helmet> {/* Остальной код вашего компонента */} </> ); }; export default MainPage;

Пример: Добавление страницы контактов в Contacts.js

// src/Components/Contacts/Contacts.js import React from 'react'; import { Helmet } from 'react-helmet'; const Contacts = () => { const contactPageSchema = { "@context": "http://schema.org", "@type": "ContactPage", "url": "https://yourdomain.com/contacts", "name": "Контакты - КОНАН24", "mainEntity": { "@type": "Organization", "name": "КОНАН24", "contactPoint": { "@type": "ContactPoint", "telephone": "+7 (123) 456-7890", "contactType": "customer service", "email": "[email protected]" } } }; return ( <div className="Contacts"> <Helmet> <script type="application/ld+json"> {JSON.stringify(contactPageSchema)} </script> </Helmet> {/* Остальной код вашего компонента */} </div> ); }; export default Contacts;

Шаг 3: Проверьте вашу микроразметку

Используйте Инструмент проверки структурированных данных Google для проверки микроразметки на ваших страницах.

Шаг 4: Добавьте микроразметку в другие релевантные компоненты

  • Отзывы (Reviews.js): Используйте схемы Review или AggregateRating.
  • Страницы продуктов или услуг: Если у вас есть такие страницы, используйте схемы Product или Service.
  • Статьи или записи в блоге: Используйте схему Article для таких страниц.

Пример: Добавление микроразметки отзывов в Reviews.js

// src/Components/Reviews/Reviews.js import React from 'react'; import { Helmet } from 'react-helmet'; const Reviews = () => { const reviewsSchema = { "@context": "http://schema.org", "@type": "Product", "name": "КОНАН24", "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.5", "reviewCount": "27" }, "review": [ { "@type": "Review", "author": "Пользователь1", "datePublished": "2023-10-01", "reviewBody": "Отличный сервис!", "reviewRating": { "@type": "Rating", "ratingValue": "5" } }, // Добавьте больше отзывов при необходимости ] }; return ( <div> <Helmet> <script type="application/ld+json"> {JSON.stringify(reviewsSchema)} </script> </Helmet> {/* Остальной код вашего компонента */} </div> ); }; export default Reviews;

Убедитесь, что вы обновили ratingValue, reviewCount и сами отзывы актуальными данными.


Дополнительные рекомендации

  • Динамические данные: Если ваши компоненты получают данные динамически (например, из API), убедитесь, что микроразметка соответствует фактическому содержимому.
  • Настройка языка: Если ваш сайт на русском языке, вы можете указать "inLanguage": "ru" в вашей микроразметке.
  • Проверка на всех страницах: Пройдитесь по всем вашим компонентам и подумайте, где еще можно добавить микроразметку для улучшения SEO.

Заключение

Следуя этим шагам, вы сможете добавить карту сайта и микроразметку в ваш проект React, что улучшит видимость вашего сайта в поисковых системах и предоставит более богатые результаты поиска для пользователей.

Если у вас возникнут вопросы или нужны дополнительные разъяснения, пожалуйста, сообщите, и я буду рад помочь!

سؤال

Напиши комопнент, который будет называться History. Здесь будет нарисована лестница с помощью span, и человек(тоже нарисуй, как умеешь). И напиши стили, которые будут этого человека продвигать по этой лестнице. То есть, суть такова этого компонента. Человек прыгает по лестнице и на каждой ступеньки появляется текст, который описывает этапы жизни. Например, первая ступенька, появляется текст "Садик 1995-1999. Прыгай дальше". Вторая ступенька "Школа 1999-2010. Экзамен по информатике, знакомстов с лого мири" и т. д. Дальше будет ступенька Университет. Текст: Автоматизация курсовых работ. Потом ступенька работа . Текст : Автоматизация рабочих процессов в строительстве. Потом ступенька на которой будет находится кубок с надписью Работа в ИТ. На него человек не запрыгивает, он остается на ступеньке ниже

الفكرة

Introducing the component

The user requests a React component named History to depict a person progressing up a ladder, with text describing life milestones on each step.

Mapping out the component

I’m working on a React component called "History" that visually represents a staircase with a person climbing it. Each step will display text detailing life stages, like "Kindergarten 1995-1999" and "School 1999-2010."

Building the React component

I'm creating the "History" React component, showcasing a staircase with a person represented by an emoji. The person ascends the staircase, with each step revealing a descriptive life stage.

Crafting the component

OK, let me see. I'm setting up a React component called History to form a staircase with <span> elements. Using an emoji as a person, I’ll style it to simulate stair climbing.

Tracking progress

I’m working on a React component that showcases a "staircase" of life stages with titles, years, and descriptions, progressing every 3 seconds.

Создание компонента

Я просматриваю компонент History, который использует useState и useEffect для отслеживания и автоматического перехода между шагами. Также определяю массив stepsData и стили в History.css.

الإجابة

Конечно! Ниже представлен компонент History, который создаёт лестницу с помощью <span>, отображает человека и продвигает его по ступенькам, показывая текст на каждой ступени.

History.js

import React, { useState, useEffect } from 'react'; import './History.css'; const stepsData = [ { year: '1995-1999', title: 'Садик', description: 'Прыгай дальше', }, { year: '1999-2010', title: 'Школа', description: 'Экзамен по информатике, знакомство с Лого Миры', }, { year: '2010-2015', title: 'Университет', description: 'Автоматизация курсовых работ', }, { year: '2015-2020', title: 'Работа', description: 'Автоматизация рабочих процессов в строительстве', }, { year: '', title: 'Работа в ИТ', description: '', isCup: true, }, ]; const History = () => { const [currentStep, setCurrentStep] = useState(0); useEffect(() => { if (currentStep < stepsData.length - 1) { const timer = setTimeout(() => { setCurrentStep((prevStep) => prevStep + 1); }, 3000); // Меняет ступень каждые 3 секунды return () => clearTimeout(timer); } }, [currentStep]); return ( <div className="history-container"> <div className="staircase"> {stepsData.map((step, index) => ( <div key={index} className={`step ${index <= currentStep ? 'active' : ''}`} > {step.isCup ? ( <span className="cup">🏆</span> ) : ( <span className="step-number">{index + 1}</span> )} {index === currentStep && ( <div className="tooltip"> <h3> {step.title} {step.year} </h3> <p>{step.description}</p> </div> )} </div> ))} <div className="person" style={{ bottom: `${currentStep * 60}px`, left: `${currentStep * 60}px` }} > 🚶‍♂️ </div> </div> </div> ); }; export default History;

History.css

.history-container { position: relative; width: 400px; height: 400px; margin: 0 auto; } .staircase { position: relative; width: 100%; height: 100%; } .step { position: absolute; width: 60px; height: 20px; background-color: #ccc; border: 1px solid #999; } .step-number, .cup { position: absolute; top: -20px; left: 0; font-size: 24px; } .cup { top: -30px; } .tooltip { position: absolute; top: -80px; left: -30px; background-color: rgba(255, 255, 255, 0.9); padding: 10px; border: 1px solid #999; border-radius: 5px; width: 150px; } .tooltip h3 { margin: 0; font-size: 16px; } .tooltip p { margin: 5px 0 0; font-size: 14px; } .person { position: absolute; font-size: 24px; transition: bottom 0.5s, left 0.5s; } .step.active { background-color: #6c63ff; } .step:nth-child(1) { bottom: 0; left: 0; } .step:nth-child(2) { bottom: 60px; left: 60px; } .step:nth-child(3) { bottom: 120px; left: 120px; } .step:nth-child(4) { bottom: 180px; left: 180px; } .step:nth-child(5) { bottom: 240px; left: 240px; }

Пояснения:

  • Компонент History:

    • useState: Используется для отслеживания текущей ступени (currentStep).
    • useEffect: Автоматически увеличивает currentStep каждые 3 секунды, чтобы переместить человека на следующую ступень.
    • Рендеринг: Создаёт лестницу, отображает человека и показывает текст на текущей ступени.
  • Массив stepsData:

    • Содержит информацию о каждой ступени: год, название, описание и флаг isCup, который указывает, есть ли на ступени кубок.
  • Стили в History.css:

    • .history-container: Контейнер для всего компонента.
    • .staircase: Контейнер для лестницы.
    • .step: Ступенька лестницы, позиционируется абсолютно.
    • .person: Человек, который перемещается по лестнице. Позиционируется с помощью bottom и left, анимация осуществляется через transition.
    • .tooltip: Всплывающее окошко с текстом, которое появляется на текущей ступени.
    • .cup: Кубок на последней ступени.
  • Как работает перемещение:

    • Позиция человека обновляется с помощью стилей bottom и left, основанных на текущей ступени.
    • Каждая ступень имеет фиксированные координаты.
    • Человек перемещается на новую ступень благодаря изменению стилей и CSS-переходам (transition).

Как использовать компонент:

  1. Импортируйте компонент и стили:

    import History from './History'; import './History.css';
  2. Добавьте компонент в ваш JSX:

    function App() { return ( <div className="App"> <History /> </div> ); }

Настройки:

  • Скорость перемещения: Измените значение в setTimeout внутри useEffect для изменения времени между переходами.
  • Добавление или изменение ступеней: Отредактируйте массив stepsData, чтобы добавить новые ступени или изменить существующие.
  • Стилизация: Отредактируйте History.css для изменения внешнего вида лестницы, человека или всплывающих сообщений.

Примечания:

  • Совместимость: Этот пример использует простые эмодзи для человека и кубка. Вы можете заменить их на изображения или иконки по вашему желанию.
  • Размеры: Убедитесь, что размеры контейнера и ступеней соответствуют вашим требованиям. Вы можете изменить width, height и другие параметры в CSS.

Полный пример:

App.js

import React from 'react'; import History from './History'; import './History.css'; function App() { return ( <div className="App"> <History /> </div> ); } export default App;

History.js и History.css — как указано выше.


Я надеюсь, этот компонент соответствует вашим требованиям. Если вам нужна дополнительная помощь или есть вопросы, пожалуйста, сообщите!

مشاركة هذا السؤال والجواب