LCOV - code coverage report
Current view: top level - src/components/UserProfile.jsx - UserProfile.jsx (source / functions) Hit Total Coverage
Test: changed-lcov.info Lines: 0 212 0.0 %
Date: 2025-11-22 00:01:26 Functions: 1 1 100.0 %

          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;

Generated by: LCOV version 1.15.alpha0w