0.5.8-panel-upgrade-community-crud-fix
All checks were successful
Deploy on push / deploy (push) Successful in 6s
All checks were successful
Deploy on push / deploy (push) Successful in 6s
This commit is contained in:
188
panel/modals/EnvVariableModal.tsx
Normal file
188
panel/modals/EnvVariableModal.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { Component, createMemo, createSignal, Show } from 'solid-js'
|
||||
import { query } from '../graphql'
|
||||
import { EnvVariable } from '../graphql/generated/schema'
|
||||
import { ADMIN_UPDATE_ENV_VARIABLE_MUTATION } from '../graphql/mutations'
|
||||
import formStyles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
import TextPreview from '../ui/TextPreview'
|
||||
|
||||
interface EnvVariableModalProps {
|
||||
isOpen: boolean
|
||||
variable: EnvVariable
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
onValueChange?: (value: string) => void // FIXME: no need
|
||||
}
|
||||
|
||||
const EnvVariableModal: Component<EnvVariableModalProps> = (props) => {
|
||||
const [value, setValue] = createSignal(props.variable.value)
|
||||
const [saving, setSaving] = createSignal(false)
|
||||
const [error, setError] = createSignal<string | null>(null)
|
||||
const [showFormatted, setShowFormatted] = createSignal(false)
|
||||
|
||||
// Определяем нужно ли использовать textarea
|
||||
const needsTextarea = createMemo(() => {
|
||||
const val = value()
|
||||
return (
|
||||
val.length > 50 ||
|
||||
val.includes('\n') ||
|
||||
props.variable.type === 'json' ||
|
||||
props.variable.key.includes('URL') ||
|
||||
props.variable.key.includes('SECRET')
|
||||
)
|
||||
})
|
||||
|
||||
// Форматируем JSON если возможно
|
||||
const formattedValue = createMemo(() => {
|
||||
if (props.variable.type === 'json' || (value().startsWith('{') && value().endsWith('}'))) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value()), null, 2)
|
||||
} catch {
|
||||
return value()
|
||||
}
|
||||
}
|
||||
return value()
|
||||
})
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await query<{ updateEnvVariable: boolean }>(
|
||||
`${location.origin}/graphql`,
|
||||
ADMIN_UPDATE_ENV_VARIABLE_MUTATION,
|
||||
{
|
||||
key: props.variable.key,
|
||||
value: value()
|
||||
}
|
||||
)
|
||||
|
||||
if (result?.updateEnvVariable) {
|
||||
props.onSave()
|
||||
} else {
|
||||
setError('Failed to update environment variable')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatValue = () => {
|
||||
if (props.variable.type === 'json') {
|
||||
try {
|
||||
const formatted = JSON.stringify(JSON.parse(value()), null, 2)
|
||||
setValue(formatted)
|
||||
} catch (_e) {
|
||||
setError('Invalid JSON format')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={props.isOpen}
|
||||
title={`Редактировать ${props.variable.key}`}
|
||||
onClose={props.onClose}
|
||||
size="large"
|
||||
>
|
||||
<div class={formStyles['modal-wide']}>
|
||||
<form class={formStyles.form} onSubmit={(e) => e.preventDefault()}>
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles['form-label']}>Ключ:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.variable.key}
|
||||
disabled
|
||||
class={formStyles['form-input-disabled']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles['form-label']}>
|
||||
Значение:
|
||||
<span class={formStyles['form-label-info']}>
|
||||
{props.variable.type} {props.variable.isSecret && '(секретное)'}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Show when={needsTextarea()}>
|
||||
<div class={formStyles['textarea-container']}>
|
||||
<textarea
|
||||
value={value()}
|
||||
onInput={(e) => setValue(e.currentTarget.value)}
|
||||
class={formStyles['form-textarea']}
|
||||
rows={Math.min(Math.max(value().split('\n').length + 2, 4), 15)}
|
||||
placeholder="Введите значение переменной..."
|
||||
/>
|
||||
<Show when={props.variable.type === 'json'}>
|
||||
<div class={formStyles['textarea-actions']}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={formatValue}
|
||||
title="Форматировать JSON"
|
||||
>
|
||||
🎨 Форматировать
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => setShowFormatted(!showFormatted())}
|
||||
title={showFormatted() ? 'Скрыть превью' : 'Показать превью'}
|
||||
>
|
||||
{showFormatted() ? '👁️ Скрыть' : '👁️ Превью'}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!needsTextarea()}>
|
||||
<input
|
||||
type={props.variable.isSecret ? 'password' : 'text'}
|
||||
value={value()}
|
||||
onInput={(e) => setValue(e.currentTarget.value)}
|
||||
class={formStyles['form-input']}
|
||||
placeholder="Введите значение переменной..."
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={showFormatted() && (props.variable.type === 'json' || value().startsWith('{'))}>
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles['form-label']}>Превью (форматированное):</label>
|
||||
<div class={formStyles['code-preview-container']}>
|
||||
<TextPreview content={formattedValue()} />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.variable.description}>
|
||||
<div class={formStyles['form-help']}>
|
||||
<strong>Описание:</strong> {props.variable.description}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={error()}>
|
||||
<div class={formStyles['form-error']}>{error()}</div>
|
||||
</Show>
|
||||
|
||||
<div class={formStyles['form-actions']}>
|
||||
<Button variant="secondary" onClick={props.onClose} disabled={saving()}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave} loading={saving()}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnvVariableModal
|
272
panel/modals/RolesModal.tsx
Normal file
272
panel/modals/RolesModal.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { Component, createEffect, createSignal, For } from 'solid-js'
|
||||
import type { AdminUserInfo } from '../graphql/generated/schema'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
|
||||
export interface UserEditModalProps {
|
||||
user: AdminUserInfo
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (userData: {
|
||||
id: number
|
||||
email?: string
|
||||
name?: string
|
||||
slug?: string
|
||||
roles: string[]
|
||||
}) => Promise<void>
|
||||
}
|
||||
|
||||
const AVAILABLE_ROLES = [
|
||||
{ id: 'admin', name: 'Администратор', description: 'Полный доступ к системе' },
|
||||
{ id: 'editor', name: 'Редактор', description: 'Редактирование публикаций и управление сообществом' },
|
||||
{
|
||||
id: 'expert',
|
||||
name: 'Эксперт',
|
||||
description: 'Добавление доказательств и опровержений, управление темами'
|
||||
},
|
||||
{ id: 'author', name: 'Автор', description: 'Создание и редактирование своих публикаций' },
|
||||
{ id: 'reader', name: 'Читатель', description: 'Чтение и комментирование' }
|
||||
]
|
||||
|
||||
const UserEditModal: Component<UserEditModalProps> = (props) => {
|
||||
const [formData, setFormData] = createSignal({
|
||||
email: props.user.email || '',
|
||||
name: props.user.name || '',
|
||||
slug: props.user.slug || '',
|
||||
roles: props.user.roles || []
|
||||
})
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
const [errors, setErrors] = createSignal<Record<string, string>>({})
|
||||
|
||||
// Сброс формы при открытии модалки
|
||||
createEffect(() => {
|
||||
if (props.isOpen) {
|
||||
setFormData({
|
||||
email: props.user.email || '',
|
||||
name: props.user.name || '',
|
||||
slug: props.user.slug || '',
|
||||
roles: props.user.roles || []
|
||||
})
|
||||
setErrors({})
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
const data = formData()
|
||||
|
||||
// Валидация email
|
||||
if (!data.email.trim()) {
|
||||
newErrors.email = 'Email обязателен'
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
||||
newErrors.email = 'Некорректный формат email'
|
||||
}
|
||||
|
||||
// Валидация имени
|
||||
if (!data.name.trim()) {
|
||||
newErrors.name = 'Имя обязательно'
|
||||
}
|
||||
|
||||
// Валидация slug
|
||||
if (!data.slug.trim()) {
|
||||
newErrors.slug = 'Slug обязателен'
|
||||
} else if (!/^[a-z0-9-_]+$/.test(data.slug)) {
|
||||
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
|
||||
}
|
||||
|
||||
// Валидация ролей
|
||||
if (data.roles.length === 0) {
|
||||
newErrors.roles = 'Выберите хотя бы одну роль'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
// Очищаем ошибку для поля при изменении
|
||||
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
|
||||
const handleRoleToggle = (roleId: string) => {
|
||||
const current = formData().roles
|
||||
const newRoles = current.includes(roleId) ? current.filter((r) => r !== roleId) : [...current, roleId]
|
||||
|
||||
setFormData((prev) => ({ ...prev, roles: newRoles }))
|
||||
setErrors((prev) => ({ ...prev, roles: '' }))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await props.onSave({
|
||||
id: props.user.id,
|
||||
email: formData().email,
|
||||
name: formData().name,
|
||||
slug: formData().slug,
|
||||
roles: formData().roles
|
||||
})
|
||||
props.onClose()
|
||||
} catch (error) {
|
||||
console.error('Error saving user:', error)
|
||||
setErrors({ general: 'Ошибка при сохранении данных пользователя' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (timestamp?: number | null) => {
|
||||
if (!timestamp) return '—'
|
||||
return new Date(timestamp * 1000).toLocaleString('ru-RU')
|
||||
}
|
||||
|
||||
const footer = (
|
||||
<>
|
||||
<Button variant="secondary" onClick={props.onClose} disabled={loading()}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave} loading={loading()} disabled={loading()}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`Редактирование пользователя #${props.user.id}`}
|
||||
isOpen={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
footer={footer}
|
||||
size="medium"
|
||||
>
|
||||
<div class={styles.form}>
|
||||
{errors().general && (
|
||||
<div class={styles.error} style={{ 'margin-bottom': '20px' }}>
|
||||
{errors().general}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Информационная секция */}
|
||||
<div
|
||||
class={styles.section}
|
||||
style={{
|
||||
'margin-bottom': '20px',
|
||||
padding: '15px',
|
||||
background: '#f8f9fa',
|
||||
'border-radius': '8px'
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 10px 0', color: '#495057' }}>Системная информация</h4>
|
||||
<div style={{ 'font-size': '14px', color: '#6c757d' }}>
|
||||
<div>
|
||||
<strong>ID:</strong> {props.user.id}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Дата регистрации:</strong> {formatDate(props.user.created_at)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Последняя активность:</strong> {formatDate(props.user.last_seen)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Основные данные */}
|
||||
<div class={styles.section}>
|
||||
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Основные данные</h4>
|
||||
|
||||
<div class={styles.field}>
|
||||
<label for="email" class={styles.label}>
|
||||
Email <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
class={`${styles.input} ${errors().email ? styles.inputError : ''}`}
|
||||
value={formData().email}
|
||||
onInput={(e) => updateField('email', e.currentTarget.value)}
|
||||
disabled={loading()}
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
{errors().email && <div class={styles.fieldError}>{errors().email}</div>}
|
||||
</div>
|
||||
|
||||
<div class={styles.field}>
|
||||
<label for="name" class={styles.label}>
|
||||
Имя <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
class={`${styles.input} ${errors().name ? styles.inputError : ''}`}
|
||||
value={formData().name}
|
||||
onInput={(e) => updateField('name', e.currentTarget.value)}
|
||||
disabled={loading()}
|
||||
placeholder="Иван Иванов"
|
||||
/>
|
||||
{errors().name && <div class={styles.fieldError}>{errors().name}</div>}
|
||||
</div>
|
||||
|
||||
<div class={styles.field}>
|
||||
<label for="slug" class={styles.label}>
|
||||
Slug (URL) <span style={{ color: 'red' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="slug"
|
||||
type="text"
|
||||
class={`${styles.input} ${errors().slug ? styles.inputError : ''}`}
|
||||
value={formData().slug}
|
||||
onInput={(e) => updateField('slug', e.currentTarget.value.toLowerCase())}
|
||||
disabled={loading()}
|
||||
placeholder="ivan-ivanov"
|
||||
/>
|
||||
<div class={styles.fieldHint}>
|
||||
Используется в URL профиля. Только латинские буквы, цифры, дефисы и подчеркивания.
|
||||
</div>
|
||||
{errors().slug && <div class={styles.fieldError}>{errors().slug}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Роли */}
|
||||
<div class={styles.section}>
|
||||
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>
|
||||
Роли <span style={{ color: 'red' }}>*</span>
|
||||
</h4>
|
||||
|
||||
<div class={styles.rolesGrid}>
|
||||
<For each={AVAILABLE_ROLES}>
|
||||
{(role) => (
|
||||
<label
|
||||
class={`${styles.roleCard} ${formData().roles.includes(role.id) ? styles.roleCardSelected : ''}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData().roles.includes(role.id)}
|
||||
onChange={() => handleRoleToggle(role.id)}
|
||||
disabled={loading()}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<div class={styles.roleHeader}>
|
||||
<span class={styles.roleName}>{role.name}</span>
|
||||
<span class={styles.roleCheckmark}>
|
||||
{formData().roles.includes(role.id) ? '✓' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class={styles.roleDescription}>{role.description}</div>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{errors().roles && <div class={styles.fieldError}>{errors().roles}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserEditModal
|
52
panel/modals/ShoutBodyModal.tsx
Normal file
52
panel/modals/ShoutBodyModal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Component, For } from 'solid-js'
|
||||
import type { AdminShoutInfo, Maybe, Topic } from '../graphql/generated/schema'
|
||||
import styles from '../styles/Modal.module.css'
|
||||
import Modal from '../ui/Modal'
|
||||
import TextPreview from '../ui/TextPreview'
|
||||
|
||||
export interface ShoutBodyModalProps {
|
||||
shout: AdminShoutInfo
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const ShoutBodyModal: Component<ShoutBodyModalProps> = (props) => {
|
||||
return (
|
||||
<Modal
|
||||
title={`Просмотр публикации: ${props.shout.title}`}
|
||||
isOpen={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
size="large"
|
||||
>
|
||||
<div class={styles['shout-body']}>
|
||||
<div class={styles['shout-info']}>
|
||||
<div class={styles['info-row']}>
|
||||
<span class={styles['info-label']}>Автор:</span>
|
||||
<span class={styles['info-value']}>{props.shout?.authors?.[0]?.email}</span>
|
||||
</div>
|
||||
<div class={styles['info-row']}>
|
||||
<span class={styles['info-label']}>Просмотры:</span>
|
||||
<span class={styles['info-value']}>{props.shout.stat?.viewed || 0}</span>
|
||||
</div>
|
||||
<div class={styles['info-row']}>
|
||||
<span class={styles['info-label']}>Темы:</span>
|
||||
<div class={styles['topics-list']}>
|
||||
<For each={props.shout?.topics}>
|
||||
{(topic: Maybe<Topic>) => <span class={styles['topic-badge']}>{topic?.title || ''}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles['shout-content']}>
|
||||
<h3>Содержание</h3>
|
||||
<div class={styles['content-preview']}>
|
||||
<TextPreview content={props.shout.body || ''} maxHeight="70vh" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShoutBodyModal
|
185
panel/modals/TopicEditModal.tsx
Normal file
185
panel/modals/TopicEditModal.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
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 Topic {
|
||||
id: number
|
||||
slug: string
|
||||
title: string
|
||||
body?: string
|
||||
pic?: string
|
||||
community: number
|
||||
parent_ids?: number[]
|
||||
}
|
||||
|
||||
interface TopicEditModalProps {
|
||||
isOpen: boolean
|
||||
topic: Topic | null
|
||||
onClose: () => void
|
||||
onSave: (topic: Topic) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Модальное окно для редактирования топиков
|
||||
*/
|
||||
const TopicEditModal: Component<TopicEditModalProps> = (props) => {
|
||||
const [formData, setFormData] = createSignal<Topic>({
|
||||
id: 0,
|
||||
slug: '',
|
||||
title: '',
|
||||
body: '',
|
||||
pic: '',
|
||||
community: 0,
|
||||
parent_ids: []
|
||||
})
|
||||
|
||||
const [parentIdsText, setParentIdsText] = createSignal('')
|
||||
let bodyRef: HTMLDivElement | undefined
|
||||
|
||||
// Синхронизация с props.topic
|
||||
createEffect(() => {
|
||||
if (props.topic) {
|
||||
setFormData({ ...props.topic })
|
||||
setParentIdsText(props.topic.parent_ids?.join(', ') || '')
|
||||
|
||||
// Устанавливаем содержимое в contenteditable div
|
||||
if (bodyRef) {
|
||||
bodyRef.innerHTML = props.topic.body || ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
// Парсим parent_ids из строки
|
||||
const parentIds = parentIdsText()
|
||||
.split(',')
|
||||
.map((id) => Number.parseInt(id.trim()))
|
||||
.filter((id) => !Number.isNaN(id))
|
||||
|
||||
const updatedTopic = {
|
||||
...formData(),
|
||||
parent_ids: parentIds.length > 0 ? parentIds : undefined
|
||||
}
|
||||
|
||||
props.onSave(updatedTopic)
|
||||
}
|
||||
|
||||
const handleBodyInput = (e: Event) => {
|
||||
const target = e.target as HTMLDivElement
|
||||
setFormData((prev) => ({ ...prev, body: target.innerHTML }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
title={`Редактирование топика: ${props.topic?.title || ''}`}
|
||||
>
|
||||
<div class={styles['modal-content']}>
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().id}
|
||||
disabled
|
||||
class={formStyles.input}
|
||||
style={{ background: '#f5f5f5', cursor: 'not-allowed' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().slug}
|
||||
onInput={(e) => setFormData((prev) => ({ ...prev, slug: e.target.value }))}
|
||||
class={formStyles.input}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Название</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().title}
|
||||
onInput={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
|
||||
class={formStyles.input}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Описание (HTML)</label>
|
||||
<div
|
||||
ref={bodyRef}
|
||||
contentEditable
|
||||
onInput={handleBodyInput}
|
||||
class={formStyles.input}
|
||||
style={{
|
||||
'min-height': '120px',
|
||||
'font-family': 'Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
'font-size': '13px',
|
||||
'line-height': '1.4',
|
||||
'white-space': 'pre-wrap',
|
||||
'overflow-wrap': 'break-word'
|
||||
}}
|
||||
data-placeholder="Введите HTML описание топика..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Картинка (URL)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().pic || ''}
|
||||
onInput={(e) => setFormData((prev) => ({ ...prev, pic: e.target.value }))}
|
||||
class={formStyles.input}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>Сообщество (ID)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData().community}
|
||||
onInput={(e) =>
|
||||
setFormData((prev) => ({ ...prev, community: Number.parseInt(e.target.value) || 0 }))
|
||||
}
|
||||
class={formStyles.input}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={formStyles['form-group']}>
|
||||
<label class={formStyles.label}>
|
||||
Родительские топики (ID через запятую)
|
||||
<small style={{ display: 'block', color: '#666', 'margin-top': '4px' }}>
|
||||
Например: 1, 5, 12
|
||||
</small>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={parentIdsText()}
|
||||
onInput={(e) => setParentIdsText(e.target.value)}
|
||||
class={formStyles.input}
|
||||
placeholder="1, 5, 12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles['modal-actions']}>
|
||||
<Button variant="secondary" onClick={props.onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopicEditModal
|
Reference in New Issue
Block a user