This commit is contained in:
@@ -98,10 +98,6 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||
<div class={styles['header-container']}>
|
||||
<div class={styles['header-left']}>
|
||||
<img src={publyLogo} alt="Logo" class={styles.logo} />
|
||||
<h1>
|
||||
Панель администратора
|
||||
<span class={styles['version-badge']}>v{__APP_VERSION__}</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class={styles['header-right']}>
|
||||
<CommunitySelector />
|
||||
|
@@ -56,21 +56,12 @@ const LoginPage = () => {
|
||||
<div class={styles['login-form-container']}>
|
||||
<form class={formStyles.form} onSubmit={handleSubmit}>
|
||||
<img src={publyLogo} alt="Logo" class={styles['login-logo']} />
|
||||
<h1 class={formStyles.title}>Вход в админ панель</h1>
|
||||
|
||||
<div class={formStyles.fieldGroup}>
|
||||
<label class={formStyles.label}>
|
||||
<span class={formStyles.labelText}>
|
||||
<span class={formStyles.labelIcon}>📧</span>
|
||||
Email
|
||||
<span class={formStyles.required}>*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={username()}
|
||||
onInput={(e) => setUsername(e.currentTarget.value)}
|
||||
placeholder="admin@discours.io"
|
||||
placeholder="admin@media"
|
||||
required
|
||||
class={`${formStyles.input} ${error() ? formStyles.error : ''}`}
|
||||
disabled={loading()}
|
||||
@@ -78,13 +69,6 @@ const LoginPage = () => {
|
||||
</div>
|
||||
|
||||
<div class={formStyles.fieldGroup}>
|
||||
<label class={formStyles.label}>
|
||||
<span class={formStyles.labelText}>
|
||||
<span class={formStyles.labelIcon}>🔒</span>
|
||||
Пароль
|
||||
<span class={formStyles.required}>*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password()}
|
||||
@@ -103,7 +87,7 @@ const LoginPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class={formStyles.actions}>
|
||||
<div class={formStyles.actions} style={{ 'margin': 'auto' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
|
@@ -6,7 +6,7 @@ import { query } from '../graphql'
|
||||
import type { Query, AdminShoutInfo as Shout } from '../graphql/generated/schema'
|
||||
import { ADMIN_GET_SHOUTS_QUERY } from '../graphql/queries'
|
||||
import styles from '../styles/Admin.module.css'
|
||||
import EditableCodePreview from '../ui/EditableCodePreview'
|
||||
import HTMLEditor from '../ui/HTMLEditor'
|
||||
import Modal from '../ui/Modal'
|
||||
import Pagination from '../ui/Pagination'
|
||||
import SortableHeader from '../ui/SortableHeader'
|
||||
@@ -351,53 +351,73 @@ const ShoutsRoute = (props: ShoutsRouteProps) => {
|
||||
<Modal
|
||||
isOpen={showBodyModal()}
|
||||
onClose={() => setShowBodyModal(false)}
|
||||
title="Содержимое публикации"
|
||||
title="Редактирование содержимого публикации"
|
||||
size="large"
|
||||
footer={
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
class={`${styles.button} ${styles.secondary}`}
|
||||
onClick={() => setShowBodyModal(false)}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`${styles.button} ${styles.primary}`}
|
||||
onClick={() => {
|
||||
// TODO: добавить логику сохранения изменений в базу данных
|
||||
props.onSuccess?.('Содержимое публикации обновлено')
|
||||
setShowBodyModal(false)
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EditableCodePreview
|
||||
content={selectedShoutBody()}
|
||||
maxHeight="85vh"
|
||||
language="html"
|
||||
autoFormat={true}
|
||||
onContentChange={(newContent) => {
|
||||
setSelectedShoutBody(newContent)
|
||||
}}
|
||||
onSave={(_content) => {
|
||||
// FIXME: добавить логику сохранения изменений в базу данных
|
||||
props.onSuccess?.('Содержимое публикации обновлено')
|
||||
setShowBodyModal(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowBodyModal(false)
|
||||
}}
|
||||
placeholder="Введите содержимое публикации..."
|
||||
/>
|
||||
<div style="padding: 1rem;">
|
||||
<HTMLEditor
|
||||
value={selectedShoutBody()}
|
||||
onInput={(value) => setSelectedShoutBody(value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={showMediaBodyModal()}
|
||||
onClose={() => setShowMediaBodyModal(false)}
|
||||
title="Содержимое media.body"
|
||||
title="Редактирование содержимого media.body"
|
||||
size="large"
|
||||
footer={
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
class={`${styles.button} ${styles.secondary}`}
|
||||
onClick={() => setShowMediaBodyModal(false)}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`${styles.button} ${styles.primary}`}
|
||||
onClick={() => {
|
||||
// TODO: добавить логику сохранения изменений media.body
|
||||
props.onSuccess?.('Содержимое media.body обновлено')
|
||||
setShowMediaBodyModal(false)
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EditableCodePreview
|
||||
content={selectedMediaBody()}
|
||||
maxHeight="85vh"
|
||||
language="html"
|
||||
autoFormat={true}
|
||||
onContentChange={(newContent) => {
|
||||
setSelectedMediaBody(newContent)
|
||||
}}
|
||||
onSave={(_content) => {
|
||||
// FIXME: добавить логику сохранения изменений media.body
|
||||
props.onSuccess?.('Содержимое media.body обновлено')
|
||||
setShowMediaBodyModal(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowMediaBodyModal(false)
|
||||
}}
|
||||
placeholder="Введите содержимое media.body..."
|
||||
/>
|
||||
<div style="padding: 1rem;">
|
||||
<HTMLEditor
|
||||
value={selectedMediaBody()}
|
||||
onInput={(value) => setSelectedMediaBody(value)}
|
||||
/>gjl
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
|
@@ -14,7 +14,7 @@ interface TopicsProps {
|
||||
}
|
||||
|
||||
export const Topics = (props: TopicsProps) => {
|
||||
const { selectedCommunity, loadTopicsByCommunity, topics: contextTopics } = useData()
|
||||
const { selectedCommunity, loadTopicsByCommunity, topics: contextTopics, getTopicTitle } = useData()
|
||||
|
||||
// Состояние поиска
|
||||
const [searchQuery, setSearchQuery] = createSignal('')
|
||||
@@ -133,16 +133,32 @@ export const Topics = (props: TopicsProps) => {
|
||||
/**
|
||||
* Сохранение изменений топика
|
||||
*/
|
||||
const handleTopicSave = (updatedTopic: Topic) => {
|
||||
const handleTopicSave = async (updatedTopic: Topic) => {
|
||||
console.log('[TopicsRoute] Saving topic:', updatedTopic)
|
||||
console.log('[TopicsRoute] Topic parent_ids:', updatedTopic.parent_ids)
|
||||
|
||||
// TODO: добавить логику сохранения изменений в базу данных
|
||||
// await updateTopic(updatedTopic)
|
||||
// Сразу обновляем локальные данные для мгновенного отображения
|
||||
const currentTopics = contextTopics()
|
||||
console.log('[TopicsRoute] Current topics count:', currentTopics.length)
|
||||
|
||||
const updatedTopics = currentTopics.map(topic =>
|
||||
topic.id === updatedTopic.id ? updatedTopic : topic
|
||||
)
|
||||
|
||||
console.log('[TopicsRoute] Updated topics count:', updatedTopics.length)
|
||||
|
||||
// Обновляем состояние контекста напрямую (это сработает мгновенно)
|
||||
const { setTopics } = useData()
|
||||
setTopics(updatedTopics)
|
||||
|
||||
props.onSuccess?.('Топик успешно обновлён')
|
||||
|
||||
// Обновляем локальные данные (пока что просто перезагружаем)
|
||||
void loadTopicsForCommunity()
|
||||
// Ждем большее время чтобы сервер точно обработал изменения и инвалидировал кеш
|
||||
console.log('[TopicsRoute] Scheduling reload in 500ms...')
|
||||
setTimeout(() => {
|
||||
console.log('[TopicsRoute] Reloading topics from server...')
|
||||
void loadTopicsForCommunity()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,6 +168,40 @@ export const Topics = (props: TopicsProps) => {
|
||||
props.onError?.(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Рендер родительских тем для топика
|
||||
*/
|
||||
const renderParentTopics = (parentIds?: number[]) => {
|
||||
if (!parentIds || parentIds.length === 0) {
|
||||
return <span style="color: #999; font-style: italic;">Нет родителей</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
|
||||
<For each={parentIds}>
|
||||
{(parentId) => {
|
||||
const parentTitle = getTopicTitle(parentId)
|
||||
return (
|
||||
<span
|
||||
style="
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
"
|
||||
title={`ID: ${parentId}`}
|
||||
>
|
||||
#{parentTitle || `ID:${parentId}`}
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Рендер строки топика
|
||||
*/
|
||||
@@ -169,6 +219,9 @@ export const Topics = (props: TopicsProps) => {
|
||||
<td class={styles.tableCell} title={topic.slug}>
|
||||
{truncateText(topic.slug, 30)}
|
||||
</td>
|
||||
<td class={styles.tableCell}>
|
||||
{renderParentTopics(topic.parent_ids)}
|
||||
</td>
|
||||
<td class={styles.tableCell}>
|
||||
{topic.body ? (
|
||||
<span style="color: #666;">{truncateText(topic.body.replace(/<[^>]*>/g, ''), 60)}</span>
|
||||
@@ -203,20 +256,21 @@ export const Topics = (props: TopicsProps) => {
|
||||
<SortableHeader field="slug" allowedFields={TOPICS_SORT_CONFIG.allowedFields}>
|
||||
Slug
|
||||
</SortableHeader>
|
||||
<th class={styles.tableHeaderCell}>Родительские темы</th>
|
||||
<th class={styles.tableHeaderCell}>Body</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={loading()}>
|
||||
<tr>
|
||||
<td colspan="4" class={styles.loadingCell}>
|
||||
<td colspan="5" class={styles.loadingCell}>
|
||||
Загрузка...
|
||||
</td>
|
||||
</tr>
|
||||
</Show>
|
||||
<Show when={!loading() && sortedTopics().length === 0}>
|
||||
<tr>
|
||||
<td colspan="4" class={styles.emptyCell}>
|
||||
<td colspan="5" class={styles.emptyCell}>
|
||||
Нет топиков
|
||||
</td>
|
||||
</tr>
|
||||
|
Reference in New Issue
Block a user