diff --git a/frontend/src/components/Paginator.js b/frontend/src/components/Paginator.js
new file mode 100644
index 0000000..c3d43df
--- /dev/null
+++ b/frontend/src/components/Paginator.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import styles from '../styles/Paginator.module.css';
+
+export default function Paginator({ page, total, pageSize, onPageChange, className }) {
+ const pageCount = Math.ceil(total / pageSize) || 1;
+ if (pageCount <= 1) return null;
+ return (
+
+
+ {Array.from({ length: pageCount }, (_, i) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateCampaignModal.js b/frontend/src/modals/CreateCampaignModal.js
new file mode 100644
index 0000000..7223231
--- /dev/null
+++ b/frontend/src/modals/CreateCampaignModal.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/CampaignModal.module.css';
+
+export default function CreateCampaignModal({ isOpen, onClose, campaign, groups, versions, loading, onChange, onSave, getVersionName }) {
+ return (
+
+ Добавить кампанию
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateGroupModal.js b/frontend/src/modals/CreateGroupModal.js
new file mode 100644
index 0000000..3c42bd9
--- /dev/null
+++ b/frontend/src/modals/CreateGroupModal.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/GroupModal.module.css';
+
+export default function CreateGroupModal({ isOpen, onClose, group, loading, onChange, onSave }) {
+ return (
+
+ Добавить группу
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateSmtpModal.js b/frontend/src/modals/CreateSmtpModal.js
new file mode 100644
index 0000000..ba488ce
--- /dev/null
+++ b/frontend/src/modals/CreateSmtpModal.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/SmtpModal.module.css';
+
+export default function CreateSmtpModal({ isOpen, onClose, server, loading, onChange, onSave }) {
+ return (
+
+ Добавить SMTP-сервер
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateSubscriberModal.js b/frontend/src/modals/CreateSubscriberModal.js
new file mode 100644
index 0000000..8af97a6
--- /dev/null
+++ b/frontend/src/modals/CreateSubscriberModal.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/SubscriberModal.module.css';
+
+export default function CreateSubscriberModal({ isOpen, onClose, sub, loading, onChange, onSave }) {
+ return (
+
+ Добавить подписчика
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateTemplateModal.js b/frontend/src/modals/CreateTemplateModal.js
new file mode 100644
index 0000000..35030b8
--- /dev/null
+++ b/frontend/src/modals/CreateTemplateModal.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/TemplateModal.module.css';
+
+export default function CreateTemplateModal({ isOpen, onClose, template, loading, onChange, onSave }) {
+ return (
+
+ Добавить шаблон
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateUnsubModal.js b/frontend/src/modals/CreateUnsubModal.js
new file mode 100644
index 0000000..55f7a14
--- /dev/null
+++ b/frontend/src/modals/CreateUnsubModal.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/UnsubModal.module.css';
+
+export default function CreateUnsubModal({ isOpen, onClose, sub, loading, onChange, onSave }) {
+ return (
+
+ Добавить запись
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/CreateUserModal.js b/frontend/src/modals/CreateUserModal.js
new file mode 100644
index 0000000..c5ae34d
--- /dev/null
+++ b/frontend/src/modals/CreateUserModal.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/UserModal.module.css';
+
+export default function CreateUserModal({ isOpen, onClose, user, roles, loading, onChange, onSave }) {
+ return (
+
+ Добавить пользователя
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditCampaignModal.js b/frontend/src/modals/EditCampaignModal.js
new file mode 100644
index 0000000..b47e1be
--- /dev/null
+++ b/frontend/src/modals/EditCampaignModal.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/CampaignModal.module.css';
+
+export default function EditCampaignModal({ isOpen, onClose, campaign, groups, versions, loading, onChange, onSave, getVersionName }) {
+ return (
+
+ Редактировать кампанию
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditGroupModal.js b/frontend/src/modals/EditGroupModal.js
new file mode 100644
index 0000000..1431118
--- /dev/null
+++ b/frontend/src/modals/EditGroupModal.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/GroupModal.module.css';
+
+export default function EditGroupModal({ isOpen, onClose, group, loading, onChange, onSave }) {
+ return (
+
+ Редактировать группу
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditSmtpModal.js b/frontend/src/modals/EditSmtpModal.js
new file mode 100644
index 0000000..a923b4a
--- /dev/null
+++ b/frontend/src/modals/EditSmtpModal.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/SmtpModal.module.css';
+
+export default function EditSmtpModal({ isOpen, onClose, server, loading, onChange, onSave }) {
+ return (
+
+ Редактировать SMTP-сервер
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditSubscriberModal.js b/frontend/src/modals/EditSubscriberModal.js
new file mode 100644
index 0000000..bf5e23f
--- /dev/null
+++ b/frontend/src/modals/EditSubscriberModal.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/SubscriberModal.module.css';
+
+export default function EditSubscriberModal({ isOpen, onClose, sub, loading, onChange, onSave }) {
+ return (
+
+ Редактировать подписчика
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditTemplateModal.js b/frontend/src/modals/EditTemplateModal.js
new file mode 100644
index 0000000..4d38f9b
--- /dev/null
+++ b/frontend/src/modals/EditTemplateModal.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/TemplateModal.module.css';
+
+export default function EditTemplateModal({ isOpen, onClose, template, loading, onChange, onSave }) {
+ return (
+
+ Редактировать шаблон
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditUnsubModal.js b/frontend/src/modals/EditUnsubModal.js
new file mode 100644
index 0000000..3d64868
--- /dev/null
+++ b/frontend/src/modals/EditUnsubModal.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/UnsubModal.module.css';
+
+export default function EditUnsubModal({ isOpen, onClose, sub, loading, onChange, onSave }) {
+ return (
+
+ Редактировать запись
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/EditUserModal.js b/frontend/src/modals/EditUserModal.js
new file mode 100644
index 0000000..268f5a5
--- /dev/null
+++ b/frontend/src/modals/EditUserModal.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import Modal from './Modal';
+import styles from '../styles/UserModal.module.css';
+
+export default function EditUserModal({ isOpen, onClose, user, roles, loading, onChange, onSave }) {
+ return (
+
+ Редактировать пользователя
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/modals/Modal.js b/frontend/src/modals/Modal.js
new file mode 100644
index 0000000..6c4a82f
--- /dev/null
+++ b/frontend/src/modals/Modal.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import styles from '../styles/Modal.module.css';
+
+export default function Modal({ isOpen, onClose, children, disabled }) {
+ if (!isOpen) return null;
+ return (
+
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/CampaignPage.js b/frontend/src/pages/CampaignPage.js
index fc020ad..f0e82cb 100644
--- a/frontend/src/pages/CampaignPage.js
+++ b/frontend/src/pages/CampaignPage.js
@@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import EditCampaignModal from '../modals/EditCampaignModal';
+import CreateCampaignModal from '../modals/CreateCampaignModal';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -228,126 +231,39 @@ function CampaignPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editCampaign && (
-
-
-
-
Редактировать кампанию
-
-
-
+ setEditCampaign(null)}
+ campaign={editCampaign}
+ groups={groups}
+ versions={versions}
+ loading={editLoading}
+ onChange={setEditCampaign}
+ onSave={handleEditSave}
+ getVersionName={getVersionName}
+ />
)}
{createCampaign && (
-
-
-
-
Добавить кампанию
-
-
-
+ setCreateCampaign(null)}
+ campaign={createCampaign}
+ groups={groups}
+ versions={versions}
+ loading={createLoading}
+ onChange={setCreateCampaign}
+ onSave={handleCreateSave}
+ getVersionName={getVersionName}
+ />
)}
);
@@ -364,9 +280,5 @@ const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
export default CampaignPage;
\ No newline at end of file
diff --git a/frontend/src/pages/DeliveryHistoryPage.js b/frontend/src/pages/DeliveryHistoryPage.js
index 4225547..4ec00df 100644
--- a/frontend/src/pages/DeliveryHistoryPage.js
+++ b/frontend/src/pages/DeliveryHistoryPage.js
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -80,25 +81,12 @@ function DeliveryHistoryPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
@@ -107,9 +95,5 @@ function DeliveryHistoryPage() {
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
const tdStyle = { padding: '10px 16px', background: '#fff' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
export default DeliveryHistoryPage;
\ No newline at end of file
diff --git a/frontend/src/pages/EmailTemplatesPage.js b/frontend/src/pages/EmailTemplatesPage.js
index 8d12560..3f7ea73 100644
--- a/frontend/src/pages/EmailTemplatesPage.js
+++ b/frontend/src/pages/EmailTemplatesPage.js
@@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import EditTemplateModal from '../modals/EditTemplateModal';
+import CreateTemplateModal from '../modals/CreateTemplateModal';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -164,76 +167,33 @@ function EmailTemplatesPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editTemplate && (
-
-
-
-
Редактировать шаблон
-
-
-
+ setEditTemplate(null)}
+ template={editTemplate}
+ loading={editLoading}
+ onChange={setEditTemplate}
+ onSave={handleEditSave}
+ />
)}
{createTemplate && (
-
-
-
-
Добавить шаблон
-
-
-
+ setCreateTemplate(null)}
+ template={createTemplate}
+ loading={createLoading}
+ onChange={setCreateTemplate}
+ onSave={handleCreateSave}
+ />
)}
);
@@ -249,10 +209,6 @@ const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: '
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
export default EmailTemplatesPage;
\ No newline at end of file
diff --git a/frontend/src/pages/GroupsPage.js b/frontend/src/pages/GroupsPage.js
index 59fb6c4..815f4a4 100644
--- a/frontend/src/pages/GroupsPage.js
+++ b/frontend/src/pages/GroupsPage.js
@@ -1,5 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import EditGroupModal from '../modals/EditGroupModal';
+import CreateGroupModal from '../modals/CreateGroupModal';
+import EditSubscriberModal from '../modals/EditSubscriberModal';
+import CreateSubscriberModal from '../modals/CreateSubscriberModal';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -171,82 +176,33 @@ function GroupsPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editGroup && (
-
-
-
-
Редактировать группу
-
-
-
+ setEditGroup(null)}
+ group={editGroup}
+ loading={editLoading}
+ onChange={setEditGroup}
+ onSave={handleEditSave}
+ />
)}
{createGroup && (
-
-
-
-
Добавить группу
-
-
-
+ setCreateGroup(null)}
+ group={createGroup}
+ loading={createLoading}
+ onChange={setCreateGroup}
+ onSave={handleCreateSave}
+ />
)}
);
@@ -433,96 +389,33 @@ function SubscribersInGroupPage({ group, onBack, token }) {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editSub && (
-
-
-
-
Редактировать подписчика
-
-
-
+ setEditSub(null)}
+ sub={editSub}
+ loading={editLoading}
+ onChange={setEditSub}
+ onSave={handleEditSave}
+ />
)}
{createSub && (
-
-
-
-
Добавить подписчика
-
-
-
+ setCreateSub(null)}
+ sub={createSub}
+ loading={createLoading}
+ onChange={setCreateSub}
+ onSave={handleCreateSave}
+ />
)}
);
@@ -539,9 +432,5 @@ const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
export default GroupsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/SmtpServersPage.js b/frontend/src/pages/SmtpServersPage.js
index c2d533e..46f97ac 100644
--- a/frontend/src/pages/SmtpServersPage.js
+++ b/frontend/src/pages/SmtpServersPage.js
@@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import EditSmtpModal from '../modals/EditSmtpModal';
+import CreateSmtpModal from '../modals/CreateSmtpModal';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -178,118 +181,33 @@ function SmtpServersPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editServer && (
-
-
-
-
Редактировать SMTP-сервер
-
-
-
+ setEditServer(null)}
+ server={editServer}
+ loading={editLoading}
+ onChange={setEditServer}
+ onSave={handleEditSave}
+ />
)}
{createServer && (
-
-
-
-
Добавить SMTP-сервер
-
-
-
+ setCreateServer(null)}
+ server={createServer}
+ loading={createLoading}
+ onChange={setCreateServer}
+ onSave={handleCreateSave}
+ />
)}
);
@@ -305,10 +223,6 @@ const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: '
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
export default SmtpServersPage;
\ No newline at end of file
diff --git a/frontend/src/pages/UnsubscribedPage.js b/frontend/src/pages/UnsubscribedPage.js
index 440d49d..c46a6af 100644
--- a/frontend/src/pages/UnsubscribedPage.js
+++ b/frontend/src/pages/UnsubscribedPage.js
@@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import EditUnsubModal from '../modals/EditUnsubModal';
+import CreateUnsubModal from '../modals/CreateUnsubModal';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -168,82 +171,33 @@ function UnsubscribedPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editSub && (
-
-
-
-
Редактировать запись
-
-
-
+ setEditSub(null)}
+ sub={editSub}
+ loading={editLoading}
+ onChange={setEditSub}
+ onSave={handleEditSave}
+ />
)}
{createSub && (
-
-
-
-
Добавить запись
-
-
-
+ setCreateSub(null)}
+ sub={createSub}
+ loading={createLoading}
+ onChange={setCreateSub}
+ onSave={handleCreateSave}
+ />
)}
);
@@ -260,9 +214,5 @@ const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
export default UnsubscribedPage;
\ No newline at end of file
diff --git a/frontend/src/pages/UsersPage.js b/frontend/src/pages/UsersPage.js
index ce222f3..6766b8d 100644
--- a/frontend/src/pages/UsersPage.js
+++ b/frontend/src/pages/UsersPage.js
@@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
+import EditUserModal from '../modals/EditUserModal';
+import CreateUserModal from '../modals/CreateUserModal';
+import Paginator from '../components/Paginator';
const PAGE_SIZE = 10;
@@ -199,99 +202,35 @@ function UsersPage() {
)}
-
-
-
- {Array.from({ length: Math.ceil(usersTotal / PAGE_SIZE) || 1 }, (_, i) => (
-
- ))}
-
-
-
+
>
)}
{editUser && (
-
-
-
-
Редактировать пользователя
-
-
-
+ setEditUser(null)}
+ user={editUser}
+ roles={roles}
+ loading={editLoading}
+ onChange={setEditUser}
+ onSave={handleEditSave}
+ />
)}
{createUser && (
-
-
-
-
Добавить пользователя
-
-
-
+ setCreateUser(null)}
+ user={createUser}
+ roles={roles}
+ loading={createLoading}
+ onChange={setCreateUser}
+ onSave={handleCreateSave}
+ />
)}
);
@@ -308,11 +247,6 @@ const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
-const paginatorWrapperStyle = { marginTop: 24, display: 'flex', gap: 6, alignItems: 'center', justifyContent: 'flex-end' };
-const paginatorBtnStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 18, fontWeight: 700, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageStyle = { border: 'none', background: '#f3f4f6', color: '#6366f1', borderRadius: '50%', width: 36, height: 36, fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.18s, color 0.18s', boxShadow: '0 1px 4px 0 rgba(99,102,241,0.06)' };
-const paginatorPageActiveStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', cursor: 'default', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)' };
-
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
export default UsersPage;
\ No newline at end of file
diff --git a/frontend/src/styles/CampaignModal.module.css b/frontend/src/styles/CampaignModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/CampaignModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file
diff --git a/frontend/src/styles/GroupModal.module.css b/frontend/src/styles/GroupModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/GroupModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file
diff --git a/frontend/src/styles/Modal.module.css b/frontend/src/styles/Modal.module.css
new file mode 100644
index 0000000..822ebe0
--- /dev/null
+++ b/frontend/src/styles/Modal.module.css
@@ -0,0 +1,44 @@
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0,0,0,0.18);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal {
+ background: #fff;
+ border-radius: 14px;
+ padding: 32px 28px 24px 28px;
+ min-width: 340px;
+ box-shadow: 0 12px 48px 0 rgba(31,38,135,0.22);
+ position: relative;
+ animation: modalIn 0.18s cubic-bezier(.4,1.3,.6,1);
+}
+
+.closeBtn {
+ position: absolute;
+ top: 12px;
+ right: 16px;
+ background: none;
+ border: none;
+ font-size: 26px;
+ color: #6366f1;
+ cursor: pointer;
+ font-weight: 700;
+ line-height: 1;
+ padding: 0;
+ z-index: 2;
+ opacity: 0.8;
+ transition: opacity 0.2s;
+}
+
+@keyframes modalIn {
+ from { transform: translateY(40px) scale(0.98); opacity: 0; }
+ to { transform: none; opacity: 1; }
+}
\ No newline at end of file
diff --git a/frontend/src/styles/Paginator.module.css b/frontend/src/styles/Paginator.module.css
new file mode 100644
index 0000000..26d271a
--- /dev/null
+++ b/frontend/src/styles/Paginator.module.css
@@ -0,0 +1,39 @@
+.wrapper {
+ margin-top: 24px;
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ justify-content: flex-end;
+}
+.btn {
+ border: none;
+ background: #f3f4f6;
+ color: #6366f1;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ font-size: 18px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: background 0.18s, color 0.18s;
+ box-shadow: 0 1px 4px 0 rgba(99,102,241,0.06);
+}
+.page {
+ border: none;
+ background: #f3f4f6;
+ color: #6366f1;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.18s, color 0.18s;
+ box-shadow: 0 1px 4px 0 rgba(99,102,241,0.06);
+}
+.pageActive {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ cursor: default;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+}
\ No newline at end of file
diff --git a/frontend/src/styles/SmtpModal.module.css b/frontend/src/styles/SmtpModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/SmtpModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file
diff --git a/frontend/src/styles/SubscriberModal.module.css b/frontend/src/styles/SubscriberModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/SubscriberModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file
diff --git a/frontend/src/styles/TemplateModal.module.css b/frontend/src/styles/TemplateModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/TemplateModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file
diff --git a/frontend/src/styles/UnsubModal.module.css b/frontend/src/styles/UnsubModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/UnsubModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file
diff --git a/frontend/src/styles/UserModal.module.css b/frontend/src/styles/UserModal.module.css
new file mode 100644
index 0000000..1936cfc
--- /dev/null
+++ b/frontend/src/styles/UserModal.module.css
@@ -0,0 +1,59 @@
+.title {
+ margin: 0 0 18px 0;
+ text-align: center;
+ color: #3730a3;
+ font-weight: 700;
+}
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ min-width: 260px;
+}
+.label {
+ font-weight: 500;
+ color: #374151;
+ font-size: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.input {
+ margin-top: 4px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1.5px solid #c7d2fe;
+ font-size: 16px;
+ outline: none;
+ background: #f8fafc;
+ transition: border 0.2s;
+}
+.actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 10px;
+ justify-content: flex-end;
+}
+.saveBtn {
+ background: linear-gradient(90deg, #6366f1 0%, #06b6d4 100%);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 22px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 2px 8px 0 rgba(99,102,241,0.10);
+ transition: background 0.2s;
+}
+.cancelBtn {
+ background: #f3f4f6;
+ color: #6366f1;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 18px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
\ No newline at end of file