0.5.10-invites-crud
All checks were successful
Deploy on push / deploy (push) Successful in 7s

This commit is contained in:
2025-06-30 22:19:46 +03:00
parent 1e2c85e56a
commit 41395eb7c6
14 changed files with 1748 additions and 385 deletions

View File

@@ -5,6 +5,7 @@ import {
UPDATE_COLLECTION_MUTATION
} from '../graphql/mutations'
import { GET_COLLECTIONS_QUERY } from '../graphql/queries'
import CollectionEditModal from '../modals/CollectionEditModal'
import styles from '../styles/Table.module.css'
import Button from '../ui/Button'
import Modal from '../ui/Modal'
@@ -49,14 +50,6 @@ const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
})
const [createModal, setCreateModal] = createSignal(false)
// Форма для редактирования/создания
const [formData, setFormData] = createSignal({
slug: '',
title: '',
desc: '',
pic: ''
})
/**
* Загружает список всех коллекций
*/
@@ -98,12 +91,6 @@ const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
* Открывает модалку редактирования
*/
const openEditModal = (collection: Collection) => {
setFormData({
slug: collection.slug,
title: collection.title,
desc: collection.desc || '',
pic: collection.pic
})
setEditModal({ show: true, collection })
}
@@ -111,28 +98,25 @@ const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
* Открывает модалку создания
*/
const openCreateModal = () => {
setFormData({
slug: '',
title: '',
desc: '',
pic: ''
})
setCreateModal(true)
}
/**
* Создает новую коллекцию
* Обрабатывает сохранение коллекции (создание или обновление)
*/
const createCollection = async () => {
const handleSaveCollection = async (collectionData: Partial<Collection>) => {
try {
const isCreating = createModal()
const mutation = isCreating ? CREATE_COLLECTION_MUTATION : UPDATE_COLLECTION_MUTATION
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: CREATE_COLLECTION_MUTATION,
variables: { collection_input: formData() }
query: mutation,
variables: { collection_input: collectionData }
})
})
@@ -142,49 +126,19 @@ const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
throw new Error(result.errors[0].message)
}
if (result.data.create_collection.error) {
throw new Error(result.data.create_collection.error)
const resultData = isCreating ? result.data.create_collection : result.data.update_collection
if (resultData.error) {
throw new Error(resultData.error)
}
props.onSuccess('Коллекция успешно создана')
props.onSuccess(isCreating ? 'Коллекция успешно создана' : 'Коллекция успешно обновлена')
setCreateModal(false)
await loadCollections()
} catch (error) {
props.onError(`Ошибка создания коллекции: ${(error as Error).message}`)
}
}
/**
* Обновляет коллекцию
*/
const updateCollection = async () => {
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: UPDATE_COLLECTION_MUTATION,
variables: { collection_input: formData() }
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
if (result.data.update_collection.error) {
throw new Error(result.data.update_collection.error)
}
props.onSuccess('Коллекция успешно обновлена')
setEditModal({ show: false, collection: null })
await loadCollections()
} catch (error) {
props.onError(`Ошибка обновления коллекции: ${(error as Error).message}`)
props.onError(
`Ошибка ${createModal() ? 'создания' : 'обновления'} коллекции: ${(error as Error).message}`
)
}
}
@@ -230,7 +184,6 @@ const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
return (
<div class={styles.container}>
<div class={styles.header}>
<h2>Управление коллекциями</h2>
<div style={{ display: 'flex', gap: '10px' }}>
<Button onClick={openCreateModal} variant="primary">
Создать коллекцию
@@ -313,181 +266,20 @@ const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
</Show>
{/* Модальное окно создания */}
<Modal isOpen={createModal()} onClose={() => setCreateModal(false)} title="Создание новой коллекции">
<div style={{ padding: '20px' }}>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Slug <span style={{ color: 'red' }}>*</span>
</label>
<input
type="text"
value={formData().slug}
onInput={(e) => setFormData((prev) => ({ ...prev, slug: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
required
placeholder="my-collection"
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Название <span style={{ color: 'red' }}>*</span>
</label>
<input
type="text"
value={formData().title}
onInput={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
required
placeholder="Моя коллекция"
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Описание
</label>
<textarea
value={formData().desc}
onInput={(e) => setFormData((prev) => ({ ...prev, desc: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px',
'min-height': '80px',
resize: 'vertical'
}}
placeholder="Описание коллекции..."
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Картинка (URL)
</label>
<input
type="text"
value={formData().pic}
onInput={(e) => setFormData((prev) => ({ ...prev, pic: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
placeholder="https://example.com/image.jpg"
/>
</div>
<div class={styles['modal-actions']}>
<Button variant="secondary" onClick={() => setCreateModal(false)}>
Отмена
</Button>
<Button variant="primary" onClick={createCollection}>
Создать
</Button>
</div>
</div>
</Modal>
<CollectionEditModal
isOpen={createModal()}
collection={null}
onClose={() => setCreateModal(false)}
onSave={handleSaveCollection}
/>
{/* Модальное окно редактирования */}
<Modal
<CollectionEditModal
isOpen={editModal().show}
collection={editModal().collection}
onClose={() => setEditModal({ show: false, collection: null })}
title={`Редактирование коллекции: ${editModal().collection?.title || ''}`}
>
<div style={{ padding: '20px' }}>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>Slug</label>
<input
type="text"
value={formData().slug}
onInput={(e) => setFormData((prev) => ({ ...prev, slug: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
required
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Название
</label>
<input
type="text"
value={formData().title}
onInput={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Описание
</label>
<textarea
value={formData().desc}
onInput={(e) => setFormData((prev) => ({ ...prev, desc: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px',
'min-height': '80px',
resize: 'vertical'
}}
placeholder="Описание коллекции..."
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Картинка (URL)
</label>
<input
type="text"
value={formData().pic}
onInput={(e) => setFormData((prev) => ({ ...prev, pic: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
placeholder="https://example.com/image.jpg"
/>
</div>
<div class={styles['modal-actions']}>
<Button variant="secondary" onClick={() => setEditModal({ show: false, collection: null })}>
Отмена
</Button>
<Button variant="primary" onClick={updateCollection}>
Сохранить
</Button>
</div>
</div>
</Modal>
onSave={handleSaveCollection}
/>
{/* Модальное окно подтверждения удаления */}
<Modal

View File

@@ -1,6 +1,11 @@
import { Component, createSignal, For, onMount, Show } from 'solid-js'
import { DELETE_COMMUNITY_MUTATION, UPDATE_COMMUNITY_MUTATION } from '../graphql/mutations'
import {
CREATE_COMMUNITY_MUTATION,
DELETE_COMMUNITY_MUTATION,
UPDATE_COMMUNITY_MUTATION
} from '../graphql/mutations'
import { GET_COMMUNITIES_QUERY } from '../graphql/queries'
import CommunityEditModal from '../modals/CommunityEditModal'
import styles from '../styles/Table.module.css'
import Button from '../ui/Button'
import Modal from '../ui/Modal'
@@ -46,13 +51,8 @@ const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
show: false,
community: null
})
// Форма для редактирования
const [formData, setFormData] = createSignal({
slug: '',
name: '',
desc: '',
pic: ''
const [createModal, setCreateModal] = createSignal<{ show: boolean }>({
show: false
})
/**
@@ -92,32 +92,36 @@ const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
return new Date(timestamp * 1000).toLocaleDateString('ru-RU')
}
/**
* Открывает модалку создания
*/
const openCreateModal = () => {
setCreateModal({ show: true })
}
/**
* Открывает модалку редактирования
*/
const openEditModal = (community: Community) => {
setFormData({
slug: community.slug,
name: community.name,
desc: community.desc || '',
pic: community.pic
})
setEditModal({ show: true, community })
}
/**
* Обновляет сообщество
* Обрабатывает сохранение сообщества (создание или обновление)
*/
const updateCommunity = async () => {
const handleSaveCommunity = async (communityData: Partial<Community>) => {
try {
const isCreating = !editModal().community && createModal().show
const mutation = isCreating ? CREATE_COMMUNITY_MUTATION : UPDATE_COMMUNITY_MUTATION
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: UPDATE_COMMUNITY_MUTATION,
variables: { community_input: formData() }
query: mutation,
variables: { community_input: communityData }
})
})
@@ -127,15 +131,19 @@ const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
throw new Error(result.errors[0].message)
}
if (result.data.update_community.error) {
throw new Error(result.data.update_community.error)
const resultData = isCreating ? result.data.create_community : result.data.update_community
if (resultData.error) {
throw new Error(resultData.error)
}
props.onSuccess('Сообщество успешно обновлено')
props.onSuccess(isCreating ? 'Сообщество успешно создано' : 'Сообщество успешно обновлено')
setCreateModal({ show: false })
setEditModal({ show: false, community: null })
await loadCommunities()
} catch (error) {
props.onError(`Ошибка обновления сообщества: ${(error as Error).message}`)
props.onError(
`Ошибка ${createModal().show ? 'создания' : 'обновления'} сообщества: ${(error as Error).message}`
)
}
}
@@ -181,10 +189,12 @@ const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
return (
<div class={styles.container}>
<div class={styles.header}>
<h2>Управление сообществами</h2>
<Button onClick={loadCommunities} disabled={loading()}>
{loading() ? 'Загрузка...' : 'Обновить'}
</Button>
<Button variant="primary" onClick={openCreateModal}>
Создать сообщество
</Button>
</div>
<Show
@@ -260,93 +270,21 @@ const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
</table>
</Show>
{/* Модальное окно создания */}
<CommunityEditModal
isOpen={createModal().show}
community={null}
onClose={() => setCreateModal({ show: false })}
onSave={handleSaveCommunity}
/>
{/* Модальное окно редактирования */}
<Modal
<CommunityEditModal
isOpen={editModal().show}
community={editModal().community}
onClose={() => setEditModal({ show: false, community: null })}
title={`Редактирование сообщества: ${editModal().community?.name || ''}`}
>
<div style={{ padding: '20px' }}>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>Slug</label>
<input
type="text"
value={formData().slug}
onInput={(e) => setFormData((prev) => ({ ...prev, slug: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
required
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Название
</label>
<input
type="text"
value={formData().name}
onInput={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Описание
</label>
<textarea
value={formData().desc}
onInput={(e) => setFormData((prev) => ({ ...prev, desc: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px',
'min-height': '80px',
resize: 'vertical'
}}
placeholder="Описание сообщества..."
/>
</div>
<div style={{ 'margin-bottom': '16px' }}>
<label style={{ display: 'block', 'margin-bottom': '4px', 'font-weight': 'bold' }}>
Картинка (URL)
</label>
<input
type="text"
value={formData().pic}
onInput={(e) => setFormData((prev) => ({ ...prev, pic: e.target.value }))}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
'border-radius': '4px'
}}
placeholder="https://example.com/image.jpg"
/>
</div>
<div class={styles['modal-actions']}>
<Button variant="secondary" onClick={() => setEditModal({ show: false, community: null })}>
Отмена
</Button>
<Button variant="primary" onClick={updateCommunity}>
Сохранить
</Button>
</div>
</div>
</Modal>
onSave={handleSaveCommunity}
/>
{/* Модальное окно подтверждения удаления */}
<Modal

424
panel/routes/invites.tsx Normal file
View File

@@ -0,0 +1,424 @@
import { Component, createSignal, For, onMount, Show } from 'solid-js'
import {
ADMIN_CREATE_INVITE_MUTATION,
ADMIN_DELETE_INVITE_MUTATION,
ADMIN_UPDATE_INVITE_MUTATION
} from '../graphql/mutations'
import { ADMIN_GET_INVITES_QUERY } from '../graphql/queries'
import InviteEditModal from '../modals/InviteEditModal'
import styles from '../styles/Table.module.css'
import Button from '../ui/Button'
import Modal from '../ui/Modal'
import Pagination from '../ui/Pagination'
/**
* Интерфейсы для приглашений
*/
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 InvitesRouteProps {
onError: (error: string) => void
onSuccess: (message: string) => void
}
/**
* Компонент для управления приглашениями
*/
const InvitesRoute: Component<InvitesRouteProps> = (props) => {
const [invites, setInvites] = createSignal<Invite[]>([])
const [loading, setLoading] = createSignal(false)
const [search, setSearch] = createSignal('')
const [statusFilter, setStatusFilter] = createSignal('all')
const [pagination, setPagination] = createSignal({
page: 1,
perPage: 10,
total: 0,
totalPages: 1
})
const [editModal, setEditModal] = createSignal<{ show: boolean; invite: Invite | null }>({
show: false,
invite: null
})
const [deleteModal, setDeleteModal] = createSignal<{ show: boolean; invite: Invite | null }>({
show: false,
invite: null
})
const [createModal, setCreateModal] = createSignal<{ show: boolean }>({
show: false
})
/**
* Загружает список приглашений с учетом фильтров и пагинации
*/
const loadInvites = async (page: number = 1) => {
setLoading(true)
try {
const limit = pagination().perPage
const offset = (page - 1) * limit
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: ADMIN_GET_INVITES_QUERY,
variables: {
limit,
offset,
search: search().trim() || null,
status: statusFilter() === 'all' ? null : statusFilter()
}
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
const data = result.data.adminGetInvites
setInvites(data.invites || [])
setPagination({
page: data.page || 1,
perPage: data.perPage || 10,
total: data.total || 0,
totalPages: data.totalPages || 1
})
} catch (error) {
props.onError(`Ошибка загрузки приглашений: ${(error as Error).message}`)
} finally {
setLoading(false)
}
}
/**
* Обработчик изменения страницы
*/
const handlePageChange = (page: number) => {
void loadInvites(page)
}
/**
* Обработчик поиска
*/
const handleSearch = () => {
void loadInvites(1) // Сброс на первую страницу при поиске
}
/**
* Обработчик изменения фильтра статуса
*/
const handleStatusFilterChange = (status: string) => {
setStatusFilter(status)
void loadInvites(1)
}
/**
* Получает отображаемое название статуса
*/
const getStatusDisplay = (status: string) => {
switch (status) {
case 'PENDING':
return { text: 'Ожидает', badge: 'warning' }
case 'ACCEPTED':
return { text: 'Принято', badge: 'success' }
case 'REJECTED':
return { text: 'Отклонено', badge: 'error' }
default:
return { text: status, badge: 'secondary' }
}
}
/**
* Открывает модалку создания
*/
const openCreateModal = () => {
setCreateModal({ show: true })
}
/**
* Открывает модалку редактирования
*/
const openEditModal = (invite: Invite) => {
setEditModal({ show: true, invite })
}
/**
* Обрабатывает сохранение приглашения (создание или обновление)
*/
const handleSaveInvite = async (inviteData: Partial<Invite>) => {
try {
const isCreating = !editModal().invite && createModal().show
const mutation = isCreating ? ADMIN_CREATE_INVITE_MUTATION : ADMIN_UPDATE_INVITE_MUTATION
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: mutation,
variables: { invite: inviteData }
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
const resultData = isCreating ? result.data.adminCreateInvite : result.data.adminUpdateInvite
if (!resultData.success) {
throw new Error(resultData.error || 'Неизвестная ошибка')
}
props.onSuccess(isCreating ? 'Приглашение успешно создано' : 'Приглашение успешно обновлено')
setCreateModal({ show: false })
setEditModal({ show: false, invite: null })
await loadInvites(pagination().page)
} catch (error) {
props.onError(
`Ошибка ${createModal().show ? 'создания' : 'обновления'} приглашения: ${(error as Error).message}`
)
}
}
/**
* Удаляет приглашение
*/
const deleteInvite = async (invite: Invite) => {
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: ADMIN_DELETE_INVITE_MUTATION,
variables: {
inviter_id: invite.inviter_id,
author_id: invite.author_id,
shout_id: invite.shout_id
}
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
if (!result.data.adminDeleteInvite.success) {
throw new Error(result.data.adminDeleteInvite.error || 'Неизвестная ошибка')
}
props.onSuccess('Приглашение успешно удалено')
setDeleteModal({ show: false, invite: null })
await loadInvites(pagination().page)
} catch (error) {
props.onError(`Ошибка удаления приглашения: ${(error as Error).message}`)
}
}
// Загружаем приглашения при монтировании компонента
onMount(() => {
void loadInvites()
})
return (
<div class={styles.container}>
<div class={styles.header}>
<div class={styles.controls}>
<input
type="text"
placeholder="Поиск приглашений..."
value={search()}
onInput={(e) => setSearch(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
class={styles.searchInput}
/>
<Button onClick={handleSearch} disabled={loading()}>
🔍
</Button>
<select
value={statusFilter()}
onChange={(e) => handleStatusFilterChange(e.target.value)}
class={styles.statusFilter}
>
<option value="all">Все статусы</option>
<option value="pending">Ожидает ответа</option>
<option value="accepted">Принято</option>
<option value="rejected">Отклонено</option>
</select>
<Button onClick={() => loadInvites(pagination().page)} disabled={loading()}>
{loading() ? 'Загрузка...' : 'Обновить'}
</Button>
</div>
<Button variant="primary" onClick={openCreateModal}>
Создать приглашение
</Button>
</div>
<Show when={loading()}>
<div class={styles.loading}>Загрузка приглашений...</div>
</Show>
<Show when={!loading() && invites().length === 0}>
<div class={styles.empty}>Приглашения не найдены</div>
</Show>
<Show when={!loading() && invites().length > 0}>
<div class={styles.tableContainer}>
<table class={styles.table}>
<thead>
<tr>
<th>Приглашающий</th>
<th>Приглашаемый</th>
<th>Публикация</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<For each={invites()}>
{(invite) => {
const statusDisplay = getStatusDisplay(invite.status)
return (
<tr
class={styles.clickableRow}
onClick={() => openEditModal(invite)}
title="Нажмите для редактирования"
>
<td>
<div>
<strong>{invite.inviter.name || 'Без имени'}</strong>
</div>
<div class={styles.subtitle}>{invite.inviter.email}</div>
<div class={styles.subtitle}>ID: {invite.inviter_id}</div>
</td>
<td>
<div>
<strong>{invite.author.name || 'Без имени'}</strong>
</div>
<div class={styles.subtitle}>{invite.author.email}</div>
<div class={styles.subtitle}>ID: {invite.author_id}</div>
</td>
<td>
<div>
<strong>{invite.shout.title}</strong>
</div>
<div class={styles.subtitle}>Автор: {invite.shout.created_by.name}</div>
<div class={styles.subtitle}>ID: {invite.shout_id}</div>
</td>
<td>
<span class={`${styles.badge} ${styles[statusDisplay.badge]}`}>
{statusDisplay.text}
</span>
</td>
<td>
<button
class={styles.deleteButton}
onClick={(e) => {
e.stopPropagation()
setDeleteModal({ show: true, invite })
}}
title="Удалить приглашение"
>
×
</button>
</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
<Pagination
currentPage={pagination().page}
totalPages={pagination().totalPages}
total={pagination().total}
limit={pagination().perPage}
onPageChange={handlePageChange}
/>
</Show>
{/* Модальные окна */}
<InviteEditModal
isOpen={createModal().show}
invite={null}
onClose={() => setCreateModal({ show: false })}
onSave={handleSaveInvite}
/>
<InviteEditModal
isOpen={editModal().show}
invite={editModal().invite}
onClose={() => setEditModal({ show: false, invite: null })}
onSave={handleSaveInvite}
/>
{/* Модальное окно подтверждения удаления */}
<Modal
isOpen={deleteModal().show}
onClose={() => setDeleteModal({ show: false, invite: null })}
title="Подтверждение удаления"
size="small"
>
<div class={styles.deleteConfirmation}>
<p>
Вы действительно хотите удалить приглашение от{' '}
<strong>{deleteModal().invite?.inviter.name}</strong> для{' '}
<strong>{deleteModal().invite?.author.name}</strong> к публикации{' '}
<strong>"{deleteModal().invite?.shout.title}"</strong>?
</p>
<div class={styles.modalActions}>
<Button variant="secondary" onClick={() => setDeleteModal({ show: false, invite: null })}>
Отмена
</Button>
<Button
variant="danger"
onClick={() => deleteModal().invite && deleteInvite(deleteModal().invite!)}
>
Удалить
</Button>
</div>
</div>
</Modal>
</div>
)
}
export default InvitesRoute

View File

@@ -6,7 +6,7 @@
import { Component, createEffect, createSignal, For, JSX, on, onMount, Show, untrack } from 'solid-js'
import { query } from '../graphql'
import type { Query } from '../graphql/generated/schema'
import { DELETE_TOPIC_MUTATION, UPDATE_TOPIC_MUTATION } from '../graphql/mutations'
import { CREATE_TOPIC_MUTATION, DELETE_TOPIC_MUTATION, UPDATE_TOPIC_MUTATION } from '../graphql/mutations'
import { GET_TOPICS_QUERY } from '../graphql/queries'
import TopicEditModal from '../modals/TopicEditModal'
import styles from '../styles/Table.module.css'
@@ -53,6 +53,9 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
show: false,
topic: null
})
const [createModal, setCreateModal] = createSignal<{ show: boolean }>({
show: false
})
/**
* Загружает список всех топиков
@@ -268,6 +271,40 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
}
}
/**
* Создает новый топик
*/
const createTopic = async (newTopic: Topic) => {
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: CREATE_TOPIC_MUTATION,
variables: { topic_input: newTopic }
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
if (result.data.create_topic.error) {
throw new Error(result.data.create_topic.error)
}
props.onSuccess('Топик успешно создан')
setCreateModal({ show: false })
await loadTopics() // Перезагружаем список
} catch (error) {
props.onError(`Ошибка создания топика: ${(error as Error).message}`)
}
}
/**
* Удаляет топик
*/
@@ -305,7 +342,6 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
return (
<div class={styles.container}>
<div class={styles.header}>
<h2>Управление топиками</h2>
<div style={{ display: 'flex', gap: '12px', 'align-items': 'center' }}>
<div style={{ display: 'flex', gap: '8px', 'align-items': 'center' }}>
<label style={{ 'font-size': '14px', color: '#666' }}>Сортировка:</label>
@@ -339,6 +375,9 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
<Button onClick={loadTopics} disabled={loading()}>
{loading() ? 'Загрузка...' : 'Обновить'}
</Button>
<Button variant="primary" onClick={() => setCreateModal({ show: true })}>
Создать тему
</Button>
</div>
</div>
@@ -369,6 +408,14 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
</table>
</Show>
{/* Модальное окно создания */}
<TopicEditModal
isOpen={createModal().show}
topic={null}
onClose={() => setCreateModal({ show: false })}
onSave={createTopic}
/>
{/* Модальное окно редактирования */}
<TopicEditModal
isOpen={editModal().show}