diff --git a/src/components/delete-dialog-song.tsx b/src/components/delete-dialog-song.tsx index 2143278178ba91c22f7b3c26070284383cec9844..25b0a14ac1425fecb2bb000f1d68cb665625c981 100644 --- a/src/components/delete-dialog-song.tsx +++ b/src/components/delete-dialog-song.tsx @@ -7,7 +7,7 @@ const DeleteSongDialog = () => { const handleDeleteSong = async () => { try { const response = await api.delete( - `/premium-album/${albumId}/${songId}` + `/premium-album/${albumId}/song/${songId}` ); console.log(response); navigate(`/${albumId}/songs`); diff --git a/src/components/songs-dropdown.tsx b/src/components/songs-dropdown.tsx index 13403c053a62190215845ae9fe7ae992934724c7..56b6ca2311a9b838667ce0b85da9e3cd8c385c96 100644 --- a/src/components/songs-dropdown.tsx +++ b/src/components/songs-dropdown.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { DropdownMenu, DropdownMenuTrigger, @@ -6,42 +5,22 @@ import { DropdownMenuItem, } from "./ui/dropdown-menu"; import { Button } from "./ui/button"; -import {useNavigate, useParams} from 'react-router-dom'; -export function SongDropdown({songId} : {songId : number}) { - const [dropdownOpen, setDropdownOpen] = useState(false); - const { albumId } = useParams(); +export function SongDropdown({handler} : {handler: (edit : boolean) => void}) { - console.log(dropdownOpen); - - const toggleDropdown = () => { - setDropdownOpen(prevState => !prevState); - }; - - const navigate = useNavigate(); - - const toEditSong = () => { - navigate(`/${albumId}/edit-song/${songId}`); - } - - const handleDeleteSong = () => { - console.log('Delete Song'); - setDropdownOpen(false); - navigate(`/${albumId}/delete-song/${songId}`); - }; return ( <DropdownMenu> <DropdownMenuTrigger asChild> - <Button variant="outline" onClick={toggleDropdown} className='bg-zinc-800 border-none text-white rounded h-3 pb-5'> + <Button variant="outline" className='bg-zinc-800 border-none text-white rounded h-3 pb-5'> ... </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-40 text-white bg-black border-gray-200" style={{ position: 'absolute', left: -20 }}> - <DropdownMenuItem onClick={toEditSong} className='hover:bg-zinc-800'> + <DropdownMenuItem onClick={() => handler(true)} className='hover:bg-zinc-800'> Edit Song </DropdownMenuItem> - <DropdownMenuItem onClick={handleDeleteSong}> + <DropdownMenuItem onClick={() => handler(false)}> Delete Song </DropdownMenuItem> </DropdownMenuContent> diff --git a/src/components/songs-table.tsx b/src/components/songs-table.tsx index c8176d40a3e60b84c7fe9b3a037d6a175dc3d512..2b9b01e50502f919662e435a40c1c6a520ac983e 100644 --- a/src/components/songs-table.tsx +++ b/src/components/songs-table.tsx @@ -1,27 +1,19 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"; import { SongDropdown } from "./songs-dropdown"; - -interface PremiumSong { - songId: number; - albumId: number; - title: string; - artist: string; - songNumber: number; - discNumber:number; - duration:number; - audioFilename: string; - } +import {useNavigate} from "react-router-dom"; +import {PremiumSong} from "@/types/premium-song.ts"; export function TableSongs({data}: { data: PremiumSong[] } ) { - const formatDuration = (seconds: number): string => { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; + const navigate = useNavigate(); + const formatDuration = (seconds: number): string => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; - const formattedMinutes = String(minutes).padStart(2, '0'); - const formattedSeconds = String(remainingSeconds).padStart(2, '0'); + const formattedMinutes = String(minutes).padStart(2, '0'); + const formattedSeconds = String(remainingSeconds).padStart(2, '0'); - return `${formattedMinutes}:${formattedSeconds}`; - }; + return `${formattedMinutes}:${formattedSeconds}`; + }; return ( <Table className="text-white ml-20 w-[790px] border"> @@ -40,7 +32,13 @@ export function TableSongs({data}: { data: PremiumSong[] } ) { <TableCell>{song.title}</TableCell> <TableCell>{formatDuration(song.duration)}</TableCell> <TableCell className=""> - <SongDropdown songId={song.songId}/> + <SongDropdown handler={(edit) => { + if (edit) { + navigate(`/${song.albumId}/edit-song/${song.songId}`); + } else { + navigate(`/${song.albumId}/delete-song/${song.songId}`); + } + }}/> </TableCell> </TableRow> ))} diff --git a/src/pages/AddSongPage.tsx b/src/pages/AddSongPage.tsx index 2d913c1782e088995371c03dc130229187c4f085..f68e5cda0f4bd93e706227e8c21eb18539da9f4e 100644 --- a/src/pages/AddSongPage.tsx +++ b/src/pages/AddSongPage.tsx @@ -1,39 +1,17 @@ import { useState } from "react"; import { useParams } from "react-router"; -import { object, string } from "zod"; import {useForm} from "react-hook-form"; import {zodResolver} from "@hookform/resolvers/zod"; import api from "@/api/api.ts"; import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form.tsx"; import {Input} from "@/components/ui/input.tsx"; +import {songFormSchema} from "@/validations/premium-song-form-validation.ts"; -const songSchema = object({ - title: string().min(1, "Title cannot be empty"), - artist: string().min(1, "Artist cannot be empty"), - songNumber: string().refine((str) => { - if (!str || isNaN(Number(str))) { - return false; - } - return true; - }, "Song Number must be an integer").refine((str) => (+str) >= 1, "Song Number must be greater than or equal to 1"), - discNumber: string().refine((str) => { - if (!str || isNaN(Number(str))) { - return false; - } - return true; - }, "Disc Number must be an integer").refine((str) => (+str) >= 1, "Disc Number must be greater than or equal to 1"), - duration: string().refine((str) => { - if (!str || isNaN(Number(str))) { - return false; - } - return true; - }, "Duration must be an integer").refine((str) => (+str) >= 1, "Duration must be greater than or equal to 1"), -}); const AddSong = () => { const { albumId }= useParams(); const form = useForm({ - resolver: zodResolver(songSchema), + resolver: zodResolver(songFormSchema), defaultValues: { title: "", artist: "", @@ -71,6 +49,7 @@ const AddSong = () => { console.log("Song added successfully!"); } catch (error) { console.error("Error adding Song:", error); + setAudioFile(null) } }); diff --git a/src/pages/EditSongPage.tsx b/src/pages/EditSongPage.tsx index 9d59f16021a8926f2bf999e197b56d8f6a0f3298..9b7e48d0067c17f4d1e2da78f539a1534a1065de 100644 --- a/src/pages/EditSongPage.tsx +++ b/src/pages/EditSongPage.tsx @@ -1,37 +1,73 @@ -import {ChangeEventHandler, useState} from "react"; +import {useEffect, useState} from "react"; import {useNavigate, useParams} from "react-router-dom"; -import { number, object, string } from "zod"; import api from "@/api/api.ts"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {useForm} from "react-hook-form"; +import {songFormSchema} from "@/validations/premium-song-form-validation.ts"; +import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form.tsx"; +import {Input} from "@/components/ui/input.tsx"; -const songSchema = object({ - title: string().min(1, "Title cannot be empty"), - artist: string().min(1, "Artist cannot be empty"), - songNumber: number().int("Song Number must be an integer").min(1, "Song Number must be greater than or equal to 1"), - discNumber: number().int("Disc Number must be an integer").min(1, "Disc Number must be greater than or equal to 1"), - duration: number().int("Duration must be an integer").min(1, "Duration must be greater than or equal to 1"), - audioFile: string().nullable(), -}); - - -// TODO: default values const EditSong = () => { const navigate = useNavigate(); const { albumId, songId } = useParams(); - const [songsData, setSongsData] = useState({ - title: '', - artist: '', - songNumber: '', - discNumber: '', - duration: '', - audioFile: null - }); + const [audioFile, setAudioFile] = useState<File | null>(null); + + const form = useForm({ + resolver: zodResolver(songFormSchema), + defaultValues: { + title: "", + artist: "", + songNumber: "", + discNumber: "", + duration: "", + } + }) + + useEffect(() => { + const fetchSongData = async () => { + try { + const response = await api.get( + `/premium-album/${albumId}/song/${songId}` + ); + + form.setValue("title", response.data.title); + form.setValue("artist", response.data.artist); + form.setValue("songNumber", response.data.songNumber); + form.setValue("discNumber", response.data.discNumber); + form.setValue("duration", response.data.duration); - const handleEditSong = async () => { + console.log('Song data fetched successfully!'); + } catch (error) { + console.error('Error fetching song data:', error); + } + } + + fetchSongData(); + }, [songId]); + + const handleEditSong = form.handleSubmit(async (data) => { try { - songSchema.parse(songsData); + const formData = new FormData(); + if (audioFile == null) { + alert("Please select an audio file."); + return; + } + + formData.append("title", data.title); + formData.append("artist", data.artist); + formData.append("songNumber", data.songNumber); + formData.append("discNumber", data.discNumber); + formData.append("duration", data.duration); + formData.append("audioFile", audioFile); + await api.patch( - `/premium-album/${albumId}/${songId}`, - songsData + `/premium-album/${albumId}/song/${songId}`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } ); navigate(`/${albumId}/songs`); @@ -39,102 +75,106 @@ const EditSong = () => { } catch (error) { console.error('Error editing album:', error); } - }; - - const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => { - setSongsData({ ...songsData, [e.target.id]: e.target.value }); - }; + }); return ( <div className="w-full max-w-xs ml-[450px] mt-[50px]"> - <form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> - <div className="mb-2 text-2xl font-bold"> - Edit Song - </div> - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="title"> - Title - </label> - <input - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - id="title" - type="text" - placeholder="Title" - onChange={handleChange} - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="artist"> - Artist - </label> - <input - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - id="artist" - type="text" - placeholder="Artist" - onChange={handleChange} - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="songNumber"> - Song Number - </label> - <input - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - id="songNumber" - type="text" - placeholder="Song Number" - onChange={handleChange} - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="discNumber"> - Disc Number - </label> - <input - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - id="discNumber" - type="text" - placeholder="Disc Number" - onChange={handleChange} - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="duration"> - Duration - </label> - <input - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - id="duration" - type="text" - placeholder="Duration" - onChange={handleChange} - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="audioFile"> - Audio File - </label> - <input - className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" - id="audioFile" - type="file" - accept="audio/*" - onChange={handleChange} + <Form {...form}> + <form className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={handleEditSong}> + <div className="mb-2 text-2xl font-bold"> + Edit Song + </div> + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Title</FormLabel> + <FormControl> + <Input className={"bg-white text-black placeholder:text-black"} placeholder="Title" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - - <div className="flex items-center justify-center"> - <button onClick={handleEditSong} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="button"> - Edit - </button> - </div> - </form> + + <FormField + control={form.control} + name="artist" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Artist</FormLabel> + <FormControl> + <Input className={"bg-white text-black placeholder:text-black"} placeholder="Artist" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> + + <FormField + control={form.control} + name="songNumber" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Song Number</FormLabel> + <FormControl> + <Input type="number" min="1" className={"bg-white text-black placeholder:text-black"} placeholder="Song Number" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> + + <FormField + control={form.control} + name="discNumber" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Disc Number</FormLabel> + <FormControl> + <Input type="number" min="1" className={"bg-white text-black placeholder:text-black"} placeholder="Disc Number" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> + + <FormField + control={form.control} + name="duration" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Duration</FormLabel> + <FormControl> + <Input type="number" min="1" className={"bg-white text-black placeholder:text-black"} placeholder="Duration" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> + + <div className="mb-4"> + <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="audioFile"> + Audio File + </label> + <input + className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" + id="audioFile" + type="file" + accept="audio/*" + onChange={(e) => { + const files = e.target.files; + if (files && files.length > 0) { + setAudioFile(files[0]); + } + }} + /> + </div> + + <div className="flex items-center justify-center"> + <button onClick={handleEditSong} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="button"> + Edit + </button> + </div> + </form> + </Form> </div> ); } diff --git a/src/pages/SongsPage.tsx b/src/pages/SongsPage.tsx index 01bd8f2ae36e6fdfed82aa0590e3287215335b7f..b16c2e855449f30ca0db5400aad2c4d7a81c2ca3 100644 --- a/src/pages/SongsPage.tsx +++ b/src/pages/SongsPage.tsx @@ -3,41 +3,43 @@ import cover from '../assets/images/default-cover.jpg' import { TableSongs } from '@/components/songs-table'; import { AlbumDropdown } from '@/components/album-dropdown'; import { useNavigate, useParams } from 'react-router-dom'; -import api from "@/api/api.ts"; - -interface PremiumSong { - songId: number; - albumId: number; - title: string; - artist: string; - songNumber: number; - discNumber:number; - duration:number; - audioFilename: string; -} +import api, {restUrl} from "@/api/api.ts"; +import {PremiumAlbum} from "@/types/premium-album.ts"; +import {PremiumSong} from "@/types/premium-song.ts"; const SongsPage = () => { const { albumId } = useParams<{ albumId: string }>(); + const [albumData, setAlbumData] = useState<PremiumAlbum | null>(null); const [songsData, setSongsData] = useState<PremiumSong[]>([]); const [loading, setLoading] = useState(true); - useEffect(() => { - const fetchData = async () => { - try { - const response = await api.get( - `/premium-album/${albumId}`); - console.log(response); + const fetchData = async () => { + try { + const responseAlbum = await api.get( + `/premium-album/${albumId}`, + ); + setAlbumData(responseAlbum.data.data); + + const responseSongs = await api.get( + `/premium-album/${albumId}/song`, + ); + setSongsData(() => ([ + + ...responseSongs.data, + ])); + console.log("response songs", responseSongs.data.data); - setSongsData(response.data); - setLoading(false); - } catch (error) { - console.error('Error fetching data:', error); - setLoading(true) - } - }; + setLoading(false); + } catch (error) { + console.error('Error fetching data:', error); + setLoading(true) + } + }; - fetchData(); - }, []); + useEffect(() => { + const interval = setInterval(fetchData, 1000); // 100 milliseconds + return () => clearInterval(interval); + }, [songsData]); const calculateTotalDuration = (songsArray: Array<{ duration: number }>): string => { const totalSeconds = songsArray.reduce((total, song) => total + song.duration, 0); @@ -60,22 +62,32 @@ const SongsPage = () => { const navigate = useNavigate(); + const releaseDate = new Date(albumData?.releaseDate ?? 0); + releaseDate.setHours(0, 0, 0, 0); + const formattedReleaseDate = releaseDate.toISOString().split('T')[0]; + const toAddSong = () => { navigate(`/${albumId}/add-song`); } return ( <div className='mt-2 w-800 flex flex-col items-center '> <div className='flex items-end w-[760px]'> - <img src={cover} alt="" className="w-[270px]"/> + <img src={albumData?.coverFilename ? restUrl + `/${albumData.coverFilename}` : cover} alt="" className="w-[270px]"/> <div className='text-white text-left ml-5'> - <div className='text-5xl'>Title</div> - <div>Singer</div> - <div>Year</div> + <div className='text-5xl'>{albumData?.albumName}</div> + <div>{albumData?.artist}</div> + <div>{formattedReleaseDate}</div> <div>{countSong} songs</div> <div>{totalDuration}</div> </div> <div className='absolute right-64 top-7'> - <AlbumDropdown/> + <AlbumDropdown handler={(edit) => { + if (edit) { + navigate(`/${albumId}/edit-album`); + } else { + navigate(`/${albumId}/delete-album`); + } + }}/> </div> </div> <div className='mt-[10px] mb-[80px] justify-start w-[900px]'> diff --git a/src/types/premium-song.ts b/src/types/premium-song.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0b9eb6870197485e97cb642d7d7f3a2e9be6093 --- /dev/null +++ b/src/types/premium-song.ts @@ -0,0 +1,10 @@ +export type PremiumSong = { + songId: number; + albumId: number; + title: string; + artist: string; + songNumber: number; + discNumber:number; + duration:number; + audioFilename: string; +} diff --git a/src/validations/premium-song-form-validation.ts b/src/validations/premium-song-form-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a99d092df7d8a6cb38a5e2c313cafb4dec42592 --- /dev/null +++ b/src/validations/premium-song-form-validation.ts @@ -0,0 +1,9 @@ +import {number, object, string} from "zod"; + +export const songFormSchema = object({ + title: string().min(1, "Title cannot be empty"), + artist: string().min(1, "Artist cannot be empty"), + songNumber: number().min(1, "Song Number must be greater than or equal to 1"), + discNumber: number().min(1, "Disc Number must be greater than or equal to 1"), + duration: number().min(1, "Duration must be greater than or equal to 1"), +}); diff --git a/src/validations/premium-song-validation.ts b/src/validations/premium-song-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3fce685565b213de46637842a926cb7ef6ea7f2 --- /dev/null +++ b/src/validations/premium-song-validation.ts @@ -0,0 +1,9 @@ +import {number, object, string} from "zod"; + +export const songSchema = object({ + title: string().min(1, "Title cannot be empty"), + artist: string().min(1, "Artist cannot be empty"), + songNumber: number().int("Song Number must be an integer").min(1, "Song Number must be greater than or equal to 1"), + discNumber: number().int("Disc Number must be an integer").min(1, "Disc Number must be greater than or equal to 1"), + duration: number().int("Duration must be an integer").min(1, "Duration must be greater than or equal to 1"), +});