style and modals
This commit is contained in:
parent
bee42c5b16
commit
f3aeaad948
37
frontend/src/components/Paginator.js
Normal file
37
frontend/src/components/Paginator.js
Normal 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>
|
||||
);
|
||||
}
|
||||
45
frontend/src/modals/CreateCampaignModal.js
Normal file
45
frontend/src/modals/CreateCampaignModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
23
frontend/src/modals/CreateGroupModal.js
Normal file
23
frontend/src/modals/CreateGroupModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
41
frontend/src/modals/CreateSmtpModal.js
Normal file
41
frontend/src/modals/CreateSmtpModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
30
frontend/src/modals/CreateSubscriberModal.js
Normal file
30
frontend/src/modals/CreateSubscriberModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
20
frontend/src/modals/CreateTemplateModal.js
Normal file
20
frontend/src/modals/CreateTemplateModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
23
frontend/src/modals/CreateUnsubModal.js
Normal file
23
frontend/src/modals/CreateUnsubModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
33
frontend/src/modals/CreateUserModal.js
Normal file
33
frontend/src/modals/CreateUserModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
45
frontend/src/modals/EditCampaignModal.js
Normal file
45
frontend/src/modals/EditCampaignModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
23
frontend/src/modals/EditGroupModal.js
Normal file
23
frontend/src/modals/EditGroupModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
41
frontend/src/modals/EditSmtpModal.js
Normal file
41
frontend/src/modals/EditSmtpModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
30
frontend/src/modals/EditSubscriberModal.js
Normal file
30
frontend/src/modals/EditSubscriberModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
20
frontend/src/modals/EditTemplateModal.js
Normal file
20
frontend/src/modals/EditTemplateModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
23
frontend/src/modals/EditUnsubModal.js
Normal file
23
frontend/src/modals/EditUnsubModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
30
frontend/src/modals/EditUserModal.js
Normal file
30
frontend/src/modals/EditUserModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
22
frontend/src/modals/Modal.js
Normal file
22
frontend/src/modals/Modal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
59
frontend/src/styles/CampaignModal.module.css
Normal file
59
frontend/src/styles/CampaignModal.module.css
Normal 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;
|
||||
}
|
||||
59
frontend/src/styles/GroupModal.module.css
Normal file
59
frontend/src/styles/GroupModal.module.css
Normal 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;
|
||||
}
|
||||
44
frontend/src/styles/Modal.module.css
Normal file
44
frontend/src/styles/Modal.module.css
Normal 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; }
|
||||
}
|
||||
39
frontend/src/styles/Paginator.module.css
Normal file
39
frontend/src/styles/Paginator.module.css
Normal 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);
|
||||
}
|
||||
59
frontend/src/styles/SmtpModal.module.css
Normal file
59
frontend/src/styles/SmtpModal.module.css
Normal 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;
|
||||
}
|
||||
59
frontend/src/styles/SubscriberModal.module.css
Normal file
59
frontend/src/styles/SubscriberModal.module.css
Normal 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;
|
||||
}
|
||||
59
frontend/src/styles/TemplateModal.module.css
Normal file
59
frontend/src/styles/TemplateModal.module.css
Normal 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;
|
||||
}
|
||||
59
frontend/src/styles/UnsubModal.module.css
Normal file
59
frontend/src/styles/UnsubModal.module.css
Normal 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;
|
||||
}
|
||||
59
frontend/src/styles/UserModal.module.css
Normal file
59
frontend/src/styles/UserModal.module.css
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user