source: chapterx-frontend/src/pages/profile/ProfilePage.tsx@ 99c1e45

main
Last change on this file since 99c1e45 was 99c1e45, checked in by kikisrbinoska <srbinoskakristina07@…>, 11 days ago

Fixed writer section and admin management

  • Property mode set to 100644
File size: 8.9 KB
Line 
1import React, { useState, useEffect } from 'react'
2import { useParams, useNavigate } from 'react-router-dom'
3import { BookOpen, Heart, Calendar, MessageCircle } from 'lucide-react'
4import { useAuthStore } from '../../store/authStore'
5import { useStoryStore } from '../../store/storyStore'
6import { useUIStore } from '../../store/uiStore'
7import { Avatar } from '../../components/ui/Avatar'
8import { RoleBadge, StatusBadge } from '../../components/ui/Badge'
9import { StoryCard } from '../../components/ui/StoryCard'
10import { GenreBadge } from '../../components/ui/Badge'
11import { Modal } from '../../components/ui/Modal'
12import { Button } from '../../components/ui/Button'
13
14type Tab = 'stories' | 'about'
15
16export const ProfilePage: React.FC = () => {
17 const { username } = useParams<{ username: string }>()
18 const navigate = useNavigate()
19 const { allUsers, currentUser, fetchAllUsers, updateUser } = useAuthStore()
20 const { stories, fetchStories } = useStoryStore()
21 const { addToast } = useUIStore()
22 const [tab, setTab] = useState<Tab>('stories')
23 const [loading, setLoading] = useState(false)
24 const [editOpen, setEditOpen] = useState(false)
25 const [editForm, setEditForm] = useState({ username: '', email: '', name: '', surname: '' })
26 const [saving, setSaving] = useState(false)
27
28 useEffect(() => {
29 fetchStories()
30 if (allUsers.length === 0) {
31 setLoading(true)
32 fetchAllUsers().finally(() => setLoading(false))
33 }
34 }, [])
35
36 const user = allUsers.find(u => u.username === username)
37
38 const openEdit = () => {
39 if (!user) return
40 setEditForm({ username: user.username, email: user.email, name: user.name, surname: user.surname })
41 setEditOpen(true)
42 }
43
44 const handleEditSave = async () => {
45 if (!user) return
46 setSaving(true)
47 try {
48 await updateUser(user.user_id, editForm)
49 addToast('Profile updated successfully!')
50 setEditOpen(false)
51 navigate(`/profile/${editForm.username}`, { replace: true })
52 } catch (err: any) {
53 addToast(err.response?.data?.message ?? 'Failed to update profile.', 'error')
54 } finally {
55 setSaving(false)
56 }
57 }
58
59 if (loading) {
60 return (
61 <div className="max-w-4xl mx-auto px-4 py-20 text-center">
62 <p className="text-slate-400">Loading profile...</p>
63 </div>
64 )
65 }
66
67 if (!user) {
68 return (
69 <div className="max-w-4xl mx-auto px-4 py-20 text-center">
70 <h2 className="text-2xl text-white mb-4">User not found</h2>
71 </div>
72 )
73 }
74
75 const userStories = stories.filter(s => s.user_id === user.user_id && s.status === 'published')
76 const totalLikes = userStories.reduce((acc, s) => acc + s.total_likes, 0)
77 const totalComments = userStories.reduce((acc, s) => acc + s.total_comments, 0)
78 const allGenres = [...new Set(userStories.flatMap(s => s.genres))]
79
80 return (
81 <div className="max-w-5xl mx-auto px-4 py-8">
82 {/* Profile header */}
83 <div className="relative mb-8">
84 {/* Cover */}
85 <div className="h-32 sm:h-48 rounded-2xl bg-gradient-to-br from-indigo-900 via-violet-900 to-purple-900 mb-16 sm:mb-20 overflow-hidden">
86 <div className="absolute inset-0 rounded-2xl opacity-40">
87 <div className="absolute top-4 left-16 w-32 h-32 rounded-full bg-white/10 blur-2xl" />
88 <div className="absolute bottom-2 right-8 w-24 h-24 rounded-full bg-white/10 blur-xl" />
89 </div>
90 </div>
91
92 {/* Avatar + info */}
93 <div className="absolute bottom-0 left-0 right-0 px-6 flex items-end justify-between">
94 <Avatar name={`${user.name} ${user.surname}`} size="xl" className="ring-4 ring-slate-950" />
95 {currentUser?.user_id === user.user_id && (
96 <button onClick={openEdit} className="mb-2 text-sm text-indigo-400 hover:text-indigo-300 transition-colors">
97 Edit Profile
98 </button>
99 )}
100 </div>
101 </div>
102
103 {/* User info */}
104 <div className="mb-8">
105 <div className="flex items-center gap-3 mb-1">
106 <h1 className="font-serif text-2xl font-bold text-white">
107 {user.name} {user.surname}
108 </h1>
109 <RoleBadge role={user.role} />
110 </div>
111 <p className="text-slate-400">@{user.username}</p>
112 {user.bio && <p className="text-slate-300 mt-3 max-w-xl">{user.bio}</p>}
113 <div className="flex items-center gap-4 mt-4 flex-wrap">
114 <span className="flex items-center gap-1 text-slate-400 text-sm">
115 <Calendar size={14} />
116 Joined {new Date(user.created_at).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
117 </span>
118 </div>
119 </div>
120
121 {/* Stats */}
122 <div className="grid grid-cols-3 gap-4 mb-8">
123 {[
124 { icon: <BookOpen size={16} className="text-indigo-400" />, value: userStories.length, label: 'Stories' },
125 { icon: <Heart size={16} className="text-rose-400" />, value: totalLikes.toLocaleString(), label: 'Likes' },
126 { icon: <MessageCircle size={16} className="text-amber-400" />, value: totalComments, label: 'Comments' },
127 ].map(s => (
128 <div key={s.label} className="bg-slate-800 border border-slate-700 rounded-xl p-4 text-center">
129 <div className="flex justify-center mb-1">{s.icon}</div>
130 <p className="text-xl font-bold text-white">{s.value}</p>
131 <p className="text-slate-500 text-xs">{s.label}</p>
132 </div>
133 ))}
134 </div>
135
136 {/* Tabs */}
137 <div className="flex gap-1 p-1 bg-slate-800 rounded-xl mb-6 w-fit">
138 {(['stories', 'about'] as Tab[]).map(t => (
139 <button
140 key={t}
141 onClick={() => setTab(t)}
142 className={`px-4 py-2 rounded-lg text-sm font-medium capitalize transition-colors ${
143 tab === t ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white'
144 }`}
145 >
146 {t}
147 </button>
148 ))}
149 </div>
150
151 {/* Tab content */}
152 {tab === 'stories' ? (
153 <div>
154 {userStories.length === 0 ? (
155 <div className="text-center py-16 text-slate-500">
156 <BookOpen size={40} className="mx-auto mb-3 opacity-40" />
157 <p>No published stories yet.</p>
158 </div>
159 ) : (
160 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
161 {userStories.map(story => (
162 <StoryCard key={story.story_id} story={story} />
163 ))}
164 </div>
165 )}
166 </div>
167 ) : (
168 <div className="space-y-6">
169 {allGenres.length > 0 && (
170 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-6">
171 <h3 className="text-white font-semibold mb-3">Writes In</h3>
172 <div className="flex flex-wrap gap-2">
173 {allGenres.map(g => <GenreBadge key={g} genre={g} />)}
174 </div>
175 </div>
176 )}
177 <div className="bg-slate-800 border border-slate-700 rounded-2xl p-6">
178 <h3 className="text-white font-semibold mb-3">Stats</h3>
179 <div className="space-y-3">
180 <div className="flex justify-between text-sm">
181 <span className="text-slate-400">Role</span>
182 <RoleBadge role={user.role} />
183 </div>
184 <div className="flex justify-between text-sm">
185 <span className="text-slate-400">Member since</span>
186 <span className="text-white">{new Date(user.created_at).toLocaleDateString()}</span>
187 </div>
188 <div className="flex justify-between text-sm">
189 <span className="text-slate-400">Published stories</span>
190 <span className="text-white">{userStories.length}</span>
191 </div>
192 </div>
193 </div>
194 </div>
195 )}
196
197 <Modal isOpen={editOpen} onClose={() => setEditOpen(false)} title="Edit Profile">
198 <div className="space-y-4">
199 {[
200 { label: 'First Name', key: 'name' },
201 { label: 'Last Name', key: 'surname' },
202 { label: 'Username', key: 'username' },
203 { label: 'Email', key: 'email' },
204 ].map(({ label, key }) => (
205 <div key={key}>
206 <label className="block text-sm text-slate-400 mb-1.5">{label}</label>
207 <input
208 type={key === 'email' ? 'email' : 'text'}
209 value={editForm[key as keyof typeof editForm]}
210 onChange={e => setEditForm(p => ({ ...p, [key]: e.target.value }))}
211 className="w-full px-4 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
212 />
213 </div>
214 ))}
215 <div className="flex gap-3 pt-2">
216 <Button variant="ghost" className="flex-1" onClick={() => setEditOpen(false)}>Cancel</Button>
217 <Button className="flex-1" loading={saving} onClick={handleEditSave}>Save Changes</Button>
218 </div>
219 </div>
220 </Modal>
221 </div>
222 )
223}
Note: See TracBrowser for help on using the repository browser.