0.7.7-topics-editing
All checks were successful
Deploy on push / deploy (push) Successful in 6s

This commit is contained in:
2025-07-03 12:15:10 +03:00
parent 441cca8045
commit eb2140bcc6
27 changed files with 3097 additions and 805 deletions

View File

@@ -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 />

View File

@@ -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"

View File

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

View File

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