| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- 'use client'
- import { useState, useEffect, useCallback } from 'react'
- import { api, ApiRequestError } from '@/lib/api'
- import type { Place } from '@/types'
- type FilterTab = 'pending_moderation' | 'published' | 'rejected' | 'revision' | 'deleted'
- const FILTERS: { key: FilterTab; label: string }[] = [
- { key: 'pending_moderation', label: 'Новые' },
- { key: 'published', label: 'Одобренные недавно' },
- { key: 'rejected', label: 'Отклонённые' },
- { key: 'revision', label: 'На доработке' },
- { key: 'deleted', label: 'Удалённые' },
- ]
- function ReworkModal({ place, onClose, onConfirm }: { place: Place; onClose: () => void; onConfirm: (id: string, comment: string) => void }) {
- const [comment, setComment] = useState('')
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
- <div className="w-full max-w-md rounded-xl bg-[#1e293b] p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
- <h2 className="mb-2 text-lg font-bold text-white">Отправить на доработку</h2>
- <p className="mb-4 text-sm text-white/60">{place.title}</p>
- <textarea value={comment} onChange={(e) => setComment(e.target.value)}
- placeholder="Что нужно доработать?"
- rows={4}
- className="mb-4 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-sm text-white outline-none focus:border-yellow-500"
- />
- <div className="flex items-center justify-end gap-3">
- <button onClick={onClose}
- className="rounded-lg px-4 py-2 text-sm text-white/60 hover:text-white transition"
- >Отмена</button>
- <button onClick={() => onConfirm(place.id, comment)}
- disabled={!comment.trim()}
- className="rounded-lg bg-yellow-600 px-4 py-2 text-sm text-white hover:bg-yellow-500 transition disabled:opacity-50"
- >Отправить</button>
- </div>
- </div>
- </div>
- )
- }
- export default function AdminModerationPage() {
- const [filter, setFilter] = useState<FilterTab>('pending_moderation')
- const [places, setPlaces] = useState<Place[]>([])
- const [loading, setLoading] = useState(true)
- const [actionMsg, setActionMsg] = useState('')
- const [reworkPlace, setReworkPlace] = useState<Place | null>(null)
- const [hardDeletePlace, setHardDeletePlace] = useState<Place | null>(null)
- const fetchPlaces = useCallback(async () => {
- setLoading(true)
- try {
- const res = await api.get<{ data: Place[] }>(`/places?status=${filter}&limit=50`)
- setPlaces(res.data)
- } catch (err) {
- if (err instanceof ApiRequestError && err.status === 403) {
- setActionMsg('Нет прав доступа')
- }
- } finally {
- setLoading(false)
- }
- }, [filter])
- useEffect(() => { fetchPlaces() }, [fetchPlaces])
- const moderate = async (id: string, action: 'approve' | 'reject' | 'rework' | 'revoke', comment?: string) => {
- try {
- await api.post(`/places/${id}/moderate`, { action, comment: comment || '' })
- setPlaces((prev) => prev.filter((p) => p.id !== id))
- setActionMsg(
- action === 'approve' ? 'Одобрено' :
- action === 'reject' ? 'Отклонено' :
- action === 'revoke' ? 'Отозвано' : 'Отправлено на доработку'
- )
- setReworkPlace(null)
- } catch (err: any) {
- setActionMsg(err.message || 'Ошибка')
- }
- }
- const hardDelete = async (id: string) => {
- try {
- await api.delete(`/admin/places/${id}`)
- setPlaces((prev) => prev.filter((p) => p.id !== id))
- setActionMsg('Место полностью удалено')
- } catch (err: any) {
- setActionMsg(err.message || 'Ошибка при удалении')
- }
- }
- const restore = async (id: string) => {
- try {
- await api.post(`/places/${id}/restore`, {})
- setPlaces((prev) => prev.filter((p) => p.id !== id))
- setActionMsg('Место восстановлено')
- } catch (err: any) {
- setActionMsg(err.message || 'Ошибка при восстановлении')
- }
- }
- return (
- <div>
- <h1 className="mb-6 text-2xl font-bold text-white">Модерация</h1>
- {actionMsg && (
- <p className="mb-4 rounded-lg bg-green-500/10 px-4 py-2 text-sm text-green-400">{actionMsg}</p>
- )}
- <div className="mb-6 flex flex-wrap gap-2">
- {FILTERS.map((f) => (
- <button key={f.key} onClick={() => { setFilter(f.key); setActionMsg('') }}
- className={`rounded-lg px-4 py-2 text-sm transition ${
- filter === f.key ? 'bg-green-600 text-white' : 'bg-white/10 text-white/60 hover:bg-white/20'
- }`}
- >
- {f.label}
- </button>
- ))}
- </div>
- {loading ? (
- <p className="text-white/60">Загрузка...</p>
- ) : places.length === 0 ? (
- <p className="text-white/40">Нет мест</p>
- ) : (
- <div className="space-y-4">
- {places.map((place) => (
- <div key={place.id} className="rounded-lg border border-white/10 bg-white/5 p-4">
- <div className="mb-2 flex items-center gap-3">
- <span className={`rounded-full px-2 py-0.5 text-xs ${
- place.type === 'studio' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
- }`}>
- {place.type === 'studio' ? 'Студия' : 'Место'}
- </span>
- <span className="text-xs text-white/40">ID: {place.id.slice(0, 8)}</span>
- </div>
- <h3 className="mb-1 text-lg font-medium text-white">{place.title}</h3>
- {place.address && <p className="mb-2 text-sm text-white/40">{place.address}</p>}
- {place.description && <p className="mb-3 text-sm text-white/60">{place.description}</p>}
- {place.moderation_comment && (
- <p className="mb-3 rounded bg-yellow-500/10 px-3 py-2 text-sm text-yellow-400">
- Комментарий модератора: {place.moderation_comment}
- </p>
- )}
- <div className="flex items-center gap-2">
- {filter === 'pending_moderation' && (
- <>
- <button onClick={() => moderate(place.id, 'approve')}
- className="rounded-lg bg-green-600 px-4 py-2 text-sm text-white hover:bg-green-500 transition"
- >Одобрить</button>
- <button onClick={() => setReworkPlace(place)}
- className="rounded-lg bg-yellow-600/80 px-4 py-2 text-sm text-white hover:bg-yellow-500 transition"
- >На доработку</button>
- <button onClick={() => moderate(place.id, 'reject')}
- className="rounded-lg bg-red-600/80 px-4 py-2 text-sm text-white hover:bg-red-500 transition"
- >Отклонить</button>
- </>
- )}
- {filter === 'published' && (
- <>
- <button onClick={() => moderate(place.id, 'revoke')}
- className="rounded-lg bg-orange-600/80 px-4 py-2 text-sm text-white hover:bg-orange-500 transition"
- >Отозвать</button>
- <button onClick={() => setReworkPlace(place)}
- className="rounded-lg bg-yellow-600/80 px-4 py-2 text-sm text-white hover:bg-yellow-500 transition"
- >На доработку</button>
- </>
- )}
- {filter === 'deleted' && (
- <>
- <button onClick={() => restore(place.id)}
- className="rounded-lg bg-green-700 px-4 py-2 text-sm text-white hover:bg-green-600 transition"
- >Восстановить</button>
- <button onClick={() => setHardDeletePlace(place)}
- className="rounded-lg bg-red-700 px-4 py-2 text-sm text-white hover:bg-red-600 transition"
- >Полностью удалить</button>
- </>
- )}
- </div>
- </div>
- ))}
- </div>
- )}
- {reworkPlace && (
- <ReworkModal
- place={reworkPlace}
- onClose={() => setReworkPlace(null)}
- onConfirm={(id, comment) => moderate(id, 'rework', comment)}
- />
- )}
- {hardDeletePlace && (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={() => setHardDeletePlace(null)}>
- <div className="w-full max-w-sm rounded-xl bg-[#1e293b] p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
- <h3 className="mb-2 text-lg font-bold text-white">Полное удаление</h3>
- <p className="mb-2 text-sm text-white/60">{hardDeletePlace.title}</p>
- <p className="mb-4 text-sm text-red-400">Место и все его файлы будут безвозвратно удалены.</p>
- <div className="flex justify-end gap-3">
- <button onClick={() => setHardDeletePlace(null)}
- className="rounded-lg bg-white/10 px-4 py-2 text-sm text-white hover:bg-white/20 transition"
- >Отмена</button>
- <button onClick={() => { hardDelete(hardDeletePlace.id); setHardDeletePlace(null) }}
- className="rounded-lg bg-red-700 px-4 py-2 text-sm text-white hover:bg-red-600 transition"
- >Удалить навсегда</button>
- </div>
- </div>
- </div>
- )}
- </div>
- )
- }
|