page.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. 'use client'
  2. import { useState, useEffect, useCallback } from 'react'
  3. import { api, ApiRequestError } from '@/lib/api'
  4. import type { Place } from '@/types'
  5. type FilterTab = 'pending_moderation' | 'published' | 'rejected' | 'revision'
  6. const FILTERS: { key: FilterTab; label: string }[] = [
  7. { key: 'pending_moderation', label: 'Новые' },
  8. { key: 'published', label: 'Одобренные недавно' },
  9. { key: 'rejected', label: 'Отклонённые' },
  10. { key: 'revision', label: 'На доработке' },
  11. ]
  12. function ReworkModal({ place, onClose, onConfirm }: { place: Place; onClose: () => void; onConfirm: (id: string, comment: string) => void }) {
  13. const [comment, setComment] = useState('')
  14. return (
  15. <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
  16. <div className="w-full max-w-md rounded-xl bg-[#1e293b] p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
  17. <h2 className="mb-2 text-lg font-bold text-white">Отправить на доработку</h2>
  18. <p className="mb-4 text-sm text-white/60">{place.title}</p>
  19. <textarea value={comment} onChange={(e) => setComment(e.target.value)}
  20. placeholder="Что нужно доработать?"
  21. rows={4}
  22. 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"
  23. />
  24. <div className="flex items-center justify-end gap-3">
  25. <button onClick={onClose}
  26. className="rounded-lg px-4 py-2 text-sm text-white/60 hover:text-white transition"
  27. >Отмена</button>
  28. <button onClick={() => onConfirm(place.id, comment)}
  29. disabled={!comment.trim()}
  30. className="rounded-lg bg-yellow-600 px-4 py-2 text-sm text-white hover:bg-yellow-500 transition disabled:opacity-50"
  31. >Отправить</button>
  32. </div>
  33. </div>
  34. </div>
  35. )
  36. }
  37. export default function AdminModerationPage() {
  38. const [filter, setFilter] = useState<FilterTab>('pending_moderation')
  39. const [places, setPlaces] = useState<Place[]>([])
  40. const [loading, setLoading] = useState(true)
  41. const [actionMsg, setActionMsg] = useState('')
  42. const [reworkPlace, setReworkPlace] = useState<Place | null>(null)
  43. const fetchPlaces = useCallback(async () => {
  44. setLoading(true)
  45. try {
  46. const res = await api.get<{ data: Place[] }>(`/places?status=${filter}&limit=50`)
  47. setPlaces(res.data)
  48. } catch (err) {
  49. if (err instanceof ApiRequestError && err.status === 403) {
  50. setActionMsg('Нет прав доступа')
  51. }
  52. } finally {
  53. setLoading(false)
  54. }
  55. }, [filter])
  56. useEffect(() => { fetchPlaces() }, [fetchPlaces])
  57. const moderate = async (id: string, action: 'approve' | 'reject' | 'rework' | 'revoke', comment?: string) => {
  58. try {
  59. await api.post(`/places/${id}/moderate`, { action, comment: comment || '' })
  60. setPlaces((prev) => prev.filter((p) => p.id !== id))
  61. setActionMsg(
  62. action === 'approve' ? 'Одобрено' :
  63. action === 'reject' ? 'Отклонено' :
  64. action === 'revoke' ? 'Отозвано' : 'Отправлено на доработку'
  65. )
  66. setReworkPlace(null)
  67. } catch (err: any) {
  68. setActionMsg(err.message || 'Ошибка')
  69. }
  70. }
  71. return (
  72. <div>
  73. <h1 className="mb-6 text-2xl font-bold text-white">Модерация</h1>
  74. {actionMsg && (
  75. <p className="mb-4 rounded-lg bg-green-500/10 px-4 py-2 text-sm text-green-400">{actionMsg}</p>
  76. )}
  77. <div className="mb-6 flex flex-wrap gap-2">
  78. {FILTERS.map((f) => (
  79. <button key={f.key} onClick={() => { setFilter(f.key); setActionMsg('') }}
  80. className={`rounded-lg px-4 py-2 text-sm transition ${
  81. filter === f.key ? 'bg-green-600 text-white' : 'bg-white/10 text-white/60 hover:bg-white/20'
  82. }`}
  83. >
  84. {f.label}
  85. </button>
  86. ))}
  87. </div>
  88. {loading ? (
  89. <p className="text-white/60">Загрузка...</p>
  90. ) : places.length === 0 ? (
  91. <p className="text-white/40">Нет мест</p>
  92. ) : (
  93. <div className="space-y-4">
  94. {places.map((place) => (
  95. <div key={place.id} className="rounded-lg border border-white/10 bg-white/5 p-4">
  96. <div className="mb-2 flex items-center gap-3">
  97. <span className={`rounded-full px-2 py-0.5 text-xs ${
  98. place.type === 'studio' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
  99. }`}>
  100. {place.type === 'studio' ? 'Студия' : 'Место'}
  101. </span>
  102. <span className="text-xs text-white/40">ID: {place.id.slice(0, 8)}</span>
  103. </div>
  104. <h3 className="mb-1 text-lg font-medium text-white">{place.title}</h3>
  105. {place.address && <p className="mb-2 text-sm text-white/40">{place.address}</p>}
  106. {place.description && <p className="mb-3 text-sm text-white/60">{place.description}</p>}
  107. {place.moderation_comment && (
  108. <p className="mb-3 rounded bg-yellow-500/10 px-3 py-2 text-sm text-yellow-400">
  109. Комментарий модератора: {place.moderation_comment}
  110. </p>
  111. )}
  112. <div className="flex items-center gap-2">
  113. {filter === 'pending_moderation' && (
  114. <>
  115. <button onClick={() => moderate(place.id, 'approve')}
  116. className="rounded-lg bg-green-600 px-4 py-2 text-sm text-white hover:bg-green-500 transition"
  117. >Одобрить</button>
  118. <button onClick={() => setReworkPlace(place)}
  119. className="rounded-lg bg-yellow-600/80 px-4 py-2 text-sm text-white hover:bg-yellow-500 transition"
  120. >На доработку</button>
  121. <button onClick={() => moderate(place.id, 'reject')}
  122. className="rounded-lg bg-red-600/80 px-4 py-2 text-sm text-white hover:bg-red-500 transition"
  123. >Отклонить</button>
  124. </>
  125. )}
  126. {filter === 'published' && (
  127. <>
  128. <button onClick={() => moderate(place.id, 'revoke')}
  129. className="rounded-lg bg-orange-600/80 px-4 py-2 text-sm text-white hover:bg-orange-500 transition"
  130. >Отозвать</button>
  131. <button onClick={() => setReworkPlace(place)}
  132. className="rounded-lg bg-yellow-600/80 px-4 py-2 text-sm text-white hover:bg-yellow-500 transition"
  133. >На доработку</button>
  134. </>
  135. )}
  136. </div>
  137. </div>
  138. ))}
  139. </div>
  140. )}
  141. {reworkPlace && (
  142. <ReworkModal
  143. place={reworkPlace}
  144. onClose={() => setReworkPlace(null)}
  145. onConfirm={(id, comment) => moderate(id, 'rework', comment)}
  146. />
  147. )}
  148. </div>
  149. )
  150. }