Changeset c8baad1


Ignore:
Timestamp:
02/15/26 01:58:19 (5 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
d1ee039
Parents:
ce45c7a
Message:

create playlists from sidebar

Files:
1 added
11 edited
1 moved

Legend:

Unmodified
Added
Removed
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java

    rce45c7a rc8baad1  
    44
    55import com.ukim.finki.develop.finkwave.exceptions.*;
     6import com.ukim.finki.develop.finkwave.repository.PlaylistRepository;
    67import org.slf4j.Logger;
    78import org.slf4j.LoggerFactory;
     
    6061    }
    6162
     63    @ExceptionHandler(PlaylistAlreadyExistsException.class)
     64    public ResponseEntity<Map<String, String>> handlePlaylistAlreadyExits(PlaylistAlreadyExistsException ex) {
     65        return ResponseEntity
     66                .status(HttpStatus.BAD_REQUEST)
     67                .body(Map.of("error", ex.getMessage()));
     68    }
     69
    6270
    6371
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/PlaylistController.java

    rce45c7a rc8baad1  
    4040    }
    4141
     42    @PostMapping()
     43    public HttpEntity<List<BasicPlaylistDto>>createPlaylist(@RequestParam String playlistName){
     44        playlistService.createPlaylist(playlistName);
     45        return ResponseEntity.ok(playlistService.getBasicPlaylists());
     46    }
    4247
    4348    @PostMapping("/{id}/save")
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/Playlist.java

    rce45c7a rc8baad1  
    11package com.ukim.finki.develop.finkwave.model;
    22
     3import jakarta.persistence.*;
     4import lombok.NoArgsConstructor;
    35import org.hibernate.annotations.OnDelete;
    46import org.hibernate.annotations.OnDeleteAction;
     
    68import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    79
    8 import jakarta.persistence.Column;
    9 import jakarta.persistence.Entity;
    10 import jakarta.persistence.FetchType;
    11 import jakarta.persistence.Id;
    12 import jakarta.persistence.JoinColumn;
    13 import jakarta.persistence.ManyToOne;
    14 import jakarta.persistence.Table;
    1510import lombok.Getter;
    1611import lombok.Setter;
     
    2015@Entity
    2116@Table(name = "playlists", schema = "project")
     17@NoArgsConstructor
    2218@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
    2319public class Playlist {
    2420    @Id
    2521    @Column(name = "playlist_id", nullable = false)
     22    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "playlist_seq_gen")
     23    @SequenceGenerator(name = "playlist_seq_gen", sequenceName = "project.playlists_seq", schema = "project", allocationSize = 1)
    2624    private Long id;
    2725
     
    3735    private Listener createdBy;
    3836
     37    public Playlist(String cover, String name, Listener createdBy) {
     38        this.cover = cover;
     39        this.name = name;
     40        this.createdBy = createdBy;
     41    }
    3942}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/repository/PlaylistRepository.java

    rce45c7a rc8baad1  
    1010
    1111import java.util.List;
     12import java.util.Optional;
    1213
    1314@Repository
     
    3334    """)
    3435    List<BasicPlaylistDto> getPlaylistsByIdIs(@Param("userId") Long userId);
     36
     37    @Query("""
     38                SELECT p
     39                FROM Playlist p
     40                WHERE p.name=:name AND p.createdBy.id=:currentUserId
     41           
     42            """)
     43    Optional<Playlist>getPlaylistByName(@Param("name") String name,
     44                                        @Param("currentUserId") Long currentUserId);
    3545}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/PlaylistService.java

    rce45c7a rc8baad1  
    22
    33import com.ukim.finki.develop.finkwave.exceptions.MusicalEntityNotFoundException;
     4import com.ukim.finki.develop.finkwave.exceptions.PlaylistAlreadyExistsException;
    45import com.ukim.finki.develop.finkwave.exceptions.PlaylistNotFoundException;
    56import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
     
    9394    }
    9495
     96
     97
    9598    public List<Long>getPlaylistIdsThatContainSong(Long songId){
    9699
    97100        Long currentUserId=authService.getCurrentUserIDOptional().orElse(null);
    98101        return playlistSongRepository.getPlaylistsIdsWithSong(songId,currentUserId);
     102
     103    }
     104
     105    public void createPlaylist(String playlistName){
     106        Long currentUserId=authService.getCurrentUserID();
     107
     108        Optional<Playlist>playlistOptional=playlistRepository.getPlaylistByName(playlistName,currentUserId);
     109        if (playlistOptional.isPresent()){
     110            throw new PlaylistAlreadyExistsException("You already have a playlist named "+playlistName);
     111        }
     112        Listener listener=listenerRepository.findById(currentUserId).orElseThrow(
     113                UserNotFoundException::new
     114        );
     115        playlistRepository.save(new Playlist(null,playlistName,listener));
    99116
    100117    }
  • frontend/src/components/Sidebar.tsx

    rce45c7a rc8baad1  
    4343    }
    4444  }, [user]);
    45 
    46   const handleCreatePlaylist = async (playlistName: string) => {
    47     try {
    48       await axiosInstance.post("/playlists", { name: playlistName });
    49       toast.success("Playlist created successfully!");
    50       await refreshPlaylists(false);
    51     } catch (error) {
    52       toast.error(getErrorMessage(error));
    53     }
    54   };
    5545
    5646  const isLoading = songsLoading || playlistsLoading;
     
    282272        isOpen={isModalOpen}
    283273        onClose={() => setIsModalOpen(false)}
    284         onSubmit={handleCreatePlaylist}
     274        onSuccess={() => refreshPlaylists(false)}
    285275      />
    286276    </>
  • frontend/src/components/SongItem.tsx

    rce45c7a rc8baad1  
    44import { usePlayer } from "../context/playerContext";
    55import { toEmbedUrl } from "../utils/utils";
    6 import PlaylistDropdown from "./PlaylistDropdown";
     6import PlaylistDropdown from "./playlist/PlaylistDropdown";
    77
    88export interface SongItemData {
  • frontend/src/components/playlist/CreatePlaylistModal.tsx

    rce45c7a rc8baad1  
    11import { useState, useEffect } from "react";
     2import { toast } from "react-toastify";
     3import axiosInstance from "../../api/axiosInstance";
    24import type { CreatePlaylistModalProps } from "../../utils/types";
    35
     
    57  isOpen,
    68  onClose,
    7   onSubmit,
     9  onSuccess,
    810}: CreatePlaylistModalProps) => {
    911  const [playlistName, setPlaylistName] = useState("");
    1012  const [isClosing, setIsClosing] = useState(false);
    1113  const [isOpening, setIsOpening] = useState(false);
     14  const [isSubmitting, setIsSubmitting] = useState(false);
     15  const [error, setError] = useState("");
    1216
    1317  useEffect(() => {
     
    1519      setIsClosing(false);
    1620      setPlaylistName("");
     21      setError("");
    1722      setIsOpening(true);
    1823      setTimeout(() => setIsOpening(false), 10);
     
    2833  };
    2934
    30   const handleSubmit = (e: React.FormEvent) => {
     35  const handleSubmit = async (e: React.FormEvent) => {
    3136    e.preventDefault();
    32     if (playlistName.trim()) {
    33       onSubmit(playlistName.trim());
     37    if (!playlistName.trim() || isSubmitting) return;
     38
     39    setIsSubmitting(true);
     40    try {
     41      await axiosInstance.post(
     42        `/playlists?playlistName=${encodeURIComponent(playlistName.trim())}`,
     43      );
     44      toast.success("Playlist created successfully!");
     45      onSuccess();
    3446      handleClose();
     47    } catch (err: any) {
     48      setError(err?.response?.data?.error || "Failed to create playlist");
     49    } finally {
     50      setIsSubmitting(false);
    3551    }
    3652  };
     
    88104              id="playlistName"
    89105              autoFocus
    90               className="w-full bg-[#282828] border border-white/10 rounded-lg py-3 px-4 text-white placeholder-gray-500 focus:outline-none focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954] transition-all"
     106              className={`w-full bg-[#282828] border rounded-lg py-3 px-4 text-white placeholder-gray-500 focus:outline-none transition-all ${
     107                error
     108                  ? "border-red-500 focus:border-red-500 focus:ring-1 focus:ring-red-500"
     109                  : "border-white/10 focus:border-[#1db954] focus:ring-1 focus:ring-[#1db954]"
     110              }`}
    91111              placeholder="Enter playlist name"
    92112              value={playlistName}
    93               onChange={(e) => setPlaylistName(e.target.value)}
     113              onChange={(e) => {
     114                setPlaylistName(e.target.value);
     115              }}
    94116            />
     117            <div
     118              className={`overflow-hidden transition-all duration-200 ease-out ${
     119                error ? "max-h-20 opacity-100 mt-2" : "max-h-0 opacity-0"
     120              }`}
     121            >
     122              <p className="text-sm text-red-400 flex items-center gap-1.5">
     123                <svg
     124                  className="w-4 h-4 shrink-0"
     125                  fill="currentColor"
     126                  viewBox="0 0 20 20"
     127                >
     128                  <path
     129                    fillRule="evenodd"
     130                    d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
     131                    clipRule="evenodd"
     132                  />
     133                </svg>
     134                {error}
     135              </p>
     136            </div>
    95137          </div>
    96138
     
    99141              type="button"
    100142              onClick={handleClose}
    101               className="flex-1 py-3 bg-[#282828] rounded-full text-white font-semibold hover:bg-[#3a3a3a] transition-colors cursor-pointer"
     143              disabled={isSubmitting}
     144              className="flex-1 py-3 bg-[#282828] rounded-full text-white font-semibold hover:bg-[#3a3a3a] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
    102145            >
    103146              Cancel
     
    105148            <button
    106149              type="submit"
    107               disabled={!playlistName.trim()}
    108               className="flex-1 py-3 bg-[#1db954] rounded-full text-black font-semibold hover:bg-[#1ed760] transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-[#1db954] cursor-pointer"
     150              disabled={!playlistName.trim() || isSubmitting}
     151              className="flex-1 py-3 bg-[#1db954] rounded-full text-black font-semibold hover:bg-[#1ed760] transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-[#1db954] cursor-pointer flex items-center justify-center gap-2"
    109152            >
    110               Create
     153              {isSubmitting ? (
     154                <>
     155                  <div className="w-4 h-4 border-2 border-black/20 border-t-black rounded-full animate-spin"></div>
     156                  Creating...
     157                </>
     158              ) : (
     159                "Create"
     160              )}
    111161            </button>
    112162          </div>
  • frontend/src/components/playlist/PlaylistDropdown.tsx

    rce45c7a rc8baad1  
    11import { useEffect, useRef, useState } from "react";
    22import { createPortal } from "react-dom";
    3 import axiosInstance from "../api/axiosInstance";
    4 import { useCreatedPlaylists } from "../context/playlistContext";
     3import axiosInstance from "../../api/axiosInstance";
     4import { useCreatedPlaylists } from "../../context/playlistContext";
    55
    66interface PlaylistDropdownProps {
  • frontend/src/pages/LandingPage.tsx

    rce45c7a rc8baad1  
    22import { useNavigate } from "react-router-dom";
    33import axiosInstance, { baseURL } from "../api/axiosInstance";
    4 import PlaylistDropdown from "../components/PlaylistDropdown";
     4import PlaylistDropdown from "../components/playlist/PlaylistDropdown";
    55import AlbumResult from "../components/search/AlbumResult";
    66import SongResult from "../components/search/SongResult";
  • frontend/src/pages/SongDetail.tsx

    rce45c7a rc8baad1  
    33import { toast } from "react-toastify";
    44import axiosInstance, { baseURL } from "../api/axiosInstance";
    5 import PlaylistDropdown from "../components/PlaylistDropdown";
     5import PlaylistDropdown from "../components/playlist/PlaylistDropdown";
    66import { useAuth } from "../context/authContext";
    77import { usePlayer } from "../context/playerContext";
  • frontend/src/utils/types.ts

    rce45c7a rc8baad1  
    7474  isOpen: boolean;
    7575  onClose: () => void;
    76   onSubmit: (playlistName: string) => void;
     76  onSuccess: () => void;
    7777}
    7878
Note: See TracChangeset for help on using the changeset viewer.