Line data Source code
1 : // Filepath: Melodex/melodex-front-end/src/components/UserProfile.jsx
2 0 : import React, { useEffect, useState, useRef } from 'react';
3 0 : import { useSongContext } from '../contexts/SongContext';
4 0 : import { useUserContext } from '../contexts/UserContext';
5 0 : import { useNavigate } from 'react-router-dom';
6 0 : import { Auth, Storage } from 'aws-amplify';
7 :
8 0 : function UserProfile() {
9 0 : const { rankedSongs, fetchRankedSongs } = useSongContext();
10 0 : const { userID, displayName, userPicture, setUserPicture, email, checkUser } = useUserContext();
11 0 : const [stats, setStats] = useState({});
12 0 : const [isHovered, setIsHovered] = useState(false);
13 0 : const fileInputRef = useRef(null);
14 0 : const navigate = useNavigate();
15 :
16 0 : useEffect(() => {
17 0 : const fetchStats = async () => {
18 0 : if (!userID) {
19 0 : console.log('No userID yet, skipping fetchStats');
20 0 : setStats({});
21 0 : return;
22 0 : }
23 0 : try {
24 0 : const ranked = await fetchRankedSongs({ userID, genre: 'any', subgenre: 'any' });
25 0 : console.log('Fetched ranked songs:', ranked);
26 0 : const genreStats = ranked.reduce((acc, song) => {
27 0 : const genre = song.genre || 'Unknown';
28 0 : const subgenre = song.subgenre || 'None';
29 0 : if (subgenre === 'None' || subgenre === 'any') {
30 0 : acc[genre] = (acc[genre] || 0) + 1;
31 0 : } else {
32 0 : const key = `${genre} - ${subgenre}`;
33 0 : acc[key] = (acc[key] || 0) + 1;
34 0 : }
35 0 : return acc;
36 0 : }, {});
37 0 : setStats(genreStats);
38 0 : } catch (error) {
39 0 : console.error('Failed to fetch profile stats:', error);
40 0 : setStats({});
41 0 : }
42 0 : };
43 :
44 0 : fetchStats();
45 0 : }, [userID, fetchRankedSongs]);
46 :
47 0 : const handleFileUpload = async (event) => {
48 0 : const file = event.target.files[0];
49 0 : if (!file) {
50 0 : console.log('No file selected for upload');
51 0 : return;
52 0 : }
53 :
54 0 : try {
55 0 : console.log('Starting upload for user:', userID, 'File:', file.name);
56 0 : const result = await Storage.put(`profile-pictures/${userID}-${Date.now()}.${file.name.split('.').pop()}`, file, {
57 0 : contentType: file.type,
58 0 : level: 'public',
59 0 : });
60 0 : console.log('S3 upload result:', result);
61 :
62 0 : const url = `https://songranker168d4c9071004e018de33684bf3c094ede93a-dev.s3.us-east-1.amazonaws.com/public/${result.key}`;
63 0 : console.log('Generated public URL:', url);
64 :
65 0 : const user = await Auth.currentAuthenticatedUser();
66 0 : await Auth.updateUserAttributes(user, {
67 0 : 'custom:uploadedPicture': url,
68 0 : });
69 0 : console.log('Cognito attribute updated with URL:', url);
70 :
71 0 : const refreshedUser = await Auth.currentAuthenticatedUser({ bypassCache: true });
72 0 : if (refreshedUser.attributes['custom:uploadedPicture'] !== url) {
73 0 : throw new Error('custom:uploadedPicture not persisted in Cognito');
74 0 : }
75 :
76 0 : setUserPicture(url);
77 0 : console.log('Profile picture set to:', url);
78 0 : } catch (error) {
79 0 : console.error('Failed to upload profile picture:', error.message, error.stack);
80 0 : setUserPicture('https://i.imgur.com/uPnNK9Y.png');
81 0 : alert('Upload failed: ' + error.message);
82 0 : }
83 0 : };
84 :
85 0 : const triggerFileInput = () => {
86 0 : console.log('Profile picture clicked, triggering file input');
87 0 : if (fileInputRef.current) {
88 0 : fileInputRef.current.click();
89 0 : } else {
90 0 : console.error('File input ref is not set');
91 0 : }
92 0 : };
93 :
94 0 : const handleSignOut = async () => {
95 0 : try {
96 : // Best-effort: clear Spotify auth on the backend so the next Melodex user
97 : // doesn’t inherit this browser’s Spotify session.
98 0 : try {
99 0 : const rawBase =
100 0 : import.meta.env.VITE_API_BASE_URL ??
101 0 : import.meta.env.VITE_API_BASE ??
102 0 : (typeof window !== "undefined" ? window.__API_BASE__ : null) ??
103 0 : "http://localhost:8080/api";
104 :
105 0 : const baseNoTrail = String(rawBase).replace(/\/+$/, "");
106 0 : const hasApiSuffix = /\/api$/.test(baseNoTrail);
107 0 : const authRoot = hasApiSuffix
108 0 : ? baseNoTrail.replace(/\/api$/, "")
109 0 : : baseNoTrail;
110 :
111 0 : await fetch(`${authRoot}/auth/revoke`, {
112 0 : method: "POST",
113 0 : credentials: "include",
114 0 : });
115 0 : } catch (revokeErr) {
116 0 : console.warn("Spotify revoke failed (non-blocking):", revokeErr);
117 0 : }
118 :
119 : // Now sign out of Melodex (Google/AWS Cognito)
120 0 : await Auth.signOut();
121 0 : console.log("Sign out successful");
122 :
123 0 : setUserPicture("https://i.imgur.com/uPnNK9Y.png");
124 0 : await checkUser();
125 0 : navigate("/login");
126 0 : } catch (error) {
127 0 : console.error("Sign out failed:", error);
128 0 : navigate("/login");
129 0 : }
130 0 : };
131 :
132 0 : return (
133 0 : <div style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}>
134 0 : <div
135 0 : style={{
136 0 : background: '#fff',
137 0 : borderRadius: '8px',
138 0 : padding: '1.5rem',
139 0 : boxShadow: '0 2px 6px rgba(0, 0, 0, 0.05)',
140 0 : maxWidth: '400px',
141 0 : margin: '0 auto',
142 0 : }}
143 : >
144 0 : <h2 style={{ textAlign: 'center', fontSize: '1.75rem', fontWeight: 400, marginBottom: '1.5rem', color: '#141820' }}>
145 0 : {displayName || 'User Profile'}
146 0 : </h2>
147 0 : <div style={{ display: 'flex', justifyContent: 'center', marginBottom: '2rem' }}>
148 0 : <div
149 0 : style={{
150 0 : width: '100px',
151 0 : height: '100px',
152 0 : borderRadius: '50%',
153 0 : backgroundImage: `url(${userPicture})`,
154 0 : backgroundSize: 'cover',
155 0 : backgroundPosition: 'center',
156 0 : boxShadow: '0 2px 6px rgba(0, 0, 0, 0.05)',
157 0 : position: 'relative',
158 0 : cursor: 'pointer',
159 0 : transition: 'transform 0.2s ease, opacity 0.2s ease',
160 0 : opacity: isHovered ? 0.7 : 1,
161 0 : }}
162 0 : onMouseEnter={() => setIsHovered(true)}
163 0 : onMouseLeave={() => setIsHovered(false)}
164 0 : onClick={triggerFileInput}
165 : >
166 0 : {isHovered && (
167 0 : <span
168 0 : style={{
169 0 : position: 'absolute',
170 0 : top: '50%',
171 0 : left: '50%',
172 0 : transform: 'translate(-50%, -50%)',
173 0 : color: '#fff',
174 0 : fontSize: '1rem',
175 0 : fontWeight: 500,
176 0 : backgroundColor: 'rgba(0, 0, 0, 0.6)',
177 0 : padding: '0.5rem 1rem',
178 0 : borderRadius: '4px',
179 0 : }}
180 0 : >
181 : Upload
182 0 : </span>
183 : )}
184 0 : <input
185 0 : ref={fileInputRef}
186 0 : type="file"
187 0 : accept="image/*"
188 0 : style={{ display: 'none' }}
189 0 : onChange={handleFileUpload}
190 0 : />
191 0 : </div>
192 0 : </div>
193 0 : <p style={{ textAlign: 'center', fontSize: '1.1rem', color: '#666' }}>{email}</p>
194 0 : <p style={{ textAlign: 'center', fontSize: '1.1rem', color: '#666' }}>{rankedSongs.length} ranked songs</p>
195 0 : <div style={{ marginTop: '2rem', textAlign: 'center' }}>
196 0 : <h3 style={{ fontSize: '1.5rem', fontWeight: 400, marginBottom: '1rem', color: '#141820' }}>Stats</h3>
197 0 : {Object.keys(stats).length > 0 ? (
198 0 : <ul style={{ listStyle: 'none', padding: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'left' }}>
199 0 : {Object.entries(stats).map(([key, value]) => (
200 0 : <li
201 0 : key={key}
202 0 : style={{
203 0 : marginBottom: '0.5rem',
204 0 : color: '#141820',
205 0 : fontSize: '1rem',
206 0 : background: '#fff',
207 0 : padding: '0.5rem 1rem',
208 0 : borderRadius: '6px',
209 0 : boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
210 0 : border: '1px solid #e0e0e0',
211 0 : width: '300px',
212 0 : }}
213 : >
214 0 : {key}: {value} ranked songs
215 0 : </li>
216 0 : ))}
217 0 : </ul>
218 : ) : (
219 0 : <p style={{ color: '#666', fontSize: '1rem' }}>No ranking statistics available yet.</p>
220 : )}
221 0 : </div>
222 0 : <div style={{ textAlign: 'center', marginTop: '2rem' }}>
223 0 : <button
224 0 : onClick={handleSignOut}
225 0 : style={{
226 0 : background: '#e74c3c',
227 0 : padding: '0.5rem 1rem',
228 0 : color: '#fff',
229 0 : border: 'none',
230 0 : borderRadius: '0.5rem',
231 0 : cursor: 'pointer',
232 0 : }}
233 0 : >
234 : Sign Out
235 0 : </button>
236 0 : </div>
237 0 : </div>
238 0 : </div>
239 : );
240 0 : }
241 :
242 0 : export default UserProfile;
|