Ignore:
Timestamp:
02/05/26 20:52:11 (5 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
2ce7c1e
Parents:
694fc25
Message:

add working server side search to landing page; change some endpoints

File:
1 edited

Legend:

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

    r694fc25 rcb4a1da  
    11import { useEffect, useState } from "react";
    22import axiosInstance from "../api/axiosInstance";
    3 import type { Song } from "../utils/types";
    4 import Nav from "./Nav";
     3import AlbumResult from "../components/search/AlbumResult";
     4import SongResult from "../components/search/SongResult";
     5import UserResult from "../components/search/UserResult";
     6import type {
     7        Album,
     8        BaseNonAdminUser,
     9        SearchCategory,
     10        Song,
     11} from "../utils/types";
     12
     13const CATEGORIES: { value: SearchCategory; label: string }[] = [
     14        { value: "songs", label: "Songs" },
     15        { value: "albums", label: "Albums" },
     16        { value: "artists", label: "Artists" },
     17        { value: "users", label: "Users" },
     18];
    519
    620const LandingPage = () => {
    721        const [songs, setSongs] = useState<Song[]>([]);
    822        const [loading, setLoading] = useState<boolean>(true);
    9         const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
    10 
    11         // mock data for recently listened songs
    12         const recentlyListened = [
    13                 { id: 1, title: "Song One", artist: "Artist A", cover: "/favicon.png" },
    14                 { id: 2, title: "Song Two", artist: "Artist B", cover: "/favicon.png" },
    15                 { id: 3, title: "Song Three", artist: "Artist C", cover: "/favicon.png" },
    16                 { id: 4, title: "Song Four", artist: "Artist D", cover: "/favicon.png" },
    17                 { id: 5, title: "Song Five", artist: "Artist E", cover: "/favicon.png" },
    18         ];
    19 
    20         // mock data for my playlists
    21         const playlists = [
    22                 { id: 1, name: "My Favorites", songCount: 25 },
    23                 { id: 2, name: "Workout Mix", songCount: 18 },
    24                 { id: 3, name: "Chill Vibes", songCount: 32 },
    25                 { id: 4, name: "Party Hits", songCount: 45 },
    26         ];
     23
     24        // search state
     25        const [searchInput, setSearchInput] = useState("");
     26        const [activeQuery, setActiveQuery] = useState("");
     27        const [searchCategory, setSearchCategory] = useState<SearchCategory>("songs");
     28        const [searchResults, setSearchResults] = useState<unknown[]>([]);
     29        const [searchLoading, setSearchLoading] = useState(false);
     30        const [hasSearched, setHasSearched] = useState(false);
    2731
    2832        useEffect(() => {
    2933                const fetchSongs = async () => {
    3034                        try {
    31                                 const response = await axiosInstance.get("/songs");
    32                                 const data = response.data;
    33                                 console.log("Fetched songs:", data);
    34                                 setSongs(data);
     35                                const response = await axiosInstance.get("/songs/top");
     36                                setSongs(response.data);
    3537                        } catch (error) {
    3638                                console.error("Error fetching songs:", error);
     
    4244        }, []);
    4345
     46        const performSearch = async (query: string, category: SearchCategory) => {
     47                if (!query.trim()) return;
     48
     49                setSearchLoading(true);
     50                setHasSearched(true);
     51                setActiveQuery(query);
     52                setSearchCategory(category);
     53
     54                try {
     55                        let endpoint = "";
     56                        switch (category) {
     57                                case "songs":
     58                                        endpoint = `/songs/search?q=${encodeURIComponent(query)}`;
     59                                        break;
     60                                case "albums":
     61                                        endpoint = `/albums/search?q=${encodeURIComponent(query)}`;
     62                                        break;
     63                                case "artists":
     64                                        endpoint = `/users/search?type=ARTIST&q=${encodeURIComponent(query)}`;
     65                                        break;
     66                                case "users":
     67                                        endpoint = `/users/search?type=LISTENER&q=${encodeURIComponent(query)}`;
     68                                        break;
     69                        }
     70
     71                        const response = await axiosInstance.get(endpoint);
     72                        setSearchResults(response.data);
     73                } catch (error) {
     74                        console.error("Search error:", error);
     75                        setSearchResults([]);
     76                } finally {
     77                        setSearchLoading(false);
     78                }
     79        };
     80
     81        const handleSearch = () => {
     82                performSearch(searchInput, searchCategory);
     83        };
     84
     85        const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     86                if (e.key === "Enter") {
     87                        handleSearch();
     88                }
     89        };
     90
     91        const handleCategorySwitch = (category: SearchCategory) => {
     92                performSearch(activeQuery, category);
     93        };
     94
     95        const clearSearch = () => {
     96                setSearchInput("");
     97                setActiveQuery("");
     98                setHasSearched(false);
     99                setSearchResults([]);
     100        };
     101
     102        const renderResults = () => {
     103                if (searchLoading) {
     104                        return (
     105                                <div className="flex justify-center py-12">
     106                                        <div className="w-10 h-10 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     107                                </div>
     108                        );
     109                }
     110
     111                if (searchResults.length === 0) {
     112                        return (
     113                                <div className="text-center py-12 text-gray-400">
     114                                        <p className="text-lg">
     115                                                No {searchCategory} found for "{activeQuery}"
     116                                        </p>
     117                                </div>
     118                        );
     119                }
     120
     121                return (
     122                        <div className="divide-y divide-white/5">
     123                                {searchResults.map((result, index) => {
     124                                        switch (searchCategory) {
     125                                                case "songs":
     126                                                        return (
     127                                                                <SongResult key={(result as Song).id} song={result as Song} />
     128                                                        );
     129                                                case "albums":
     130                                                        return (
     131                                                                <AlbumResult
     132                                                                        key={(result as Album).id}
     133                                                                        album={result as Album}
     134                                                                />
     135                                                        );
     136                                                case "artists":
     137                                                        return (
     138                                                                <UserResult
     139                                                                        key={(result as BaseNonAdminUser).username ?? index}
     140                                                                        user={result as BaseNonAdminUser}
     141                                                                        label="Artist"
     142                                                                />
     143                                                        );
     144                                                case "users":
     145                                                        return (
     146                                                                <UserResult
     147                                                                        key={(result as BaseNonAdminUser).username ?? index}
     148                                                                        user={result as BaseNonAdminUser}
     149                                                                        label="User"
     150                                                                />
     151                                                        );
     152                                        }
     153                                })}
     154                        </div>
     155                );
     156        };
     157
    44158        return (
    45                 <>
    46                         <Nav
    47                                 isSidebarOpen={isSidebarOpen}
    48                                 onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
    49                         />
    50                         <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex pt-20">
    51                                 {/* Sidebar */}
    52                                 <div
    53                                         className={`fixed left-0 top-0 h-full bg-[#121212] border-r border-white/10 transition-transform duration-300 ease-in-out z-100 ${
    54                                                 isSidebarOpen ? "translate-x-0" : "-translate-x-full"
    55                                         } w-64 overflow-y-auto`}
    56                                 >
    57                                         <div className="p-6">
    58                                                 {/* Sidebar Header */}
    59                                                 <div className="flex justify-between items-center mb-6">
    60                                                         <h2 className="text-xl font-bold text-white">Library</h2>
    61                                                         <button
    62                                                                 onClick={() => setIsSidebarOpen(false)}
    63                                                                 className="text-gray-400 hover:text-white transition-colors"
    64                                                                 aria-label="Close sidebar"
    65                                                         >
    66                                                                 <svg
    67                                                                         className="w-6 h-6"
    68                                                                         fill="none"
    69                                                                         stroke="currentColor"
    70                                                                         viewBox="0 0 24 24"
     159                <div className="min-h-screen bg-linear-to-br from-[#1e1e2e] to-[#0f0f1e] text-white flex">
     160                        <div className="flex-1">
     161                                <div className="p-8">
     162                                        <div className="max-w-7xl mx-auto">
     163                                                {/* Search Bar */}
     164                                                <div className="mb-8 flex flex-col md:flex-row gap-3">
     165                                                        <div className="flex flex-1 gap-0">
     166                                                                {/* Category Dropdown */}
     167                                                                <select
     168                                                                        value={searchCategory}
     169                                                                        onChange={(e) =>
     170                                                                                setSearchCategory(e.target.value as SearchCategory)
     171                                                                        }
     172                                                                        className="bg-[#282828] border border-white/10 border-r-0 rounded-l-full py-2 px-4 text-white text-sm focus:outline-none focus:border-[#1db954] appearance-none cursor-pointer"
    71173                                                                >
    72                                                                         <path
    73                                                                                 strokeLinecap="round"
    74                                                                                 strokeLinejoin="round"
    75                                                                                 strokeWidth={2}
    76                                                                                 d="M6 18L18 6M6 6l12 12"
    77                                                                         />
    78                                                                 </svg>
    79                                                         </button>
    80                                                 </div>
    81 
    82                                                 {/* Recently Listened */}
    83                                                 <div className="mb-8">
    84                                                         <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
    85                                                                 Recently Played
    86                                                         </h3>
    87                                                         <div className="space-y-3">
    88                                                                 {recentlyListened.map((song) => (
    89                                                                         <div
    90                                                                                 key={song.id}
    91                                                                                 className="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
     174                                                                        {CATEGORIES.map((cat) => (
     175                                                                                <option key={cat.value} value={cat.value}>
     176                                                                                        {cat.label}
     177                                                                                </option>
     178                                                                        ))}
     179                                                                </select>
     180
     181                                                                {/* Search Input */}
     182                                                                <input
     183                                                                        type="text"
     184                                                                        placeholder={`Search ${searchCategory}...`}
     185                                                                        value={searchInput}
     186                                                                        onChange={(e) => setSearchInput(e.target.value)}
     187                                                                        onKeyDown={handleKeyDown}
     188                                                                        className="flex-1 bg-[#282828] border border-white/10 border-r-0 py-2 px-4 text-white focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all"
     189                                                                />
     190
     191                                                                {/* Search Button */}
     192                                                                <button
     193                                                                        onClick={handleSearch}
     194                                                                        className="bg-[#1db954] hover:bg-[#1ed760] text-black font-medium px-6 rounded-r-full transition-colors flex items-center gap-2"
     195                                                                >
     196                                                                        <svg
     197                                                                                className="w-5 h-5"
     198                                                                                fill="none"
     199                                                                                stroke="currentColor"
     200                                                                                viewBox="0 0 24 24"
    92201                                                                        >
    93                                                                                 <img
    94                                                                                         src={song.cover}
    95                                                                                         alt={song.title}
    96                                                                                         className="w-10 h-10 rounded object-cover"
     202                                                                                <path
     203                                                                                        strokeLinecap="round"
     204                                                                                        strokeLinejoin="round"
     205                                                                                        strokeWidth={2}
     206                                                                                        d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
    97207                                                                                />
    98                                                                                 <div className="flex-1 min-w-0">
    99                                                                                         <p className="text-sm font-medium text-white truncate">
    100                                                                                                 {song.title}
    101                                                                                         </p>
    102                                                                                         <p className="text-xs text-gray-400 truncate">
    103                                                                                                 {song.artist}
    104                                                                                         </p>
    105                                                                                 </div>
    106                                                                         </div>
    107                                                                 ))}
     208                                                                        </svg>
     209                                                                        <span className="hidden sm:inline">Search</span>
     210                                                                </button>
    108211                                                        </div>
    109212                                                </div>
    110213
    111                                                 {/* Playlists */}
    112                                                 <div>
    113                                                         <h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
    114                                                                 Your Playlists
    115                                                         </h3>
    116                                                         <div className="space-y-2">
    117                                                                 {playlists.map((playlist) => (
    118                                                                         <div
    119                                                                                 key={playlist.id}
    120                                                                                 className="flex items-center justify-between p-3 rounded-lg hover:bg-white/5 cursor-pointer transition-colors"
     214                                                {/* Search Results Section */}
     215                                                {hasSearched ? (
     216                                                        <div>
     217                                                                <div className="flex items-center justify-between mb-4">
     218                                                                        <h2 className="text-2xl font-bold text-white">
     219                                                                                Results for "{activeQuery}"
     220                                                                        </h2>
     221                                                                        <button
     222                                                                                onClick={clearSearch}
     223                                                                                className="text-sm text-gray-400 hover:text-white transition-colors"
    121224                                                                        >
    122                                                                                 <div className="flex items-center gap-3">
    123                                                                                         <div className="w-10 h-10 bg-linear-to-br from-purple-500 to-pink-500 rounded flex items-center justify-center">
    124                                                                                                 <svg
    125                                                                                                         className="w-5 h-5 text-white"
    126                                                                                                         fill="currentColor"
    127                                                                                                         viewBox="0 0 20 20"
    128                                                                                                 >
    129                                                                                                         <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />
    130                                                                                                 </svg>
    131                                                                                         </div>
    132                                                                                         <div>
    133                                                                                                 <p className="text-sm font-medium text-white">
    134                                                                                                         {playlist.name}
    135                                                                                                 </p>
    136                                                                                                 <p className="text-xs text-gray-400">
    137                                                                                                         {playlist.songCount} songs
    138                                                                                                 </p>
    139                                                                                         </div>
    140                                                                                 </div>
    141                                                                         </div>
    142                                                                 ))}
    143                                                         </div>
    144                                                 </div>
    145                                         </div>
    146                                 </div>
    147 
    148                                 {/* Main Content */}
    149                                 <div
    150                                         className={`flex-1 transition-all duration-300 ${
    151                                                 isSidebarOpen ? "ml-64" : "ml-0"
    152                                         }`}
    153                                 >
    154                                         <div className="p-8">
    155                                                 {loading ? (
    156                                                         <div className="flex flex-col items-center justify-center min-h-[60vh] gap-6">
    157                                                                 <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
    158                                                                 <p className="text-xl text-gray-400">Loading songs...</p>
     225                                                                                Clear search
     226                                                                        </button>
     227                                                                </div>
     228
     229                                                                {/* Quick category switch buttons */}
     230                                                                <div className="flex gap-2 mb-6">
     231                                                                        {CATEGORIES.map((cat) => (
     232                                                                                <button
     233                                                                                        key={cat.value}
     234                                                                                        onClick={() => handleCategorySwitch(cat.value)}
     235                                                                                        className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors ${
     236                                                                                                searchCategory === cat.value
     237                                                                                                        ? "bg-[#1db954] text-black"
     238                                                                                                        : "bg-white/10 text-white hover:bg-white/20"
     239                                                                                        }`}
     240                                                                                >
     241                                                                                        {cat.label}
     242                                                                                </button>
     243                                                                        ))}
     244                                                                </div>
     245
     246                                                                {/* Results list */}
     247                                                                <div className="bg-[#1a1a2e]/50 rounded-xl overflow-hidden">
     248                                                                        {renderResults()}
     249                                                                </div>
    159250                                                        </div>
    160251                                                ) : (
    161                                                         <div className="max-w-7xl mx-auto">
    162                                                                 <div className="mb-12 pb-6 border-b border-white/10">
     252                                                        /* Default song grid */
     253                                                        <>
     254                                                                <div className="mb-8 border-b border-white/10 pb-6">
    163255                                                                        <h1 className="text-5xl font-bold mb-3 pb-1 bg-linear-to-r from-[#1db954] to-[#1ed760] bg-clip-text text-transparent">
    164256                                                                                Top Songs
     
    168260                                                                        </p>
    169261                                                                </div>
    170                                                                 <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
    171                                                                         {songs.map((song) => (
    172                                                                                 <div
    173                                                                                         key={song.id}
    174                                                                                         className="bg-[#282828] rounded-xl overflow-hidden cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group"
    175                                                                                 >
    176                                                                                         <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818]">
    177                                                                                                 <img
    178                                                                                                         src={song.cover || "/favicon.png"}
    179                                                                                                         alt={song.title}
    180                                                                                                         className="absolute top-0 left-0 w-full h-full object-cover"
    181                                                                                                         onError={(e) => {
    182                                                                                                                 (e.target as HTMLImageElement).src = "/favicon.png";
    183                                                                                                         }}
    184                                                                                                 />
    185                                                                                                 <div className="absolute top-0 left-0 w-full h-full bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
    186                                                                                                         <div className="w-15 h-15 rounded-full bg-[#1db954] flex items-center justify-center text-2xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
    187                                                                                                                 ▶
     262
     263                                                                {loading ? (
     264                                                                        <div className="flex flex-col items-center justify-center min-h-[40vh] gap-6">
     265                                                                                <div className="w-12 h-12 border-4 border-white/10 border-t-[#1db954] rounded-full animate-spin"></div>
     266                                                                                <p className="text-xl text-gray-400">Loading songs...</p>
     267                                                                        </div>
     268                                                                ) : (
     269                                                                        <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-8 py-4">
     270                                                                                {songs.map((song) => (
     271                                                                                        <div
     272                                                                                                key={song.id}
     273                                                                                                className="bg-[#282828] rounded-xl overflow-hidden cursor-pointer shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-2 hover:shadow-2xl group"
     274                                                                                        >
     275                                                                                                <div className="relative w-full pt-[100%] overflow-hidden bg-[#181818]">
     276                                                                                                        <img
     277                                                                                                                src={song.cover || "/favicon.png"}
     278                                                                                                                alt={song.title}
     279                                                                                                                className="absolute top-0 left-0 w-full h-full object-cover"
     280                                                                                                                onError={(e) => {
     281                                                                                                                        (e.target as HTMLImageElement).src =
     282                                                                                                                                "/favicon.png";
     283                                                                                                                }}
     284                                                                                                        />
     285                                                                                                        <div className="absolute top-0 left-0 w-full h-full bg-black/60 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
     286                                                                                                                <div className="w-15 h-15 rounded-full bg-[#1db954] flex items-center justify-center text-2xl text-black shadow-[0_4px_12px_rgba(29,185,84,0.5)]">
     287                                                                                                                        ▶
     288                                                                                                                </div>
    188289                                                                                                        </div>
    189290                                                                                                </div>
     291                                                                                                <div className="p-4 flex flex-col items-center">
     292                                                                                                        <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
     293                                                                                                                {song.title}
     294                                                                                                        </h3>
     295                                                                                                        <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
     296                                                                                                                {"<album>"}
     297                                                                                                        </p>
     298                                                                                                        <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
     299                                                                                                                {song.releasedBy}
     300                                                                                                        </p>
     301                                                                                                </div>
    190302                                                                                        </div>
    191                                                                                         <div className="p-4 flex flex-col items-center">
    192                                                                                                 <h3 className="text-lg font-semibold mb-2 text-white overflow-hidden text-ellipsis whitespace-nowrap">
    193                                                                                                         {song.title}
    194                                                                                                 </h3>
    195                                                                                                 <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
    196                                                                                                         {"<album>"}
    197                                                                                                 </p>
    198                                                                                                 <p className="text-sm text-gray-400 mb-3 overflow-hidden text-ellipsis whitespace-nowrap">
    199                                                                                                         {song.releasedBy}
    200                                                                                                 </p>
    201                                                                                                 {/* <div className="inline-block bg-[#1db954]/20 px-3 py-1 rounded-xl border border-[#1db954]/40">
    202                                                                                                         <span className="text-xs text-[#1db954] font-medium uppercase tracking-wider">
    203                                                                                                                 {song.genre}
    204                                                                                                         </span>
    205                                                                                                 </div> */}
    206                                                                                         </div>
    207                                                                                 </div>
    208                                                                         ))}
    209                                                                 </div>
    210                                                         </div>
     303                                                                                ))}
     304                                                                        </div>
     305                                                                )}
     306                                                        </>
    211307                                                )}
    212308                                        </div>
    213309                                </div>
    214310                        </div>
    215                 </>
     311                </div>
    216312        );
    217313};
Note: See TracChangeset for help on using the changeset viewer.