style and modals

This commit is contained in:
romantarkin 2025-07-23 14:14:34 +05:00
parent bee42c5b16
commit f3aeaad948
32 changed files with 1171 additions and 650 deletions

View File

@ -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 (
<div className={className || styles.wrapper}>
<button
className={styles.btn}
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
style={{ opacity: page <= 1 ? 0.5 : 1 }}
>
</button>
{Array.from({ length: pageCount }, (_, i) => (
<button
key={i + 1}
className={page === i + 1 ? styles.pageActive : styles.page}
onClick={() => onPageChange(i + 1)}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button
className={styles.btn}
onClick={() => onPageChange(page + 1)}
disabled={page >= pageCount}
style={{ opacity: page >= pageCount ? 0.5 : 1 }}
>
</button>
</div>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить кампанию</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Группа
<select value={campaign.group_id} onChange={e => onChange({ ...campaign, group_id: Number(e.target.value) })} required className={styles.input}>
{groups.map(g => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</label>
<label className={styles.label}>Версия шаблона
<select value={campaign.template_version_id} onChange={e => onChange({ ...campaign, template_version_id: Number(e.target.value) })} required className={styles.input}>
{versions.map(v => (
<option key={v.id} value={v.id}>{getVersionName(v.id)}</option>
))}
</select>
</label>
<label className={styles.label}>Тема
<input type="text" value={campaign.subject_override || ''} onChange={e => onChange({ ...campaign, subject_override: e.target.value })} className={styles.input} />
</label>
<label className={styles.label}>Статус
<select value={campaign.status} onChange={e => onChange({ ...campaign, status: e.target.value })} required className={styles.input}>
<option value="draft">Черновик</option>
<option value="scheduled">Запланировано</option>
<option value="sent">Отправлено</option>
<option value="failed">Ошибка</option>
</select>
</label>
<label className={styles.label}>Запланировано на
<input type="datetime-local" value={campaign.scheduled_at} onChange={e => onChange({ ...campaign, scheduled_at: e.target.value })} className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить группу</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Название
<input type="text" value={group.name} onChange={e => onChange({ ...group, name: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Описание
<input type="text" value={group.description || ''} onChange={e => onChange({ ...group, description: e.target.value })} className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить SMTP-сервер</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Название
<input type="text" value={server.name} onChange={e => onChange({ ...server, name: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Host
<input type="text" value={server.host} onChange={e => onChange({ ...server, host: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Port
<input type="number" value={server.port} onChange={e => onChange({ ...server, port: Number(e.target.value) })} required className={styles.input} />
</label>
<label className={styles.label}>Secure
<select value={server.secure ? '1' : '0'} onChange={e => onChange({ ...server, secure: e.target.value === '1' })} className={styles.input}>
<option value="0">Нет</option>
<option value="1">Да</option>
</select>
</label>
<label className={styles.label}>Пользователь
<input type="text" value={server.username} onChange={e => onChange({ ...server, username: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Пароль
<input type="password" value={server.password} onChange={e => onChange({ ...server, password: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Отправитель (from_email)
<input type="email" value={server.from_email} onChange={e => onChange({ ...server, from_email: e.target.value })} required className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить подписчика</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Email
<input type="email" value={sub.email} onChange={e => onChange({ ...sub, email: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Имя
<input type="text" value={sub.name || ''} onChange={e => onChange({ ...sub, name: e.target.value })} className={styles.input} />
</label>
<label className={styles.label}>Статус
<select value={sub.status} onChange={e => onChange({ ...sub, status: e.target.value })} required className={styles.input}>
<option value="active">Активен</option>
<option value="unsubscribed">Отписан</option>
<option value="bounced">Ошибка</option>
</select>
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить шаблон</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Название
<input type="text" value={template.name} onChange={e => onChange({ ...template, name: e.target.value })} required className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить запись</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Email
<input type="email" value={sub.email} onChange={e => onChange({ ...sub, email: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Имя
<input type="text" value={sub.name || ''} onChange={e => onChange({ ...sub, name: e.target.value })} className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Добавить пользователя</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Email
<input type="email" value={user.email} onChange={e => onChange({ ...user, email: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Имя
<input type="text" value={user.name} onChange={e => onChange({ ...user, name: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Роль
<select value={user.role_id} onChange={e => onChange({ ...user, role_id: Number(e.target.value) })} required className={styles.input}>
{roles.map(role => (
<option key={role.id} value={role.id}>{role.name}</option>
))}
</select>
</label>
<label className={styles.label}>Пароль
<input type="password" value={user.password} onChange={e => onChange({ ...user, password: e.target.value })} required className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать кампанию</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Группа
<select value={campaign.group_id} onChange={e => onChange({ ...campaign, group_id: Number(e.target.value) })} required className={styles.input}>
{groups.map(g => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</label>
<label className={styles.label}>Версия шаблона
<select value={campaign.template_version_id} onChange={e => onChange({ ...campaign, template_version_id: Number(e.target.value) })} required className={styles.input}>
{versions.map(v => (
<option key={v.id} value={v.id}>{getVersionName(v.id)}</option>
))}
</select>
</label>
<label className={styles.label}>Тема
<input type="text" value={campaign.subject_override || ''} onChange={e => onChange({ ...campaign, subject_override: e.target.value })} className={styles.input} />
</label>
<label className={styles.label}>Статус
<select value={campaign.status} onChange={e => onChange({ ...campaign, status: e.target.value })} required className={styles.input}>
<option value="draft">Черновик</option>
<option value="scheduled">Запланировано</option>
<option value="sent">Отправлено</option>
<option value="failed">Ошибка</option>
</select>
</label>
<label className={styles.label}>Запланировано на
<input type="datetime-local" value={campaign.scheduled_at ? new Date(campaign.scheduled_at).toISOString().slice(0,16) : ''} onChange={e => onChange({ ...campaign, scheduled_at: e.target.value })} className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать группу</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Название
<input type="text" value={group.name} onChange={e => onChange({ ...group, name: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Описание
<input type="text" value={group.description || ''} onChange={e => onChange({ ...group, description: e.target.value })} className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать SMTP-сервер</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Название
<input type="text" value={server.name} onChange={e => onChange({ ...server, name: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Host
<input type="text" value={server.host} onChange={e => onChange({ ...server, host: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Port
<input type="number" value={server.port} onChange={e => onChange({ ...server, port: Number(e.target.value) })} required className={styles.input} />
</label>
<label className={styles.label}>Secure
<select value={server.secure ? '1' : '0'} onChange={e => onChange({ ...server, secure: e.target.value === '1' })} className={styles.input}>
<option value="0">Нет</option>
<option value="1">Да</option>
</select>
</label>
<label className={styles.label}>Пользователь
<input type="text" value={server.username} onChange={e => onChange({ ...server, username: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Пароль
<input type="password" value={server.password} onChange={e => onChange({ ...server, password: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Отправитель (from_email)
<input type="email" value={server.from_email} onChange={e => onChange({ ...server, from_email: e.target.value })} required className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать подписчика</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Email
<input type="email" value={sub.email} onChange={e => onChange({ ...sub, email: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Имя
<input type="text" value={sub.name || ''} onChange={e => onChange({ ...sub, name: e.target.value })} className={styles.input} />
</label>
<label className={styles.label}>Статус
<select value={sub.status} onChange={e => onChange({ ...sub, status: e.target.value })} required className={styles.input}>
<option value="active">Активен</option>
<option value="unsubscribed">Отписан</option>
<option value="bounced">Ошибка</option>
</select>
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать шаблон</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Название
<input type="text" value={template.name} onChange={e => onChange({ ...template, name: e.target.value })} required className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать запись</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Email
<input type="email" value={sub.email} onChange={e => onChange({ ...sub, email: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Имя
<input type="text" value={sub.name || ''} onChange={e => onChange({ ...sub, name: e.target.value })} className={styles.input} />
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
<h3 className={styles.title}>Редактировать пользователя</h3>
<form onSubmit={onSave} className={styles.form}>
<label className={styles.label}>Email
<input type="email" value={user.email} onChange={e => onChange({ ...user, email: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Имя
<input type="text" value={user.name} onChange={e => onChange({ ...user, name: e.target.value })} required className={styles.input} />
</label>
<label className={styles.label}>Роль
<select value={user.role_id} onChange={e => onChange({ ...user, role_id: Number(e.target.value) })} required className={styles.input}>
{roles.map(role => (
<option key={role.id} value={role.id}>{role.name}</option>
))}
</select>
</label>
<div className={styles.actions}>
<button type="submit" disabled={loading} className={styles.saveBtn}>{loading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={onClose} disabled={loading} className={styles.cancelBtn}>Отмена</button>
</div>
</form>
</Modal>
);
}

View File

@ -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 (
<div className={styles.overlay}>
<div className={styles.modal}>
<button
type="button"
className={styles.closeBtn}
onClick={onClose}
aria-label="Закрыть"
disabled={disabled}
>
×
</button>
{children}
</div>
</div>
);
}

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editCampaign && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditCampaign(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать кампанию</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Группа
<select value={editCampaign.group_id} onChange={e => setEditCampaign({ ...editCampaign, group_id: Number(e.target.value) })} required style={inputStyle}>
{groups.map(g => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</label>
<label style={labelStyle}>Версия шаблона
<select value={editCampaign.template_version_id} onChange={e => setEditCampaign({ ...editCampaign, template_version_id: Number(e.target.value) })} required style={inputStyle}>
{versions.map(v => (
<option key={v.id} value={v.id}>{getVersionName(v.id)}</option>
))}
</select>
</label>
<label style={labelStyle}>Тема
<input type="text" value={editCampaign.subject_override || ''} onChange={e => setEditCampaign({ ...editCampaign, subject_override: e.target.value })} style={inputStyle} />
</label>
<label style={labelStyle}>Статус
<select value={editCampaign.status} onChange={e => setEditCampaign({ ...editCampaign, status: e.target.value })} required style={inputStyle}>
<option value="draft">Черновик</option>
<option value="scheduled">Запланировано</option>
<option value="sent">Отправлено</option>
<option value="failed">Ошибка</option>
</select>
</label>
<label style={labelStyle}>Запланировано на
<input type="datetime-local" value={editCampaign.scheduled_at ? new Date(editCampaign.scheduled_at).toISOString().slice(0,16) : ''} onChange={e => setEditCampaign({ ...editCampaign, scheduled_at: e.target.value })} style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditCampaign(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditCampaignModal
isOpen={!!editCampaign}
onClose={() => setEditCampaign(null)}
campaign={editCampaign}
groups={groups}
versions={versions}
loading={editLoading}
onChange={setEditCampaign}
onSave={handleEditSave}
getVersionName={getVersionName}
/>
)}
{createCampaign && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateCampaign(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить кампанию</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Группа
<select value={createCampaign.group_id} onChange={e => setCreateCampaign({ ...createCampaign, group_id: Number(e.target.value) })} required style={inputStyle}>
{groups.map(g => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</label>
<label style={labelStyle}>Версия шаблона
<select value={createCampaign.template_version_id} onChange={e => setCreateCampaign({ ...createCampaign, template_version_id: Number(e.target.value) })} required style={inputStyle}>
{versions.map(v => (
<option key={v.id} value={v.id}>{getVersionName(v.id)}</option>
))}
</select>
</label>
<label style={labelStyle}>Тема
<input type="text" value={createCampaign.subject_override || ''} onChange={e => setCreateCampaign({ ...createCampaign, subject_override: e.target.value })} style={inputStyle} />
</label>
<label style={labelStyle}>Статус
<select value={createCampaign.status} onChange={e => setCreateCampaign({ ...createCampaign, status: e.target.value })} required style={inputStyle}>
<option value="draft">Черновик</option>
<option value="scheduled">Запланировано</option>
<option value="sent">Отправлено</option>
<option value="failed">Ошибка</option>
</select>
</label>
<label style={labelStyle}>Запланировано на
<input type="datetime-local" value={createCampaign.scheduled_at} onChange={e => setCreateCampaign({ ...createCampaign, scheduled_at: e.target.value })} style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateCampaign(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateCampaignModal
isOpen={!!createCampaign}
onClose={() => setCreateCampaign(null)}
campaign={createCampaign}
groups={groups}
versions={versions}
loading={createLoading}
onChange={setCreateCampaign}
onSave={handleCreateSave}
getVersionName={getVersionName}
/>
)}
</div>
);
@ -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;

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
</div>
@ -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;

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editTemplate && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditTemplate(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать шаблон</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Название
<input type="text" value={editTemplate.name} onChange={e => setEditTemplate({ ...editTemplate, name: e.target.value })} required style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditTemplate(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditTemplateModal
isOpen={!!editTemplate}
onClose={() => setEditTemplate(null)}
template={editTemplate}
loading={editLoading}
onChange={setEditTemplate}
onSave={handleEditSave}
/>
)}
{createTemplate && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateTemplate(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить шаблон</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Название
<input type="text" value={createTemplate.name} onChange={e => setCreateTemplate({ ...createTemplate, name: e.target.value })} required style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateTemplate(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateTemplateModal
isOpen={!!createTemplate}
onClose={() => setCreateTemplate(null)}
template={createTemplate}
loading={createLoading}
onChange={setCreateTemplate}
onSave={handleCreateSave}
/>
)}
</div>
);
@ -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;

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editGroup && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditGroup(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать группу</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Название
<input type="text" value={editGroup.name} onChange={e => setEditGroup({ ...editGroup, name: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Описание
<input type="text" value={editGroup.description || ''} onChange={e => setEditGroup({ ...editGroup, description: e.target.value })} style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditGroup(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditGroupModal
isOpen={!!editGroup}
onClose={() => setEditGroup(null)}
group={editGroup}
loading={editLoading}
onChange={setEditGroup}
onSave={handleEditSave}
/>
)}
{createGroup && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateGroup(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить группу</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Название
<input type="text" value={createGroup.name} onChange={e => setCreateGroup({ ...createGroup, name: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Описание
<input type="text" value={createGroup.description || ''} onChange={e => setCreateGroup({ ...createGroup, description: e.target.value })} style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateGroup(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateGroupModal
isOpen={!!createGroup}
onClose={() => setCreateGroup(null)}
group={createGroup}
loading={createLoading}
onChange={setCreateGroup}
onSave={handleCreateSave}
/>
)}
</div>
);
@ -433,96 +389,33 @@ function SubscribersInGroupPage({ group, onBack, token }) {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editSub && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditSub(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать подписчика</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Email
<input type="email" value={editSub.email} onChange={e => setEditSub({ ...editSub, email: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Имя
<input type="text" value={editSub.name || ''} onChange={e => setEditSub({ ...editSub, name: e.target.value })} style={inputStyle} />
</label>
<label style={labelStyle}>Статус
<select value={editSub.status} onChange={e => setEditSub({ ...editSub, status: e.target.value })} required style={inputStyle}>
<option value="active">Активен</option>
<option value="unsubscribed">Отписан</option>
<option value="bounced">Ошибка</option>
</select>
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditSub(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditSubscriberModal
isOpen={!!editSub}
onClose={() => setEditSub(null)}
sub={editSub}
loading={editLoading}
onChange={setEditSub}
onSave={handleEditSave}
/>
)}
{createSub && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateSub(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить подписчика</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Email
<input type="email" value={createSub.email} onChange={e => setCreateSub({ ...createSub, email: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Имя
<input type="text" value={createSub.name || ''} onChange={e => setCreateSub({ ...createSub, name: e.target.value })} style={inputStyle} />
</label>
<label style={labelStyle}>Статус
<select value={createSub.status} onChange={e => setCreateSub({ ...createSub, status: e.target.value })} required style={inputStyle}>
<option value="active">Активен</option>
<option value="unsubscribed">Отписан</option>
<option value="bounced">Ошибка</option>
</select>
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateSub(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateSubscriberModal
isOpen={!!createSub}
onClose={() => setCreateSub(null)}
sub={createSub}
loading={createLoading}
onChange={setCreateSub}
onSave={handleCreateSave}
/>
)}
</div>
);
@ -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;

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editServer && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditServer(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать SMTP-сервер</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Название
<input type="text" value={editServer.name} onChange={e => setEditServer({ ...editServer, name: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Host
<input type="text" value={editServer.host} onChange={e => setEditServer({ ...editServer, host: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Port
<input type="number" value={editServer.port} onChange={e => setEditServer({ ...editServer, port: Number(e.target.value) })} required style={inputStyle} />
</label>
<label style={labelStyle}>Secure
<select value={editServer.secure ? '1' : '0'} onChange={e => setEditServer({ ...editServer, secure: e.target.value === '1' })} style={inputStyle}>
<option value="0">Нет</option>
<option value="1">Да</option>
</select>
</label>
<label style={labelStyle}>Пользователь
<input type="text" value={editServer.username} onChange={e => setEditServer({ ...editServer, username: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Пароль
<input type="password" value={editServer.password} onChange={e => setEditServer({ ...editServer, password: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Отправитель (from_email)
<input type="email" value={editServer.from_email} onChange={e => setEditServer({ ...editServer, from_email: e.target.value })} required style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditServer(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditSmtpModal
isOpen={!!editServer}
onClose={() => setEditServer(null)}
server={editServer}
loading={editLoading}
onChange={setEditServer}
onSave={handleEditSave}
/>
)}
{createServer && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateServer(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить SMTP-сервер</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Название
<input type="text" value={createServer.name} onChange={e => setCreateServer({ ...createServer, name: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Host
<input type="text" value={createServer.host} onChange={e => setCreateServer({ ...createServer, host: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Port
<input type="number" value={createServer.port} onChange={e => setCreateServer({ ...createServer, port: Number(e.target.value) })} required style={inputStyle} />
</label>
<label style={labelStyle}>Secure
<select value={createServer.secure ? '1' : '0'} onChange={e => setCreateServer({ ...createServer, secure: e.target.value === '1' })} style={inputStyle}>
<option value="0">Нет</option>
<option value="1">Да</option>
</select>
</label>
<label style={labelStyle}>Пользователь
<input type="text" value={createServer.username} onChange={e => setCreateServer({ ...createServer, username: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Пароль
<input type="password" value={createServer.password} onChange={e => setCreateServer({ ...createServer, password: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Отправитель (from_email)
<input type="email" value={createServer.from_email} onChange={e => setCreateServer({ ...createServer, from_email: e.target.value })} required style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateServer(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateSmtpModal
isOpen={!!createServer}
onClose={() => setCreateServer(null)}
server={createServer}
loading={createLoading}
onChange={setCreateServer}
onSave={handleCreateSave}
/>
)}
</div>
);
@ -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;

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setPage(page - 1)} disabled={page <= 1} style={{ ...paginatorBtnStyle, opacity: page <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(total / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setPage(i + 1)}
style={{
...paginatorPageStyle,
...(page === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={page === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setPage(page + 1)} disabled={page >= Math.ceil(total / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={page}
total={total}
pageSize={PAGE_SIZE}
onPageChange={setPage}
/>
</>
)}
{editSub && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditSub(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать запись</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Email
<input type="email" value={editSub.email} onChange={e => setEditSub({ ...editSub, email: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Имя
<input type="text" value={editSub.name || ''} onChange={e => setEditSub({ ...editSub, name: e.target.value })} style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditSub(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditUnsubModal
isOpen={!!editSub}
onClose={() => setEditSub(null)}
sub={editSub}
loading={editLoading}
onChange={setEditSub}
onSave={handleEditSave}
/>
)}
{createSub && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateSub(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить запись</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Email
<input type="email" value={createSub.email} onChange={e => setCreateSub({ ...createSub, email: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Имя
<input type="text" value={createSub.name || ''} onChange={e => setCreateSub({ ...createSub, name: e.target.value })} style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateSub(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateUnsubModal
isOpen={!!createSub}
onClose={() => setCreateSub(null)}
sub={createSub}
loading={createLoading}
onChange={setCreateSub}
onSave={handleCreateSave}
/>
)}
</div>
);
@ -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;

View File

@ -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() {
)}
</tbody>
</table>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={paginatorWrapperStyle}>
<button onClick={() => setUsersPage(usersPage - 1)} disabled={usersPage <= 1} style={{ ...paginatorBtnStyle, opacity: usersPage <= 1 ? 0.5 : 1 }}></button>
{Array.from({ length: Math.ceil(usersTotal / PAGE_SIZE) || 1 }, (_, i) => (
<button
key={i + 1}
onClick={() => setUsersPage(i + 1)}
style={{
...paginatorPageStyle,
...(usersPage === i + 1 ? paginatorPageActiveStyle : {})
}}
disabled={usersPage === i + 1}
>
{i + 1}
</button>
))}
<button onClick={() => setUsersPage(usersPage + 1)} disabled={usersPage >= Math.ceil(usersTotal / PAGE_SIZE)} style={{ ...paginatorBtnStyle, opacity: usersPage >= Math.ceil(usersTotal / PAGE_SIZE) ? 0.5 : 1 }}></button>
</div>
</div>
<Paginator
page={usersPage}
total={usersTotal}
pageSize={PAGE_SIZE}
onPageChange={setUsersPage}
/>
</>
)}
{editUser && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setEditUser(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={editLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Редактировать пользователя</h3>
<form onSubmit={handleEditSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Email
<input type="email" value={editUser.email} onChange={e => setEditUser({ ...editUser, email: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Имя
<input type="text" value={editUser.name} onChange={e => setEditUser({ ...editUser, name: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Роль
<select value={editUser.role_id} onChange={e => setEditUser({ ...editUser, role_id: Number(e.target.value) })} required style={inputStyle}>
{roles.map(role => (
<option key={role.id} value={role.id}>{role.name}</option>
))}
</select>
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={editLoading} style={saveBtnStyle}>{editLoading ? 'Сохранение...' : 'Сохранить'}</button>
<button type="button" onClick={() => setEditUser(null)} disabled={editLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<EditUserModal
isOpen={!!editUser}
onClose={() => setEditUser(null)}
user={editUser}
roles={roles}
loading={editLoading}
onChange={setEditUser}
onSave={handleEditSave}
/>
)}
{createUser && (
<div style={modalOverlayStyle}>
<div style={modalStyle}>
<button
type="button"
onClick={() => setCreateUser(null)}
style={closeBtnStyle}
aria-label="Закрыть"
disabled={createLoading}
>
×
</button>
<h3 style={{ margin: '0 0 18px 0', textAlign: 'center', color: '#3730a3', fontWeight: 700 }}>Добавить пользователя</h3>
<form onSubmit={handleCreateSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, minWidth: 260 }}>
<label style={labelStyle}>Email
<input type="email" value={createUser.email} onChange={e => setCreateUser({ ...createUser, email: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Имя
<input type="text" value={createUser.name} onChange={e => setCreateUser({ ...createUser, name: e.target.value })} required style={inputStyle} />
</label>
<label style={labelStyle}>Роль
<select value={createUser.role_id} onChange={e => setCreateUser({ ...createUser, role_id: Number(e.target.value) })} required style={inputStyle}>
{roles.map(role => (
<option key={role.id} value={role.id}>{role.name}</option>
))}
</select>
</label>
<label style={labelStyle}>Пароль
<input type="password" value={createUser.password} onChange={e => setCreateUser({ ...createUser, password: e.target.value })} required style={inputStyle} />
</label>
<div style={{ display: 'flex', gap: 12, marginTop: 10, justifyContent: 'flex-end' }}>
<button type="submit" disabled={createLoading} style={saveBtnStyle}>{createLoading ? 'Создание...' : 'Создать'}</button>
<button type="button" onClick={() => setCreateUser(null)} disabled={createLoading} style={cancelBtnStyle}>Отмена</button>
</div>
</form>
</div>
</div>
<CreateUserModal
isOpen={!!createUser}
onClose={() => setCreateUser(null)}
user={createUser}
roles={roles}
loading={createLoading}
onChange={setCreateUser}
onSave={handleCreateSave}
/>
)}
</div>
);
@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}