shouts-admin-fix4
This commit is contained in:
parent
20840b2d54
commit
8f93d77eef
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -1,15 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "publy-admin",
|
"name": "admin-panel",
|
||||||
"version": "0.4.20",
|
"version": "0.4.22",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "publy-admin",
|
"name": "admin-panel",
|
||||||
"version": "0.4.20",
|
"version": "0.4.22",
|
||||||
|
"dependencies": {
|
||||||
|
"prismjs": "^1.30.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@types/node": "^22.15.0",
|
"@types/node": "^22.15.0",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"graphql": "^16.8.0",
|
"graphql": "^16.8.0",
|
||||||
"solid-js": "^1.9.6",
|
"solid-js": "^1.9.6",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
|
@ -1283,6 +1287,13 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prismjs": {
|
||||||
|
"version": "1.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
|
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
|
@ -1745,6 +1756,15 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prismjs": {
|
||||||
|
"version": "1.30.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||||
|
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.40.2",
|
"version": "4.40.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
||||||
|
|
|
@ -13,11 +13,15 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@types/node": "^22.15.0",
|
"@types/node": "^22.15.0",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"graphql": "^16.8.0",
|
"graphql": "^16.8.0",
|
||||||
"solid-js": "^1.9.6",
|
"solid-js": "^1.9.6",
|
||||||
|
"terser": "^5.39.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vite": "^6.3.0",
|
"vite": "^6.3.0",
|
||||||
"vite-plugin-solid": "^2.11.0",
|
"vite-plugin-solid": "^2.11.0"
|
||||||
"terser": "^5.39.0"
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"prismjs": "^1.30.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
572
panel/admin.tsx
572
panel/admin.tsx
|
@ -6,6 +6,64 @@
|
||||||
import { Component, For, Show, createSignal, onMount, createEffect } from 'solid-js'
|
import { Component, For, Show, createSignal, onMount, createEffect } from 'solid-js'
|
||||||
import { logout } from './auth'
|
import { logout } from './auth'
|
||||||
import { query } from './graphql'
|
import { query } from './graphql'
|
||||||
|
import Prism from 'prismjs'
|
||||||
|
import 'prismjs/components/prism-json'
|
||||||
|
import 'prismjs/components/prism-markup'
|
||||||
|
import 'prismjs/themes/prism-tomorrow.css'
|
||||||
|
|
||||||
|
// Определяем GraphQL запрос
|
||||||
|
const ADMIN_GET_SHOUTS_QUERY = `
|
||||||
|
query AdminGetShouts($limit: Int, $offset: Int, $status: String) {
|
||||||
|
admin_get_shouts(limit: $limit, offset: $offset, status: $status) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
body
|
||||||
|
lead
|
||||||
|
subtitle
|
||||||
|
layout
|
||||||
|
lang
|
||||||
|
cover
|
||||||
|
cover_caption
|
||||||
|
media {
|
||||||
|
url
|
||||||
|
title
|
||||||
|
body
|
||||||
|
source
|
||||||
|
pic
|
||||||
|
date
|
||||||
|
genre
|
||||||
|
artist
|
||||||
|
lyrics
|
||||||
|
}
|
||||||
|
seo
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
published_at
|
||||||
|
featured_at
|
||||||
|
deleted_at
|
||||||
|
created_by {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
name
|
||||||
|
}
|
||||||
|
authors {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
topics {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
version_of
|
||||||
|
draft
|
||||||
|
}
|
||||||
|
admin_get_shouts_count
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Интерфейс для данных пользователя
|
* Интерфейс для данных пользователя
|
||||||
|
@ -145,14 +203,9 @@ interface Shout {
|
||||||
/**
|
/**
|
||||||
* Интерфейс для ответа API с публикациями
|
* Интерфейс для ответа API с публикациями
|
||||||
*/
|
*/
|
||||||
interface AdminGetShoutsResponse {
|
interface AdminGetShoutsData {
|
||||||
adminGetShouts: {
|
admin_get_shouts: any[]
|
||||||
shouts: Shout[]
|
admin_get_shouts_count: number
|
||||||
total: number
|
|
||||||
page: number
|
|
||||||
perPage: number
|
|
||||||
totalPages: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -175,6 +228,8 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
|
const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
|
||||||
const [showRolesModal, setShowRolesModal] = createSignal(false)
|
const [showRolesModal, setShowRolesModal] = createSignal(false)
|
||||||
const [successMessage, setSuccessMessage] = createSignal<string | null>(null)
|
const [successMessage, setSuccessMessage] = createSignal<string | null>(null)
|
||||||
|
const [showBodyModal, setShowBodyModal] = createSignal(false)
|
||||||
|
const [selectedShoutBody, setSelectedShoutBody] = createSignal<string>('')
|
||||||
|
|
||||||
// Переменные среды
|
// Переменные среды
|
||||||
const [envSections, setEnvSections] = createSignal<EnvSection[]>([])
|
const [envSections, setEnvSections] = createSignal<EnvSection[]>([])
|
||||||
|
@ -209,9 +264,9 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
totalPages: number
|
totalPages: number
|
||||||
}>({
|
}>({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 10,
|
limit: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
totalPages: 1
|
totalPages: 0
|
||||||
})
|
})
|
||||||
const [shoutsSearchQuery, setShoutsSearchQuery] = createSignal('')
|
const [shoutsSearchQuery, setShoutsSearchQuery] = createSignal('')
|
||||||
|
|
||||||
|
@ -344,89 +399,32 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
* Загрузка списка публикаций с учетом пагинации и поиска
|
* Загрузка списка публикаций с учетом пагинации и поиска
|
||||||
*/
|
*/
|
||||||
async function loadShouts() {
|
async function loadShouts() {
|
||||||
|
setShowRolesModal(false)
|
||||||
setShoutsLoading(true)
|
setShoutsLoading(true)
|
||||||
setError(null)
|
const pagination = shoutsPagination()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { page, limit } = shoutsPagination()
|
const result = await query<AdminGetShoutsData>(
|
||||||
const offset = (page - 1) * limit
|
|
||||||
const search = shoutsSearchQuery().trim()
|
|
||||||
const status = shoutsStatus()
|
|
||||||
|
|
||||||
const data = await query<AdminGetShoutsResponse>(
|
|
||||||
`${location.origin}/graphql`,
|
`${location.origin}/graphql`,
|
||||||
`
|
ADMIN_GET_SHOUTS_QUERY,
|
||||||
query AdminGetShouts($limit: Int, $offset: Int, $search: String, $status: String) {
|
{
|
||||||
adminGetShouts(limit: $limit, offset: $offset, search: $search, status: $status) {
|
offset: (pagination.page - 1) * pagination.limit,
|
||||||
shouts {
|
limit: pagination.limit,
|
||||||
id
|
status: shoutsStatus()
|
||||||
title
|
|
||||||
slug
|
|
||||||
body
|
|
||||||
lead
|
|
||||||
subtitle
|
|
||||||
layout
|
|
||||||
lang
|
|
||||||
cover
|
|
||||||
cover_caption
|
|
||||||
media {
|
|
||||||
url
|
|
||||||
title
|
|
||||||
body
|
|
||||||
source
|
|
||||||
pic
|
|
||||||
date
|
|
||||||
genre
|
|
||||||
artist
|
|
||||||
lyrics
|
|
||||||
}
|
|
||||||
seo
|
|
||||||
created_at
|
|
||||||
updated_at
|
|
||||||
published_at
|
|
||||||
featured_at
|
|
||||||
deleted_at
|
|
||||||
created_by {
|
|
||||||
id
|
|
||||||
email
|
|
||||||
name
|
|
||||||
}
|
|
||||||
authors {
|
|
||||||
id
|
|
||||||
email
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
topics {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
version_of
|
|
||||||
draft
|
|
||||||
}
|
|
||||||
total
|
|
||||||
page
|
|
||||||
perPage
|
|
||||||
totalPages
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`,
|
|
||||||
{ limit, offset, search: search || undefined, status }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (data?.adminGetShouts) {
|
if (result?.admin_get_shouts) {
|
||||||
setShouts(data.adminGetShouts.shouts)
|
setShouts(result.admin_get_shouts)
|
||||||
|
// Обновляем пагинацию с учетом общего количества
|
||||||
setShoutsPagination({
|
setShoutsPagination({
|
||||||
page: data.adminGetShouts.page,
|
...pagination,
|
||||||
limit: data.adminGetShouts.perPage,
|
total: result.admin_get_shouts_count || 0,
|
||||||
total: data.adminGetShouts.total,
|
totalPages: Math.ceil((result.admin_get_shouts_count || 0) / pagination.limit)
|
||||||
totalPages: data.adminGetShouts.totalPages
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.error('Ошибка при загрузке публикаций:', err)
|
console.error('Error loading shouts:', error)
|
||||||
setError('Не удалось загрузить публикации')
|
|
||||||
} finally {
|
} finally {
|
||||||
setShoutsLoading(false)
|
setShoutsLoading(false)
|
||||||
}
|
}
|
||||||
|
@ -1219,8 +1217,398 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
return text.substring(0, maxLength) + '...'
|
return text.substring(0, maxLength) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определяет язык контента (html или json)
|
||||||
|
*/
|
||||||
|
function detectLanguage(content: string): string {
|
||||||
|
// Пробуем распарсить как JSON
|
||||||
|
try {
|
||||||
|
JSON.parse(content)
|
||||||
|
return 'json'
|
||||||
|
} catch {
|
||||||
|
// Проверяем на наличие HTML тегов
|
||||||
|
if (/<[^>]*>/g.test(content)) {
|
||||||
|
return 'markup'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'plaintext'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует и подсвечивает код
|
||||||
|
*/
|
||||||
|
function formatCode(content: string): string {
|
||||||
|
const language = detectLanguage(content)
|
||||||
|
|
||||||
|
if (language === 'json') {
|
||||||
|
try {
|
||||||
|
// Форматируем JSON с отступами
|
||||||
|
const formatted = JSON.stringify(JSON.parse(content), null, 2)
|
||||||
|
return Prism.highlight(formatted, Prism.languages[language], language)
|
||||||
|
} catch {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
} else if (language === 'markup') {
|
||||||
|
// Для HTML используем как есть
|
||||||
|
return Prism.highlight(content, Prism.languages[language], language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Компонент модального окна для просмотра содержимого публикации
|
||||||
|
*/
|
||||||
|
const BodyModal: Component = () => {
|
||||||
|
const language = () => detectLanguage(selectedShoutBody())
|
||||||
|
const formattedCode = () => formatCode(selectedShoutBody())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={showBodyModal()}>
|
||||||
|
<div class="modal-overlay" onClick={() => setShowBodyModal(false)}>
|
||||||
|
<div class="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title">
|
||||||
|
<h3>Содержимое публикации</h3>
|
||||||
|
<span class="language-badge">
|
||||||
|
{language() === 'markup' ? 'HTML' :
|
||||||
|
language() === 'json' ? 'JSON' :
|
||||||
|
'Plain Text'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="close-button" onClick={() => setShowBodyModal(false)}>×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre class="body-content">
|
||||||
|
<code class={`language-${language()}`} innerHTML={formattedCode()} />
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик изменения страницы для публикаций
|
||||||
|
* @param page - Номер страницы
|
||||||
|
*/
|
||||||
|
function handleShoutsPageChange(page: number) {
|
||||||
|
setShoutsPagination({ ...shoutsPagination(), page })
|
||||||
|
loadShouts()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик изменения количества публикаций на странице
|
||||||
|
* @param limit - Количество элементов
|
||||||
|
*/
|
||||||
|
function handleShoutsPerPageChange(limit: number) {
|
||||||
|
setShoutsPagination({ ...shoutsPagination(), page: 1, limit })
|
||||||
|
loadShouts()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Компонент пагинации для публикаций
|
||||||
|
*/
|
||||||
|
const ShoutsPagination: Component = () => {
|
||||||
|
const pagination = shoutsPagination()
|
||||||
|
const totalPages = pagination.totalPages
|
||||||
|
const currentPage = pagination.page
|
||||||
|
|
||||||
|
// Генерируем массив страниц для отображения
|
||||||
|
const pages = []
|
||||||
|
const maxVisiblePages = 5 // Максимальное количество видимых страниц
|
||||||
|
|
||||||
|
// Всегда показываем первую страницу
|
||||||
|
pages.push(1)
|
||||||
|
|
||||||
|
// Вычисляем диапазон страниц вокруг текущей
|
||||||
|
let startPage = Math.max(2, currentPage - Math.floor(maxVisiblePages / 2))
|
||||||
|
let endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 2)
|
||||||
|
|
||||||
|
// Корректируем диапазон, если он выходит за границы
|
||||||
|
if (endPage - startPage < maxVisiblePages - 2) {
|
||||||
|
startPage = Math.max(2, endPage - maxVisiblePages + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем многоточие после первой страницы, если нужно
|
||||||
|
if (startPage > 2) {
|
||||||
|
pages.push('...')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем страницы из диапазона
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем многоточие перед последней страницей, если нужно
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pages.push('...')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда показываем последнюю страницу, если есть больше одной страницы
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pages.push(totalPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="pagination">
|
||||||
|
<div class="pagination-info">
|
||||||
|
Показано {((currentPage - 1) * pagination.limit) + 1} - {Math.min(currentPage * pagination.limit, pagination.total)} из {pagination.total}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-controls">
|
||||||
|
<button
|
||||||
|
class="pagination-button"
|
||||||
|
onClick={() => handleShoutsPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<For each={pages}>
|
||||||
|
{(page) => (
|
||||||
|
<>
|
||||||
|
{page === '...' ? (
|
||||||
|
<span class="pagination-ellipsis">...</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
class={`pagination-button ${page === currentPage ? 'active' : ''}`}
|
||||||
|
onClick={() => handleShoutsPageChange(Number(page))}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="pagination-button"
|
||||||
|
onClick={() => handleShoutsPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination-per-page">
|
||||||
|
На странице:
|
||||||
|
<select
|
||||||
|
value={pagination.limit}
|
||||||
|
onChange={(e) => handleShoutsPerPageChange(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем стили для пагинации
|
||||||
|
const styles = `
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 35px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button:hover:not(:disabled) {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-ellipsis {
|
||||||
|
padding: 5px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-per-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-per-page select {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 1200px;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-content {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
tab-size: 2;
|
||||||
|
hyphens: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-content code {
|
||||||
|
font-family: inherit;
|
||||||
|
background: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
white-space: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Улучшаем стили для Prism */
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol {
|
||||||
|
color: #0099ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin {
|
||||||
|
color: #92d692;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.token.variable,
|
||||||
|
.token.inserted {
|
||||||
|
color: #9a6e3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.keyword {
|
||||||
|
color: #e68ac6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="admin-page">
|
<div class="admin-page">
|
||||||
|
<style>{styles}</style>
|
||||||
<header>
|
<header>
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<h1>Панель администратора</h1>
|
<h1>Панель администратора</h1>
|
||||||
|
@ -1381,7 +1769,7 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
<th>Авторы</th>
|
<th>Авторы</th>
|
||||||
<th>Темы</th>
|
<th>Темы</th>
|
||||||
<th>Создан</th>
|
<th>Создан</th>
|
||||||
<th>Body (preview)</th>
|
<th>Содержимое</th>
|
||||||
<th>Media</th>
|
<th>Media</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -1430,10 +1818,15 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
<td>{formatDateRelative(shout.created_at)}</td>
|
<td>{formatDateRelative(shout.created_at)}</td>
|
||||||
<td title={shout.body}>
|
<td
|
||||||
<div class="body-preview">
|
class="body-cell"
|
||||||
{truncateText(shout.body.replace(/<[^>]*>/g, ''), 100)}
|
onClick={() => {
|
||||||
</div>
|
setSelectedShoutBody(shout.body)
|
||||||
|
setShowBodyModal(true)
|
||||||
|
}}
|
||||||
|
style="cursor: pointer; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||||
|
>
|
||||||
|
{truncateText(shout.body.replace(/<[^>]*>/g, ''), 100)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Show when={shout.media && shout.media.length > 0}>
|
<Show when={shout.media && shout.media.length > 0}>
|
||||||
|
@ -1448,6 +1841,7 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<ShoutsPagination />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -1464,6 +1858,8 @@ const AdminPage: Component<AdminPageProps> = (props) => {
|
||||||
<Show when={showVariableModal()}>
|
<Show when={showVariableModal()}>
|
||||||
<VariableModal />
|
<VariableModal />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<BodyModal />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user