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