source: chapterx-frontend/src/pages/admin/AdminGenresPage.tsx@ 0b502c2

main
Last change on this file since 0b502c2 was 0b502c2, checked in by kikisrbinoska <srbinoskakristina07@…>, 12 days ago

Fixed user profile and reading lists

  • Property mode set to 100644
File size: 5.7 KB
Line 
1import React, { useState, useEffect } from 'react'
2import { useNavigate } from 'react-router-dom'
3import { ArrowLeft, Plus, Trash2, Tag } from 'lucide-react'
4import { useStoryStore } from '../../store/storyStore'
5import { useUIStore } from '../../store/uiStore'
6import { Button } from '../../components/ui/Button'
7import { Modal } from '../../components/ui/Modal'
8import { Genre } from '../../types'
9
10export const AdminGenresPage: React.FC = () => {
11 const navigate = useNavigate()
12 const { genres, fetchGenres, addGenre, deleteGenre, stories } = useStoryStore()
13 const publishedStories = stories.filter(s => s.status === 'published')
14 const { addToast } = useUIStore()
15 const [addOpen, setAddOpen] = useState(false)
16 const [newName, setNewName] = useState('')
17 const [deleteTarget, setDeleteTarget] = useState<Genre | null>(null)
18
19 useEffect(() => { fetchGenres() }, [])
20
21 const handleAdd = async () => {
22 if (!newName.trim()) return
23 if (genres.some(g => g.name.toLowerCase() === newName.trim().toLowerCase())) {
24 addToast('Genre already exists', 'error')
25 return
26 }
27 try {
28 await addGenre(newName.trim())
29 addToast(`Genre "${newName.trim()}" added!`)
30 setNewName('')
31 setAddOpen(false)
32 } catch {
33 addToast('Failed to add genre', 'error')
34 }
35 }
36
37 const handleDelete = async (genre: Genre) => {
38 try {
39 await deleteGenre(genre.genre_id)
40 addToast(`Genre "${genre.name}" deleted`, 'info')
41 } catch {
42 addToast('Failed to delete genre', 'error')
43 }
44 setDeleteTarget(null)
45 }
46
47 return (
48 <div className="max-w-4xl mx-auto px-4 py-8">
49 <div className="flex items-center justify-between mb-8">
50 <div className="flex items-center gap-3">
51 <button onClick={() => navigate('/admin')} className="text-slate-400 hover:text-white transition-colors">
52 <ArrowLeft size={20} />
53 </button>
54 <div>
55 <h1 className="font-serif text-2xl font-bold text-white">Manage Genres</h1>
56 <p className="text-slate-400 text-sm mt-0.5">{genres.length} genres</p>
57 </div>
58 </div>
59 <Button onClick={() => setAddOpen(true)}>
60 <Plus size={16} />
61 Add Genre
62 </Button>
63 </div>
64
65 <div className="bg-slate-900 border border-slate-700 rounded-2xl overflow-hidden">
66 <div className="flex items-center gap-2 px-6 py-4 border-b border-slate-700">
67 <Tag size={18} className="text-amber-400" />
68 <h2 className="text-white font-semibold">All Genres</h2>
69 </div>
70 <div className="divide-y divide-slate-800">
71 {genres.map(genre => (
72 <div key={genre.genre_id} className="flex items-center justify-between px-6 py-4 hover:bg-slate-800/50 transition-colors">
73 <div className="flex items-center gap-4">
74 <div className="w-2 h-2 rounded-full bg-indigo-500" />
75 <span className="text-white font-medium">{genre.name}</span>
76 </div>
77 <div className="flex items-center gap-4">
78 <span className="text-slate-500 text-sm">{publishedStories.filter(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase())).length} stories</span>
79 <button
80 onClick={() => setDeleteTarget(genre)}
81 className={`transition-colors p-1 rounded ${
82 publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase()))
83 ? 'text-slate-700 cursor-not-allowed'
84 : 'text-slate-500 hover:text-rose-400 hover:bg-rose-500/10'
85 }`}
86 disabled={publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase()))}
87 title={publishedStories.some(s => s.genres.some(g => g.toLowerCase() === genre.name.toLowerCase())) ? 'Cannot delete genre with stories' : 'Delete genre'}
88 >
89 <Trash2 size={14} />
90 </button>
91 </div>
92 </div>
93 ))}
94 </div>
95 </div>
96
97 {/* Add modal */}
98 <Modal isOpen={addOpen} onClose={() => setAddOpen(false)} title="Add Genre" size="sm">
99 <div className="space-y-4">
100 <div>
101 <label className="block text-sm text-slate-400 mb-1.5">Genre Name</label>
102 <input
103 value={newName}
104 onChange={e => setNewName(e.target.value)}
105 onKeyDown={e => e.key === 'Enter' && handleAdd()}
106 placeholder="e.g., Dystopia..."
107 className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
108 />
109 </div>
110 <div className="flex gap-3">
111 <Button variant="secondary" className="flex-1" onClick={() => setAddOpen(false)}>Cancel</Button>
112 <Button className="flex-1" onClick={handleAdd} disabled={!newName.trim()}>Add Genre</Button>
113 </div>
114 </div>
115 </Modal>
116
117 {/* Delete confirm */}
118 <Modal isOpen={!!deleteTarget} onClose={() => setDeleteTarget(null)} title="Delete Genre" size="sm">
119 {deleteTarget && (
120 <div className="space-y-4">
121 <p className="text-slate-300 text-sm">
122 Delete the genre <strong className="text-white">"{deleteTarget.name}"</strong>? This action cannot be undone.
123 </p>
124 <div className="flex gap-3">
125 <Button variant="secondary" className="flex-1" onClick={() => setDeleteTarget(null)}>Cancel</Button>
126 <Button variant="danger" className="flex-1" onClick={() => handleDelete(deleteTarget)}>Delete</Button>
127 </div>
128 </div>
129 )}
130 </Modal>
131 </div>
132 )
133}
Note: See TracBrowser for help on using the repository browser.