This commit is contained in:
187
panel/modals/CollectionEditModal.tsx
Normal file
187
panel/modals/CollectionEditModal.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Component, createEffect, createSignal } from 'solid-js'
|
||||
import formStyles from '../styles/Form.module.css'
|
||||
import styles from '../styles/Modal.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
|
||||
interface Collection {
|
||||
id: number
|
||||
slug: string
|
||||
title: string
|
||||
desc?: string
|
||||
pic?: string
|
||||
amount?: number
|
||||
published_at?: number
|
||||
created_at: number
|
||||
created_by: {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
interface CollectionEditModalProps {
|
||||
isOpen: boolean
|
||||
collection: Collection | null // null для создания новой
|
||||
onClose: () => void
|
||||
onSave: (collection: Partial<Collection>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Модальное окно для создания и редактирования коллекций
|
||||
*/
|
||||
const CollectionEditModal: Component<CollectionEditModalProps> = (props) => {
|
||||
const [formData, setFormData] = createSignal({
|
||||
slug: '',
|
||||
title: '',
|
||||
desc: '',
|
||||
pic: ''
|
||||
})
|
||||
const [errors, setErrors] = createSignal<Record<string, string>>({})
|
||||
|
||||
// Синхронизация с props.collection
|
||||
createEffect(() => {
|
||||
if (props.isOpen) {
|
||||
if (props.collection) {
|
||||
// Редактирование существующей коллекции
|
||||
setFormData({
|
||||
slug: props.collection.slug,
|
||||
title: props.collection.title,
|
||||
desc: props.collection.desc || '',
|
||||
pic: props.collection.pic || ''
|
||||
})
|
||||
} else {
|
||||
// Создание новой коллекции
|
||||
setFormData({
|
||||
slug: '',
|
||||
title: '',
|
||||
desc: '',
|
||||
pic: ''
|
||||
})
|
||||
}
|
||||
setErrors({})
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
const data = formData()
|
||||
|
||||
// Валидация slug
|
||||
if (!data.slug.trim()) {
|
||||
newErrors.slug = 'Slug обязателен'
|
||||
} else if (!/^[a-z0-9-_]+$/.test(data.slug)) {
|
||||
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
|
||||
}
|
||||
|
||||
// Валидация названия
|
||||
if (!data.title.trim()) {
|
||||
newErrors.title = 'Название обязательно'
|
||||
}
|
||||
|
||||
// Валидация URL картинки (если указан)
|
||||
if (data.pic.trim() && !/^https?:\/\/.+/.test(data.pic)) {
|
||||
newErrors.pic = 'Некорректный URL картинки'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
// Очищаем ошибку для поля при изменении
|
||||
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
const collectionData = { ...formData() }
|
||||
props.onSave(collectionData)
|
||||
}
|
||||
|
||||
const isCreating = () => props.collection === null
|
||||
const modalTitle = () =>
|
||||
isCreating() ? 'Создание новой коллекции' : `Редактирование коллекции: ${props.collection?.title || ''}`
|
||||
|
||||
return (
|
||||
<Modal isOpen={props.isOpen} onClose={props.onClose} title={modalTitle()} size="medium">
|
||||
<div class={styles['modal-content']}>
|
||||
<div class={formStyles.form}>
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
Slug <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().slug}
|
||||
onInput={(e) => updateField('slug', e.target.value.toLowerCase())}
|
||||
class={`${formStyles.input} ${errors().slug ? formStyles.inputError : ''}`}
|
||||
placeholder="уникальный-идентификатор"
|
||||
required
|
||||
/>
|
||||
<div class={formStyles.fieldHint}>
|
||||
Используется в URL коллекции. Только латинские буквы, цифры, дефисы и подчеркивания.
|
||||
</div>
|
||||
{errors().slug && <div class={formStyles.fieldError}>{errors().slug}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
Название <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().title}
|
||||
onInput={(e) => updateField('title', e.target.value)}
|
||||
class={`${formStyles.input} ${errors().title ? formStyles.inputError : ''}`}
|
||||
placeholder="Название коллекции"
|
||||
required
|
||||
/>
|
||||
{errors().title && <div class={formStyles.fieldError}>{errors().title}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Описание</label>
|
||||
<textarea
|
||||
value={formData().desc}
|
||||
onInput={(e) => updateField('desc', e.target.value)}
|
||||
class={formStyles.input}
|
||||
style={{
|
||||
'min-height': '80px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
placeholder="Описание коллекции..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Картинка (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().pic}
|
||||
onInput={(e) => updateField('pic', e.target.value)}
|
||||
class={`${formStyles.input} ${errors().pic ? formStyles.inputError : ''}`}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
{errors().pic && <div class={formStyles.fieldError}>{errors().pic}</div>}
|
||||
</div>
|
||||
|
||||
<div class={styles['modal-actions']}>
|
||||
<Button variant="secondary" onClick={props.onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
{isCreating() ? 'Создать' : 'Сохранить'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollectionEditModal
|
192
panel/modals/CommunityEditModal.tsx
Normal file
192
panel/modals/CommunityEditModal.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Component, createEffect, createSignal } from 'solid-js'
|
||||
import formStyles from '../styles/Form.module.css'
|
||||
import styles from '../styles/Modal.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
|
||||
interface Community {
|
||||
id: number
|
||||
slug: string
|
||||
name: string
|
||||
desc?: string
|
||||
pic: string
|
||||
created_at: number
|
||||
created_by: {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
stat: {
|
||||
shouts: number
|
||||
followers: number
|
||||
authors: number
|
||||
}
|
||||
}
|
||||
|
||||
interface CommunityEditModalProps {
|
||||
isOpen: boolean
|
||||
community: Community | null // null для создания нового
|
||||
onClose: () => void
|
||||
onSave: (community: Partial<Community>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Модальное окно для создания и редактирования сообществ
|
||||
*/
|
||||
const CommunityEditModal: Component<CommunityEditModalProps> = (props) => {
|
||||
const [formData, setFormData] = createSignal({
|
||||
slug: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
pic: ''
|
||||
})
|
||||
const [errors, setErrors] = createSignal<Record<string, string>>({})
|
||||
|
||||
// Синхронизация с props.community
|
||||
createEffect(() => {
|
||||
if (props.isOpen) {
|
||||
if (props.community) {
|
||||
// Редактирование существующего сообщества
|
||||
setFormData({
|
||||
slug: props.community.slug,
|
||||
name: props.community.name,
|
||||
desc: props.community.desc || '',
|
||||
pic: props.community.pic
|
||||
})
|
||||
} else {
|
||||
// Создание нового сообщества
|
||||
setFormData({
|
||||
slug: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
pic: ''
|
||||
})
|
||||
}
|
||||
setErrors({})
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
const data = formData()
|
||||
|
||||
// Валидация slug
|
||||
if (!data.slug.trim()) {
|
||||
newErrors.slug = 'Slug обязателен'
|
||||
} else if (!/^[a-z0-9-_]+$/.test(data.slug)) {
|
||||
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
|
||||
}
|
||||
|
||||
// Валидация названия
|
||||
if (!data.name.trim()) {
|
||||
newErrors.name = 'Название обязательно'
|
||||
}
|
||||
|
||||
// Валидация URL картинки (если указан)
|
||||
if (data.pic.trim() && !/^https?:\/\/.+/.test(data.pic)) {
|
||||
newErrors.pic = 'Некорректный URL картинки'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
// Очищаем ошибку для поля при изменении
|
||||
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
const communityData = { ...formData() }
|
||||
props.onSave(communityData)
|
||||
}
|
||||
|
||||
const isCreating = () => props.community === null
|
||||
const modalTitle = () =>
|
||||
isCreating()
|
||||
? 'Создание нового сообщества'
|
||||
: `Редактирование сообщества: ${props.community?.name || ''}`
|
||||
|
||||
return (
|
||||
<Modal isOpen={props.isOpen} onClose={props.onClose} title={modalTitle()} size="medium">
|
||||
<div class={styles['modal-content']}>
|
||||
<div class={formStyles.form}>
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
Slug <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().slug}
|
||||
onInput={(e) => updateField('slug', e.target.value.toLowerCase())}
|
||||
class={`${formStyles.input} ${errors().slug ? formStyles.inputError : ''}`}
|
||||
placeholder="уникальный-идентификатор"
|
||||
required
|
||||
/>
|
||||
<div class={formStyles.fieldHint}>
|
||||
Используется в URL сообщества. Только латинские буквы, цифры, дефисы и подчеркивания.
|
||||
</div>
|
||||
{errors().slug && <div class={formStyles.fieldError}>{errors().slug}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
Название <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().name}
|
||||
onInput={(e) => updateField('name', e.target.value)}
|
||||
class={`${formStyles.input} ${errors().name ? formStyles.inputError : ''}`}
|
||||
placeholder="Название сообщества"
|
||||
required
|
||||
/>
|
||||
{errors().name && <div class={formStyles.fieldError}>{errors().name}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Описание</label>
|
||||
<textarea
|
||||
value={formData().desc}
|
||||
onInput={(e) => updateField('desc', e.target.value)}
|
||||
class={formStyles.input}
|
||||
style={{
|
||||
'min-height': '80px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
placeholder="Описание сообщества..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Картинка (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().pic}
|
||||
onInput={(e) => updateField('pic', e.target.value)}
|
||||
class={`${formStyles.input} ${errors().pic ? formStyles.inputError : ''}`}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
{errors().pic && <div class={formStyles.fieldError}>{errors().pic}</div>}
|
||||
</div>
|
||||
|
||||
<div class={styles['modal-actions']}>
|
||||
<Button variant="secondary" onClick={props.onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
{isCreating() ? 'Создать' : 'Сохранить'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommunityEditModal
|
234
panel/modals/InviteEditModal.tsx
Normal file
234
panel/modals/InviteEditModal.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Component, createEffect, createSignal } from 'solid-js'
|
||||
import formStyles from '../styles/Form.module.css'
|
||||
import styles from '../styles/Modal.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
|
||||
interface Author {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface Shout {
|
||||
id: number
|
||||
title: string
|
||||
slug: string
|
||||
created_by: Author
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
inviter_id: number
|
||||
author_id: number
|
||||
shout_id: number
|
||||
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
|
||||
inviter: Author
|
||||
author: Author
|
||||
shout: Shout
|
||||
created_at?: number
|
||||
}
|
||||
|
||||
interface InviteEditModalProps {
|
||||
isOpen: boolean
|
||||
invite: Invite | null // null для создания нового
|
||||
onClose: () => void
|
||||
onSave: (invite: Partial<Invite>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Модальное окно для создания и редактирования приглашений
|
||||
*/
|
||||
const InviteEditModal: Component<InviteEditModalProps> = (props) => {
|
||||
const [formData, setFormData] = createSignal({
|
||||
inviter_id: 0,
|
||||
author_id: 0,
|
||||
shout_id: 0,
|
||||
status: 'PENDING' as 'PENDING' | 'ACCEPTED' | 'REJECTED'
|
||||
})
|
||||
const [errors, setErrors] = createSignal<Record<string, string>>({})
|
||||
|
||||
// Синхронизация с props.invite
|
||||
createEffect(() => {
|
||||
if (props.isOpen) {
|
||||
if (props.invite) {
|
||||
// Редактирование существующего приглашения
|
||||
setFormData({
|
||||
inviter_id: props.invite.inviter_id,
|
||||
author_id: props.invite.author_id,
|
||||
shout_id: props.invite.shout_id,
|
||||
status: props.invite.status
|
||||
})
|
||||
} else {
|
||||
// Создание нового приглашения
|
||||
setFormData({
|
||||
inviter_id: 0,
|
||||
author_id: 0,
|
||||
shout_id: 0,
|
||||
status: 'PENDING'
|
||||
})
|
||||
}
|
||||
setErrors({})
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
const data = formData()
|
||||
|
||||
// Валидация ID приглашающего
|
||||
if (!data.inviter_id || data.inviter_id <= 0) {
|
||||
newErrors.inviter_id = 'ID приглашающего обязателен'
|
||||
}
|
||||
|
||||
// Валидация ID приглашаемого
|
||||
if (!data.author_id || data.author_id <= 0) {
|
||||
newErrors.author_id = 'ID приглашаемого обязателен'
|
||||
}
|
||||
|
||||
// Валидация ID публикации
|
||||
if (!data.shout_id || data.shout_id <= 0) {
|
||||
newErrors.shout_id = 'ID публикации обязателен'
|
||||
}
|
||||
|
||||
// Проверка что приглашающий и приглашаемый не совпадают
|
||||
if (data.inviter_id === data.author_id && data.inviter_id > 0) {
|
||||
newErrors.author_id = 'Приглашающий и приглашаемый не могут быть одним и тем же автором'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const updateField = (field: string, value: string | number) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
// Очищаем ошибку для поля при изменении
|
||||
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
const inviteData = { ...formData() }
|
||||
props.onSave(inviteData)
|
||||
}
|
||||
|
||||
const isCreating = () => props.invite === null
|
||||
const modalTitle = () =>
|
||||
isCreating()
|
||||
? 'Создание нового приглашения'
|
||||
: `Редактирование приглашения: ${props.invite?.inviter.name || ''} → ${props.invite?.author.name || ''}`
|
||||
|
||||
return (
|
||||
<Modal isOpen={props.isOpen} onClose={props.onClose} title={modalTitle()} size="medium">
|
||||
<div class={styles['modal-content']}>
|
||||
<div class={formStyles.form}>
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
ID приглашающего <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData().inviter_id}
|
||||
onInput={(e) => updateField('inviter_id', parseInt(e.target.value) || 0)}
|
||||
class={`${formStyles.input} ${errors().inviter_id ? formStyles.inputError : ''}`}
|
||||
placeholder="1"
|
||||
required
|
||||
disabled={!isCreating()} // При редактировании ID нельзя менять
|
||||
/>
|
||||
<div class={formStyles.fieldHint}>
|
||||
ID автора, который отправляет приглашение
|
||||
</div>
|
||||
{errors().inviter_id && <div class={formStyles.fieldError}>{errors().inviter_id}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
ID приглашаемого <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData().author_id}
|
||||
onInput={(e) => updateField('author_id', parseInt(e.target.value) || 0)}
|
||||
class={`${formStyles.input} ${errors().author_id ? formStyles.inputError : ''}`}
|
||||
placeholder="2"
|
||||
required
|
||||
disabled={!isCreating()} // При редактировании ID нельзя менять
|
||||
/>
|
||||
<div class={formStyles.fieldHint}>
|
||||
ID автора, которого приглашают к сотрудничеству
|
||||
</div>
|
||||
{errors().author_id && <div class={formStyles.fieldError}>{errors().author_id}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
ID публикации <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData().shout_id}
|
||||
onInput={(e) => updateField('shout_id', parseInt(e.target.value) || 0)}
|
||||
class={`${formStyles.input} ${errors().shout_id ? formStyles.inputError : ''}`}
|
||||
placeholder="123"
|
||||
required
|
||||
disabled={!isCreating()} // При редактировании ID нельзя менять
|
||||
/>
|
||||
<div class={formStyles.fieldHint}>
|
||||
ID публикации, к которой приглашают на сотрудничество
|
||||
</div>
|
||||
{errors().shout_id && <div class={formStyles.fieldError}>{errors().shout_id}</div>}
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
Статус <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<select
|
||||
value={formData().status}
|
||||
onChange={(e) => updateField('status', e.target.value)}
|
||||
class={formStyles.input}
|
||||
required
|
||||
>
|
||||
<option value="PENDING">Ожидает ответа</option>
|
||||
<option value="ACCEPTED">Принято</option>
|
||||
<option value="REJECTED">Отклонено</option>
|
||||
</select>
|
||||
<div class={formStyles.fieldHint}>
|
||||
Текущий статус приглашения
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Информация о связанных объектах при редактировании */}
|
||||
{!isCreating() && props.invite && (
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Информация о приглашении</label>
|
||||
<div class={formStyles.fieldHint} style={{ 'margin-bottom': '8px' }}>
|
||||
<strong>Приглашающий:</strong> {props.invite.inviter.name} ({props.invite.inviter.email})
|
||||
</div>
|
||||
<div class={formStyles.fieldHint} style={{ 'margin-bottom': '8px' }}>
|
||||
<strong>Приглашаемый:</strong> {props.invite.author.name} ({props.invite.author.email})
|
||||
</div>
|
||||
<div class={formStyles.fieldHint}>
|
||||
<strong>Публикация:</strong> {props.invite.shout.title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class={styles['modal-actions']}>
|
||||
<Button variant="secondary" onClick={props.onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
{isCreating() ? 'Создать' : 'Сохранить'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteEditModal
|
Reference in New Issue
Block a user