Changeset 1579b4f for frontend/src/pages


Ignore:
Timestamp:
02/10/26 21:58:39 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
92db381
Parents:
0808ef2
Message:

refactor artist, collection, listener components to have similar style as other components; refactor some dto-s

Location:
frontend/src/pages
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • frontend/src/pages/LandingPage.tsx

    r0808ef2 r1579b4f  
    1212        Song,
    1313} from "../utils/types";
    14 
    15 // Convert a regular YouTube URL to an embeddable URL
    16 const toEmbedUrl = (url: string): string => {
    17         try {
    18                 const parsed = new URL(url);
    19                 if (
    20                         (parsed.hostname === "www.youtube.com" ||
    21                                 parsed.hostname === "youtube.com") &&
    22                         parsed.searchParams.has("v")
    23                 ) {
    24                         return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
    25                 }
    26                 if (parsed.hostname === "youtu.be") {
    27                         return `https://www.youtube.com/embed${parsed.pathname}`;
    28                 }
    29                 return url;
    30         } catch {
    31                 return url;
    32         }
    33 };
     14import { toEmbedUrl } from "../utils/utils";
    3415
    3516const CATEGORIES: { value: SearchCategory; label: string }[] = [
  • frontend/src/pages/MusicalCollection.tsx

    r0808ef2 r1579b4f  
    11import { useEffect, useState } from "react";
    2 import { useNavigate, useParams } from "react-router-dom";
     2import { Link, useParams } from "react-router-dom";
    33import axiosInstance from "../api/axiosInstance";
    4 import LoadingSpinner from "../components/LoadingSpinner";
    5 import type { Song, Album, Playlist } from "../utils/types";
     4import SongItem from "../components/SongItem";
    65import { handleError } from "../utils/error";
     6import type { Album, Playlist, Song } from "../utils/types";
    77interface CollectionView {
    8   id: number;
    9   title: string;
    10   genre?: string;
    11   type: string;
    12   releasedBy: string;
    13   isLikedByCurrentUser?: boolean;
    14   songs: Song[];
     8        id: number;
     9        title: string;
     10        cover?: string | null;
     11        genre?: string;
     12        type: string;
     13        releasedBy: string;
     14        isLikedByCurrentUser?: boolean;
     15        songs: Song[];
    1516}
    1617
    1718const MusicalCollection = () => {
    18   const { type, id } = useParams();
    19   const navigate = useNavigate();
    20   const [collection, setCollection] = useState<CollectionView | null>(null);
    21   const [isLoading, setIsLoading] = useState(true);
    22   const [error, setError] = useState<string | null>(null);
    23 
    24   const normalizeCollection = (
    25     data: Album | Playlist,
    26     type: string,
    27   ): CollectionView => {
    28     if (type === "album") {
    29       const album = data as Album;
    30       return {
    31         id: album.id,
    32         title: album.title,
    33         genre: album.genre,
    34         type: album.type,
    35         releasedBy: album.releasedBy,
    36         isLikedByCurrentUser: album.isLikedByCurrentUser,
    37         songs: album.songs,
    38       };
    39     } else {
    40       const playlist = data as Playlist;
    41       return {
    42         id: playlist.id,
    43         title: playlist.name,
    44         genre: undefined,
    45         type: "PLAYLIST",
    46         releasedBy: playlist.creatorName,
    47         isLikedByCurrentUser: undefined,
    48         songs: playlist.songsInPlaylist,
    49       };
    50     }
    51   };
    52 
    53   useEffect(() => {
    54     const fetchData = async () => {
    55       setIsLoading(true);
    56       setError(null);
    57       try {
    58         const endpoint =
    59           type === "album" ? `/albums/${id}` : `/playlists/${id}`;
    60         const response = await axiosInstance.get(endpoint);
    61 
    62         const normalized = normalizeCollection(response.data, type!);
    63         setCollection(normalized);
    64       } catch (err: any) {
    65         setError(handleError(err));
    66       } finally {
    67         setIsLoading(false);
    68       }
    69     };
    70     fetchData();
    71   }, [id, type]);
    72 
    73   if (isLoading) {
    74     return <LoadingSpinner />;
    75   }
    76 
    77   if (error) {
    78     return (
    79       <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    80         <h2 className="font-bold">Error</h2>
    81         <p>{error}</p>
    82       </div>
    83     );
    84   }
    85 
    86   if (!collection) return null;
    87 
    88   return (
    89     <div className="container mx-auto p-6">
    90       <button
    91         onClick={() => navigate(-1)}
    92         className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
    93       >
    94         ← Back
    95       </button>
    96 
    97       <div className="bg-white shadow-lg rounded-lg p-8">
    98         <div className="flex items-start gap-6 mb-8">
    99           <div className="shrink-0">
    100             <div className="w-40 h-40 rounded-lg bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-5xl font-bold shadow-lg">
    101               {collection.title.charAt(0).toUpperCase()}
    102             </div>
    103           </div>
    104 
    105           <div className="flex-1">
    106             <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-2">
    107               {collection.type}
    108             </span>
    109             <h1 className="text-4xl font-bold mb-3">{collection.title}</h1>
    110 
    111             <div className="flex items-center gap-3 text-gray-700 mb-4">
    112               <span className="font-semibold">{collection.releasedBy}</span>
    113               {collection.genre && (
    114                 <>
    115                   <span>•</span>
    116                   <span className="text-gray-600">{collection.genre}</span>
    117                 </>
    118               )}
    119               {collection.songs && (
    120                 <>
    121                   <span>•</span>
    122                   <span className="text-gray-600">
    123                     {collection.songs.length} song
    124                     {collection.songs.length !== 1 ? "s" : ""}
    125                   </span>
    126                 </>
    127               )}
    128             </div>
    129 
    130             {type === "album" && (
    131               <button
    132                 className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors duration-200"
    133                 aria-label={collection.isLikedByCurrentUser ? "Unlike" : "Like"}
    134               >
    135                 <svg
    136                   className="w-5 h-5"
    137                   fill={collection.isLikedByCurrentUser ? "#ef4444" : "none"}
    138                   stroke={
    139                     collection.isLikedByCurrentUser ? "#ef4444" : "#6b7280"
    140                   }
    141                   strokeWidth="2"
    142                   viewBox="0 0 24 24"
    143                 >
    144                   <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
    145                 </svg>
    146                 <span className="text-sm font-medium text-gray-700">
    147                   {collection.isLikedByCurrentUser ? "Liked" : "Like"}
    148                 </span>
    149               </button>
    150             )}
    151           </div>
    152         </div>
    153 
    154         <div className="border-t pt-6">
    155           <h2 className="text-2xl font-bold mb-4 text-gray-800">Songs</h2>
    156 
    157           {collection.songs && collection.songs.length > 0 ? (
    158             <div className="space-y-2">
    159               {collection.songs.map((song, index) => (
    160                 <div
    161                   key={song.id}
    162                   className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-colors duration-150"
    163                 >
    164                   <span className="text-gray-500 font-medium w-8 text-center">
    165                     {index + 1}
    166                   </span>
    167 
    168                   <div className="flex-1 min-w-0">
    169                     <p className="font-semibold text-gray-900 truncate">
    170                       {song.title}
    171                     </p>
    172                     <p className="text-sm text-gray-600 truncate">
    173                       {song.releasedBy}
    174                     </p>
    175                   </div>
    176 
    177                   <span className="text-sm text-gray-600 px-3 py-1 bg-gray-100 rounded-full">
    178                     {song.genre}
    179                   </span>
    180 
    181                   <button
    182                     className="p-2 hover:bg-gray-100 rounded-full transition-colors duration-200"
    183                     aria-label={song.isLikedByCurrentUser ? "Unlike" : "Like"}
    184                   >
    185                     <svg
    186                       className="w-5 h-5"
    187                       fill={song.isLikedByCurrentUser ? "#ef4444" : "none"}
    188                       stroke={song.isLikedByCurrentUser ? "#ef4444" : "#6b7280"}
    189                       strokeWidth="2"
    190                       viewBox="0 0 24 24"
    191                     >
    192                       <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
    193                     </svg>
    194                   </button>
    195                 </div>
    196               ))}
    197             </div>
    198           ) : (
    199             <p className="text-center text-gray-500 py-8">No songs available</p>
    200           )}
    201         </div>
    202       </div>
    203     </div>
    204   );
     19        const { type, id } = useParams();
     20        const [collection, setCollection] = useState<CollectionView | null>(null);
     21        const [isLoading, setIsLoading] = useState(true);
     22        const [error, setError] = useState<string | null>(null);
     23
     24        const normalizeCollection = (
     25                data: Album | Playlist,
     26                type: string,
     27        ): CollectionView => {
     28                if (type === "album") {
     29                        const album = data as Album;
     30                        return {
     31                                id: album.id,
     32                                title: album.title,
     33                                cover: album.cover,
     34                                genre: album.genre,
     35                                type: album.type,
     36                                releasedBy: album.releasedBy,
     37                                isLikedByCurrentUser: album.isLikedByCurrentUser,
     38                                songs: album.songs,
     39                        };
     40                } else {
     41                        const playlist = data as Playlist;
     42                        return {
     43                                id: playlist.id,
     44                                title: playlist.name,
     45                                cover: playlist.cover,
     46                                genre: undefined,
     47                                type: "PLAYLIST",
     48                                releasedBy: playlist.creatorName,
     49                                isLikedByCurrentUser: undefined,
     50                                songs: playlist.songsInPlaylist,
     51                        };
     52                }
     53        };
     54
     55        const toggleLike = async (songId: number) => {
     56                try {
     57                        await axiosInstance.post(`/musical-entity/${songId}/like`);
     58                        setCollection((prev) => {
     59                                if (!prev) return null;
     60                                return {
     61                                        ...prev,
     62                                        songs: prev.songs.map((s) =>
     63                                                s.id === songId
     64                                                        ? { ...s, isLikedByCurrentUser: !s.isLikedByCurrentUser }
     65                                                        : s,
     66                                        ),
     67                                };
     68                        });
     69                } catch (err) {
     70                        console.error("Error toggling like:", err);
     71                }
     72        };
     73
     74        const toggleCollectionLike = async () => {
     75                if (!collection) return;
     76                try {
     77                        await axiosInstance.post(`/musical-entity/${collection.id}/like`);
     78                        setCollection((prev) => {
     79                                if (!prev) return null;
     80                                return { ...prev, isLikedByCurrentUser: !prev.isLikedByCurrentUser };
     81                        });
     82                } catch (err) {
     83                        console.error("Error toggling collection like:", err);
     84                }
     85        };
     86
     87        useEffect(() => {
     88                const fetchData = async () => {
     89                        setIsLoading(true);
     90                        setError(null);
     91                        try {
     92                                const endpoint =
     93                                        type === "album" ? `/albums/${id}` : `/playlists/${id}`;
     94                                const response = await axiosInstance.get(endpoint);
     95
     96                                const normalized = normalizeCollection(response.data, type!);
     97                                setCollection(normalized);
     98                        } catch (err: any) {
     99                                setError(handleError(err));
     100                        } finally {
     101                                setIsLoading(false);
     102                        }
     103                };
     104                fetchData();
     105        }, [id, type]);
     106
     107        if (isLoading) {
     108                return (
     109                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     110                                <div className="flex flex-col items-center gap-4">
     111                                        <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
     112                                        <p className="text-gray-400 text-lg">Loading collection…</p>
     113                                </div>
     114                        </div>
     115                );
     116        }
     117
     118        if (error) {
     119                return (
     120                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     121                                <div className="text-center">
     122                                        <p className="text-red-400 text-xl mb-4">{error}</p>
     123                                        <Link to="/" className="text-[#1db954] hover:underline text-sm">
     124                                                ← Back to Home
     125                                        </Link>
     126                                </div>
     127                        </div>
     128                );
     129        }
     130
     131        if (!collection) return null;
     132
     133        return (
     134                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
     135                        <div className="max-w-5xl mx-auto px-6 py-10">
     136                                {/* Back link */}
     137                                <Link
     138                                        to="/"
     139                                        className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     140                                >
     141                                        <svg
     142                                                className="w-4 h-4"
     143                                                fill="none"
     144                                                stroke="currentColor"
     145                                                viewBox="0 0 24 24"
     146                                        >
     147                                                <path
     148                                                        strokeLinecap="round"
     149                                                        strokeLinejoin="round"
     150                                                        strokeWidth={2}
     151                                                        d="M15 19l-7-7 7-7"
     152                                                />
     153                                        </svg>
     154                                        Back to Home
     155                                </Link>
     156
     157                                {/* Hero section */}
     158                                <div className="flex flex-col md:flex-row gap-8 mb-10">
     159                                        {/* Cover art */}
     160                                        <div className="w-full md:w-72 shrink-0">
     161                                                <div className="relative w-full pt-[100%] rounded-xl overflow-hidden shadow-2xl bg-[#181818]">
     162                                                        <img
     163                                                                src={collection.cover || "/favicon.png"}
     164                                                                alt={collection.title}
     165                                                                className="absolute inset-0 w-full h-full object-cover"
     166                                                                onError={(e) => {
     167                                                                        (e.target as HTMLImageElement).src = "/favicon.png";
     168                                                                }}
     169                                                        />
     170                                                </div>
     171                                        </div>
     172
     173                                        {/* Collection info */}
     174                                        <div className="flex flex-col justify-end gap-3 min-w-0">
     175                                                <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     176                                                        {collection.genre ? `${collection.genre} • ` : ""}
     177                                                        {collection.type === "PLAYLIST" ? "Playlist" : "Album"}
     178                                                </span>
     179                                                <h1 className="text-4xl md:text-5xl font-extrabold leading-tight truncate">
     180                                                        {collection.title}
     181                                                </h1>
     182
     183                                                <p className="text-xl text-gray-300 font-semibold">
     184                                                        {collection.releasedBy}
     185                                                </p>
     186
     187                                                {collection.songs && (
     188                                                        <p className="text-sm text-gray-500">
     189                                                                {collection.songs.length} song
     190                                                                {collection.songs.length !== 1 ? "s" : ""}
     191                                                        </p>
     192                                                )}
     193
     194                                                {/* Action buttons */}
     195                                                <div className="flex items-center gap-3 mt-4">
     196                                                        {type === "album" && (
     197                                                                <button
     198                                                                        onClick={toggleCollectionLike}
     199                                                                        className={`flex items-center gap-2 px-5 py-3 rounded-full text-sm font-semibold transition-colors cursor-pointer ${
     200                                                                                collection.isLikedByCurrentUser
     201                                                                                        ? "bg-[#1db954] text-black"
     202                                                                                        : "bg-white/10 text-white hover:bg-white/20"
     203                                                                        }`}
     204                                                                >
     205                                                                        <svg
     206                                                                                className="w-5 h-5"
     207                                                                                fill={
     208                                                                                        collection.isLikedByCurrentUser ? "currentColor" : "none"
     209                                                                                }
     210                                                                                stroke="currentColor"
     211                                                                                viewBox="0 0 24 24"
     212                                                                        >
     213                                                                                <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
     214                                                                        </svg>
     215                                                                        {collection.isLikedByCurrentUser ? "Liked" : "Like"}
     216                                                                </button>
     217                                                        )}
     218                                                </div>
     219                                        </div>
     220                                </div>
     221
     222                                {/* Songs list */}
     223                                <div className="border-t border-white/10 pt-6">
     224                                        <h2 className="text-2xl font-bold mb-4">Songs</h2>
     225
     226                                        {collection.songs && collection.songs.length > 0 ? (
     227                                                <div className="space-y-1">
     228                                                        {collection.songs.map((song, index) => (
     229                                                                <SongItem
     230                                                                        key={song.id}
     231                                                                        song={song}
     232                                                                        index={index + 1}
     233                                                                        onLikeToggle={() => toggleLike(song.id)}
     234                                                                />
     235                                                        ))}
     236                                                </div>
     237                                        ) : (
     238                                                <div className="text-center py-12 text-gray-400">
     239                                                        <p className="text-lg">No songs available</p>
     240                                                </div>
     241                                        )}
     242                                </div>
     243                        </div>
     244                </div>
     245        );
    205246};
    206247
  • frontend/src/pages/SongDetail.tsx

    r0808ef2 r1579b4f  
    11import { useEffect, useRef, useState } from "react";
    22import { Link, useParams } from "react-router-dom";
     3import { toast } from "react-toastify";
    34import axiosInstance from "../api/axiosInstance";
    45import { useAuth } from "../context/authContext";
    56import { usePlayer } from "../context/playerContext";
    67import type { SongDetail as SongDetailType } from "../utils/types";
     8import { toEmbedUrl } from "../utils/utils";
    79
    810const ROLE_LABELS: Record<string, string> = {
     
    1820const formatRole = (role: string): string => {
    1921        return ROLE_LABELS[role] || role.replace(/_/g, " ");
    20 };
    21 
    22 // convert a regular youtube URL to an embeddable URL
    23 const toEmbedUrl = (url: string): string => {
    24         try {
    25                 const parsed = new URL(url);
    26                 // youtube.com/watch?v=ID
    27                 if (
    28                         (parsed.hostname === "www.youtube.com" ||
    29                                 parsed.hostname === "youtube.com") &&
    30                         parsed.searchParams.has("v")
    31                 ) {
    32                         return `https://www.youtube.com/embed/${parsed.searchParams.get("v")}`;
    33                 }
    34                 // youtu.be/ID
    35                 if (parsed.hostname === "youtu.be") {
    36                         return `https://www.youtube.com/embed${parsed.pathname}`;
    37                 }
    38                 // already an embed URL or other provider – return as-is
    39                 return url;
    40         } catch {
    41                 return url;
    42         }
    4322};
    4423
     
    137116                        setSong(response.data);
    138117                } catch (err) {
    139                         // todo :add toast
     118                        toast.error("Failed to delete review");
    140119                        console.error("Error deleting review:", err);
    141120                }
     
    152131                        );
    153132                } catch (err) {
     133                        toast.error("Failed to toggle like");
    154134                        console.error("Error toggling like:", err);
    155135                }
  • frontend/src/pages/UserDetail.tsx

    r0808ef2 r1579b4f  
    11import { useEffect, useState } from "react";
    2 import { useNavigate, useParams } from "react-router-dom";
    3 import axiosInstance from "../api/axiosInstance";
     2import { Link, useNavigate, useParams } from "react-router-dom";
     3import axiosInstance, { baseURL } from "../api/axiosInstance";
    44import LoadingSpinner from "../components/LoadingSpinner";
    55import ArtistView from "../components/userProfile/ArtistView";
     
    3535
    3636const UserDetail = () => {
    37         // user refers to the selected user NOT to the user from context
    38         const baseURL = import.meta.env.VITE_API_BASE_URL;
    3937        const { username: usernameParam } = useParams();
    40         // sintaksava dole znaci zemi go user od auth context i preimenuvaj go vo currentUser, za da ne se izmesa so user-ot dole
    4138        const { user: currentUser } = useAuth();
    4239        const navigate = useNavigate();
     
    4946        const [isFollowing, setIsFollowing] = useState(false);
    5047
    51         // determine which username to use: URL param or current user's username
    5248        const username = usernameParam || currentUser?.username;
    53 
    54         // if we're on /me route and no user is logged in, show error
     49        const isOwnProfile = currentUser?.username === username;
     50
    5551        if (!usernameParam && !currentUser) {
    5652                return (
    57                         <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    58                                 <h2 className="font-bold">Authentication Required</h2>
    59                                 <p>You must be logged in to view your profile.</p>
    60                                 <button
    61                                         onClick={() => navigate("/login")}
    62                                         className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
    63                                 >
    64                                         Go to Login
    65                                 </button>
     53                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     54                                <div className="text-center">
     55                                        <p className="text-red-400 text-xl mb-4">
     56                                                You must be logged in to view your profile.
     57                                        </p>
     58                                        <button
     59                                                onClick={() => navigate("/login")}
     60                                                className="text-[#1db954] hover:underline text-sm cursor-pointer"
     61                                        >
     62                                                Go to Login
     63                                        </button>
     64                                </div>
    6665                        </div>
    6766                );
     
    9594                try {
    9695                        const response = await axiosInstance.post<FollowStatus>(
    97                                 `/users/na/${username}/follow`,
     96                                `/users/na/${targetUsername}/follow`,
    9897                        );
    9998
     
    105104                                ),
    106105                        );
    107 
    108                         // if (user && user.id === targetId) {
    109                         //   setUser((prev) => {
    110                         //     if (!prev) return null;
    111                         //     return {
    112                         //       ...prev,
    113                         //       isFollowedByCurrentUser: response.data.isFollowing,
    114                         //       followers: response.data.followerCount,
    115                         //     };
    116                         //   });
    117                         // }
    118106                } catch (err: any) {
    119107                        setError(handleError(err));
     
    157145                        try {
    158146                                const response = await axiosInstance.get(`/users/na/${username}`);
    159                                 console.log(response.data);
    160147                                setUser(response.data);
    161148                        } catch (err: any) {
     
    168155        if (error) {
    169156                return (
    170                         <div className="p-6 bg-red-50 border border-red-200 text-red-700 rounded-lg">
    171                                 <h2 className="font-bold">Error</h2>
    172                                 <p>{error}</p>
     157                        <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] flex items-center justify-center">
     158                                <div className="text-center">
     159                                        <p className="text-red-400 text-xl mb-4">{error}</p>
     160                                        <Link to="/" className="text-[#1db954] hover:underline text-sm">
     161                                                ← Back to Home
     162                                        </Link>
     163                                </div>
    173164                        </div>
    174165                );
     
    180171
    181172        return (
    182                 <div className="container mx-auto p-6">
     173                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white">
    183174                        {isLoadingModal && (
    184                                 <div className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm flex items-center justify-center">
    185                                         <div className="flex items-center gap-3">
    186                                                 <div className="w-6 h-6 border-3 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
    187                                         </div>
     175                                <div className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center">
     176                                        <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin" />
    188177                                </div>
    189178                        )}
    190                         <button
    191                                 onClick={() => navigate(-1)}
    192                                 className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors duration-200 cursor-pointer"
    193                         >
    194                                 ← Back
    195                         </button>
    196 
    197                         <div className="bg-white shadow-lg rounded-lg p-8">
    198                                 <div className="flex items-start gap-6 mb-8">
    199                                         <div className="shrink-0">
    200                                                 <div className="w-32 h-32 rounded-full bg-linear-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white text-4xl font-bold shadow-lg overflow-hidden">
     179
     180                        <div className="max-w-5xl mx-auto px-6 py-10">
     181                                {/* Back link */}
     182                                <Link
     183                                        to="/"
     184                                        className="inline-flex items-center gap-1 text-gray-400 hover:text-white text-sm mb-8 transition-colors"
     185                                >
     186                                        <svg
     187                                                className="w-4 h-4"
     188                                                fill="none"
     189                                                stroke="currentColor"
     190                                                viewBox="0 0 24 24"
     191                                        >
     192                                                <path
     193                                                        strokeLinecap="round"
     194                                                        strokeLinejoin="round"
     195                                                        strokeWidth={2}
     196                                                        d="M15 19l-7-7 7-7"
     197                                                />
     198                                        </svg>
     199                                        Back to Home
     200                                </Link>
     201
     202                                {/* Hero section */}
     203                                <div className="flex flex-col md:flex-row gap-8 mb-10">
     204                                        {/* Profile photo */}
     205                                        <div className="w-full md:w-48 shrink-0">
     206                                                <div className="relative w-48 h-48 rounded-full overflow-hidden shadow-2xl bg-[#181818] mx-auto md:mx-0">
    201207                                                        {user.profilePhoto ? (
    202208                                                                <img
     
    206212                                                                />
    207213                                                        ) : (
    208                                                                 user.fullName.charAt(0).toUpperCase()
     214                                                                <div className="w-full h-full bg-linear-to-br from-[#1db954] to-[#1ed760] flex items-center justify-center text-white text-5xl font-bold">
     215                                                                        {user.fullName.charAt(0).toUpperCase()}
     216                                                                </div>
    209217                                                        )}
    210218                                                </div>
    211219                                        </div>
    212220
    213                                         <div className="flex-1">
    214                                                 <h1 className="text-4xl font-bold mb-2">{user.fullName}</h1>
    215                                                 <span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium mb-4">
    216                                                         {user.userType}
     221                                        {/* User info */}
     222                                        <div className="flex flex-col justify-end gap-3 min-w-0">
     223                                                <span className="text-xs uppercase tracking-widest text-gray-400 font-medium">
     224                                                        {user.userType === "ARTIST" ? "Artist" : "Listener"} • Profile
    217225                                                </span>
    218 
    219                                                 <div className="flex gap-6 mb-4 text-gray-700">
     226                                                <h1 className="text-4xl md:text-5xl font-extrabold leading-tight">
     227                                                        {user.fullName}
     228                                                </h1>
     229                                                <p className="text-gray-400">@{user.username}</p>
     230
     231                                                {/* Stats */}
     232                                                <div className="flex items-center gap-6 mt-2">
    220233                                                        <div
    221                                                                 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
     234                                                                className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
    222235                                                                onClick={
    223236                                                                        user.userType === "LISTENER" ? displayFollowers : undefined
    224237                                                                }
    225238                                                        >
    226                                                                 <span className="text-2xl font-bold">{user.followers}</span>
    227                                                                 <span className="text-sm text-gray-500">Followers</span>
     239                                                                <span className="text-xl font-bold text-white">
     240                                                                        {user.followers}
     241                                                                </span>
     242                                                                <span className="text-sm text-gray-400 ml-1">Followers</span>
    228243                                                        </div>
    229244                                                        <div
    230                                                                 className={`flex flex-col ${user.userType == "LISTENER" ? "cursor-pointer" : "cursor-default"}`}
     245                                                                className={`${user.userType === "LISTENER" ? "cursor-pointer hover:text-white" : ""} transition-colors`}
    231246                                                                onClick={
    232247                                                                        user.userType === "LISTENER" ? displayFollowing : undefined
    233248                                                                }
    234249                                                        >
    235                                                                 <span className="text-2xl font-bold">{user.following}</span>
    236                                                                 <span className="text-sm text-gray-500">Following</span>
     250                                                                <span className="text-xl font-bold text-white">
     251                                                                        {user.following}
     252                                                                </span>
     253                                                                <span className="text-sm text-gray-400 ml-1">Following</span>
    237254                                                        </div>
    238255                                                </div>
    239256
    240                                                 <button
    241                                                         onClick={handleFollow}
    242                                                         disabled={isFollowing}
    243                                                         className={`
    244                 px-6 py-2 font-semibold rounded-lg shadow-md
    245                 transition-colors duration-200
    246                 ${
    247                                                                         isFollowing
    248                                                                                 ? "bg-gray-400 text-gray-200 cursor-not-allowed"
    249                                                                                 : user.isFollowedByCurrentUser
    250                                                                                         ? "bg-gray-200 text-gray-700 hover:bg-gray-300 cursor-pointer"
    251                                                                                         : "bg-blue-500 text-white hover:bg-blue-600 cursor-pointer"
    252                                                                 }
    253               `}
    254                                                 >
    255                                                         {user.isFollowedByCurrentUser ? "Unfollow" : "Follow"}
    256                                                 </button>
     257                                                {/* Follow button - hidden on own profile */}
     258                                                {!isOwnProfile && (
     259                                                        <div className="mt-4">
     260                                                                <button
     261                                                                        onClick={handleFollow}
     262                                                                        disabled={isFollowing}
     263                                                                        className={`flex items-center gap-2 px-6 py-3 rounded-full text-sm font-semibold transition-all cursor-pointer ${
     264                                                                                isFollowing
     265                                                                                        ? "bg-gray-700 text-gray-400 cursor-not-allowed"
     266                                                                                        : user.isFollowedByCurrentUser
     267                                                                                                ? "bg-white/10 text-white hover:bg-white/20"
     268                                                                                                : "bg-[#1db954] text-black hover:bg-[#1ed760] hover:scale-105"
     269                                                                        }`}
     270                                                                >
     271                                                                        {user.isFollowedByCurrentUser ? (
     272                                                                                <>
     273                                                                                        <svg
     274                                                                                                className="w-5 h-5"
     275                                                                                                fill="none"
     276                                                                                                stroke="currentColor"
     277                                                                                                viewBox="0 0 24 24"
     278                                                                                        >
     279                                                                                                <path
     280                                                                                                        strokeLinecap="round"
     281                                                                                                        strokeLinejoin="round"
     282                                                                                                        strokeWidth={2}
     283                                                                                                        d="M5 13l4 4L19 7"
     284                                                                                                />
     285                                                                                        </svg>
     286                                                                                        Following
     287                                                                                </>
     288                                                                        ) : (
     289                                                                                <>
     290                                                                                        <svg
     291                                                                                                className="w-5 h-5"
     292                                                                                                fill="none"
     293                                                                                                stroke="currentColor"
     294                                                                                                viewBox="0 0 24 24"
     295                                                                                        >
     296                                                                                                <path
     297                                                                                                        strokeLinecap="round"
     298                                                                                                        strokeLinejoin="round"
     299                                                                                                        strokeWidth={2}
     300                                                                                                        d="M12 4v16m8-8H4"
     301                                                                                                />
     302                                                                                        </svg>
     303                                                                                        Follow
     304                                                                                </>
     305                                                                        )}
     306                                                                </button>
     307                                                        </div>
     308                                                )}
    257309                                        </div>
    258310                                </div>
    259311
     312                                {/* Content */}
    260313                                {user.userType === "ARTIST" ? (
    261314                                        <ArtistView contributions={user.contributions} />
Note: See TracChangeset for help on using the changeset viewer.