diff --git a/src/components/album-card.tsx b/src/components/album-card.tsx index b2ebd86285d10ccf434420bd452d01c6a01a4cb1..ae6481365d2bb93f7782e7bde280a5869f6d3198 100644 --- a/src/components/album-card.tsx +++ b/src/components/album-card.tsx @@ -2,28 +2,20 @@ import React from "react"; import { Card } from "./ui/card"; import "../styles/Albums.css"; import {useNavigate} from "react-router-dom"; +import {restUrl} from "@/api/api.ts"; +import {PremiumAlbum} from "@/types/premium-album.ts"; -interface AlbumCard { - albumId: number; - albumName: string; - releaseDate: Date; - genre: string; - artist: string; - coverFilename: string; -} -const staticFileUrl = import.meta.env.VITE_REST_STATIC_URL; - -const AlbumCard: React.FC<AlbumCard> = ({ albumId, albumName, artist, coverFilename }) => { +const AlbumCard: React.FC<PremiumAlbum> = ({ albumId, albumName, artist, coverFilename }) => { const navigate = useNavigate(); return ( <Card onClick={() => navigate(`/${albumId}/songs`, {replace : true})} style={{ width: "200px", height: "250px", backgroundColor: "#D9D9D9" }} className="album-card cursor-pointer border-none transition duration-300 ease-in-out transform hover:scale-105"> <div className="image-container"> - <img src={staticFileUrl + coverFilename} alt={albumName} className="album-image" /> + <img src={restUrl + coverFilename} alt={albumName} className="album-image" /> </div> <div className="text-container text-left pl-6"> - <h3>{albumName}</h3> - <p>{artist}</p> + <h3 className="text-white">{albumName}</h3> + <p className="text-white">{artist}</p> </div> </Card> ); diff --git a/src/components/album-dropdown.tsx b/src/components/album-dropdown.tsx index 5685e1d1474ca61b22919c005acacc6075cf5d42..28b13faa4ab70bcd1e6730e6567b9206b7967994 100644 --- a/src/components/album-dropdown.tsx +++ b/src/components/album-dropdown.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { DropdownMenu, DropdownMenuTrigger, @@ -6,44 +5,21 @@ import { DropdownMenuItem, } from "./ui/dropdown-menu"; import { Button } from "./ui/button"; -import {useNavigate, useParams} from 'react-router-dom'; -export function AlbumDropdown() { - const [dropdownOpen, setDropdownOpen] = useState(false); - const { albumId } = useParams(); - - console.log(dropdownOpen); - - const toggleDropdown = () => { - setDropdownOpen(prevState => !prevState); - }; - - const navigate = useNavigate(); - - const handleEditAlbum = () => { - console.log('Edit Album'); - setDropdownOpen(false); - navigate(`/${albumId}/edit-album`); - } - - const handleDeleteAlbum = () => { - console.log('Delete Album'); - setDropdownOpen(false); - navigate(`/${albumId}/delete-album`); - }; +export function AlbumDropdown({handler} : {handler: (edit : boolean) => void}) { 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-grey-200" style={{ position: 'absolute', right: -20 }}> - <DropdownMenuItem onClick={handleEditAlbum} className='hover:bg-zinc-800'> + <DropdownMenuItem onClick={() => handler(true)} className='hover:bg-zinc-800'> Edit Album </DropdownMenuItem> - <DropdownMenuItem onClick={handleDeleteAlbum}> + <DropdownMenuItem onClick={() => handler(false)}> Delete Album </DropdownMenuItem> </DropdownMenuContent> diff --git a/src/components/edit-album-form.tsx b/src/components/edit-album-form.tsx index ab71d4df7fbb12bdfdf2f1298772c742eb6bd65b..2fdc263cbbd227f892b3cc579d3d84414c6c5cd3 100644 --- a/src/components/edit-album-form.tsx +++ b/src/components/edit-album-form.tsx @@ -1,12 +1,5 @@ import React, { useState } from 'react'; - -interface FormState { - albumName: string; - releaseDate: string; - genre: string; - artist: string; - coverFile: File | null; -} +import {FormState} from "@/types/premium-album-form.ts"; const initialFormState: FormState = { albumName: '', diff --git a/src/pages/AddAlbumPage.tsx b/src/pages/AddAlbumPage.tsx index 3ac0fcb8d364cf1147748ed18d0a0d3587956f5f..ed4c12061ca645aa8d57c0e88239a25257cbe777 100644 --- a/src/pages/AddAlbumPage.tsx +++ b/src/pages/AddAlbumPage.tsx @@ -1,125 +1,140 @@ import {useState} from "react"; -import axios from "axios"; -import { date, object, string, z } from "zod"; import api from "@/api/api.ts"; - -const albumSchema = object({ - albumName: string().min(1, "Album Name cannot be empty"), - releaseDate: date(), - genre: string().min(1, "Genre cannot be empty"), - artist: string().min(1, "Artist cannot be empty"), -}); +import {useForm} from "react-hook-form"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form.tsx"; +import {Input} from "@/components/ui/input.tsx"; +import {albumFormSchema} from "@/validations/premium-album-form-validation.ts"; export default function AddAlbum() { - const [albumName, setAlbumName] = useState(""); - const [releaseDate, setReleaseDate] = useState(""); - const [genre, setGenre] = useState(""); - const [artist, setArtist] = useState(""); - const [coverFile, setCoverFile] = useState(null); + const form = useForm({ + resolver: zodResolver(albumFormSchema), + defaultValues: { + albumName: "", + releaseDate: "", + genre: "", + artist: "", + } + }) + const [coverFile, setCoverFile] = useState<File | null>(null); - const handleAddAlbum = async () => { + const handleAddAlbum = form.handleSubmit(async (data) => { try { - const form = document.getElementById("addForm") as HTMLFormElement; - const submitter = document.getElementsByTagName("button[type=submit]")[0] as HTMLElement; - const formData = new FormData(form, submitter); - formData.append("albumName", albumName); - formData.append("releaseDate", releaseDate); - formData.append("genre", genre); - formData.append("artist", artist); - if (coverFile !== null) { - formData.append("coverFile", coverFile); + const formData = new FormData(); + if (coverFile == null) { + alert("Please select a cover file."); + return; } + formData.append("albumName", data.albumName); + const releaseDate = new Date(data.releaseDate); + releaseDate.setHours(0, 0, 0, 0); + formData.append("releaseDate", releaseDate.toISOString()); + formData.append("genre", data.genre); + formData.append("artist", data.artist); + formData.append("coverFile", coverFile); - // Validate - albumSchema.parse({ albumName, releaseDate, genre, artist }); - - await api.post("/premium-album", formData, { - headers: { + await api.post("/premium-album", + formData, + { + headers: { "Content-Type": "multipart/form-data", }, }); + form.reset(); + setCoverFile(null); console.log("Album added successfully!"); } catch (error) { console.error("Error adding album:", error); } - }; + }); return ( <div className="w-full max-w-xs ml-[450px] mt-[50px]"> - <form id="addForm" onSubmit={(e) => { - e.preventDefault(); - handleAddAlbum(); - }} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> - <div className="mb-2 text-2xl font-bold"> - Add Album - </div> - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="albumName"> - Album Name - </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="albumName" - type="text" - placeholder="Album Name" - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="releaseDate"> - Release Date - </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="releaseDate" - type="date" + <Form {...form}> + <form id="addForm"className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={handleAddAlbum}> + <div className="mb-2 text-2xl font-bold"> + Add Album + </div> + <FormField + control={form.control} + name="albumName" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Title</FormLabel> + <FormControl> + <Input className={"bg-white text-black placeholder:text-black"} placeholder="Album Name" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="genre"> - Genre - </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="genre" - type="text" - placeholder="Genre" + <FormField + control={form.control} + name="releaseDate" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Release Date</FormLabel> + <FormControl> + <Input type="date" className={"bg-white text-black placeholder:text-black"} placeholder="Release Date" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </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" + <FormField + control={form.control} + name="genre" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Genre</FormLabel> + <FormControl> + <Input className={"bg-white text-black placeholder:text-black"} placeholder="Genre" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="coverFile"> - Cover 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="coverFile" - type="file" - accept="image/*" + <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> + )} /> - </div> - <div className="flex items-center justify-center"> - <button onClick={handleAddAlbum} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit"> - Add - </button> - </div> - </form> + <div className="mb-4"> + <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="coverFile"> + Cover 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="coverFile" + type="file" + accept="image/*" + onChange={(e) => { + if (e.target.files) { + setCoverFile(e.target.files[0]); + }}} + /> + </div> + <div className="flex items-center justify-center"> + <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> + Add + </button> + </div> + </form> + </Form> </div> ); } diff --git a/src/pages/AlbumPage.tsx b/src/pages/AlbumPage.tsx index c3f356392fd282ce0d89778fbaf3dd7456991f30..b97b2ff52c29e5ddfc8b087082aebbbdef9dab01 100644 --- a/src/pages/AlbumPage.tsx +++ b/src/pages/AlbumPage.tsx @@ -4,16 +4,9 @@ import "../styles/Albums.css"; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import api from "@/api/api.ts"; +import {PremiumAlbum} from "@/types/premium-album.ts"; -interface PremiumAlbum { - albumId: number; - albumName: string; - releaseDate: Date; - genre: string; - artist: string; - coverFilename: string; -} - +// TODO : Prettier pagination const AlbumPage = () => { const [dataAlbums, setDataAlbums] = useState<PremiumAlbum[]>([]); const [loading, setLoading] = useState(true); @@ -27,7 +20,7 @@ const AlbumPage = () => { `/premium-album?page=${page}`); console.log(response); - setDataAlbums(response.data.data); + setDataAlbums(() => [...response.data.data]); setTotalPages(response.data.paging.totalPages); setLoading(false); } catch (error) { @@ -37,8 +30,9 @@ const AlbumPage = () => { }; useEffect(() => { - fetchData(currentPage); - }, [currentPage]); + const interval = setInterval(fetchData, 1000); // 100 milliseconds + return () => clearInterval(interval); + }, [dataAlbums]); const toAddAlbum = () => { navigate('/add-album', ); diff --git a/src/pages/EditAlbumPage.tsx b/src/pages/EditAlbumPage.tsx index 81b098699a77cb6892d1a88e93dda872c84d44b4..faa1f21341f48272e3986728767bfaef996a6065 100644 --- a/src/pages/EditAlbumPage.tsx +++ b/src/pages/EditAlbumPage.tsx @@ -1,25 +1,28 @@ -import {ChangeEventHandler, useEffect, useState} from 'react'; +import {useEffect, useState} from 'react'; import { useParams } from 'react-router-dom'; -import { date, object, string } from 'zod'; import api from "@/api/api.ts"; +import {useForm} from "react-hook-form"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {useNavigate} from "react-router"; +import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form.tsx"; +import {Input} from "@/components/ui/input.tsx"; +import {albumFormSchema} from "@/validations/premium-album-form-validation.ts"; -const albumSchema = object({ - albumName: string().min(1, "Album Name cannot be empty"), - releaseDate: date(), - genre: string().min(1, "Genre cannot be empty"), - artist: string().min(1, "Artist cannot be empty"), - coverFile: string().nullable(), -}); export default function EditAlbum() { + const navigate = useNavigate(); const { albumId } = useParams(); - const [albumData, setAlbumData] = useState({ - albumName: '', - releaseDate: '', - genre: '', - artist: '', - coverFile: null - }); + const [coverFile, setCoverFile] = useState<File | null>(null); + + const form = useForm({ + resolver: zodResolver(albumFormSchema), + defaultValues: { + albumName: "", + releaseDate: "", + genre: "", + artist: "", + } + }) useEffect(() => { const fetchAlbumData = async () => { @@ -27,7 +30,18 @@ export default function EditAlbum() { const response = await api.get( `/premium-album/${albumId}` ); - setAlbumData(response.data); + + form.setValue("albumName", response.data.data.albumName); + + const releaseDate = new Date(response.data.data.releaseDate); + releaseDate.setHours(0, 0, 0, 0); + const formattedReleaseDate = releaseDate.toISOString().split('T')[0]; + + form.setValue("releaseDate", formattedReleaseDate); + form.setValue("genre", response.data.data.genre); + form.setValue("artist", response.data.data.artist); + + console.log('Album data fetched successfully!'); } catch (error) { console.error('Error fetching album data:', error); } @@ -36,102 +50,124 @@ export default function EditAlbum() { fetchAlbumData(); }, [albumId]); - const handleEditAlbum = async () => { + const handleEditAlbum = form.handleSubmit( async (data) => { try { - albumSchema.parse(albumData); + const formData = new FormData(); + if (coverFile == null) { + alert("Please select a cover file."); + return; + } + formData.append("albumName", data.albumName); + const releaseDate = new Date(data.releaseDate); + releaseDate.setHours(0, 0, 0, 0); + formData.append("releaseDate", releaseDate.toISOString()); + formData.append("genre", data.genre); + formData.append("artist", data.artist); + formData.append("coverFile", coverFile); + await api.patch( `/premium-album/${albumId}`, - albumData + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } ); + navigate(`/${albumId}/songs`); console.log('Album edited successfully!'); } catch (error) { console.error('Error editing album:', error); } - }; - - const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => { - setAlbumData({ ...albumData, [e.target.id]: e.target.value }); - }; + }); return ( <div className="w-full max-w-xs ml-[450px] mt-[50px]"> - <form id="edit-form" className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> - <div className="mb-2 text-2xl font-bold"> - Edit Album - </div> - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="albumName"> - Album Name - </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="albumName" - type="text" - placeholder="Artist" - onChange={handleChange} - value={albumData.albumName} - /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="releaseDate"> - Release Date - </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="releaseDate" - type="date" - onChange={handleChange} - value={albumData.releaseDate} + <Form {...form}> + <form id="edit-form" className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={handleEditAlbum}> + <div className="mb-2 text-2xl font-bold"> + Edit Album + </div> + <FormField + control={form.control} + name="albumName" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Title</FormLabel> + <FormControl> + <Input className={"bg-white text-black placeholder:text-black"} placeholder="Album Name" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="genre"> - Genre - </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="genre" - type="text" - placeholder="Genre" - value={albumData.genre} - onChange={handleChange} + + <FormField + control={form.control} + name="releaseDate" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Release Date</FormLabel> + <FormControl> + <Input type="date" className={"bg-white text-black placeholder:text-black"} placeholder="Release Date" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </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} - value={albumData.artist} + + <FormField + control={form.control} + name="genre" + render={({ field }) => ( + <FormItem> + <FormLabel className={"text-black"}>Genre</FormLabel> + <FormControl> + <Input className={"bg-white text-black placeholder:text-black"} placeholder="Genre" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </div> - - <div className="mb-4"> - <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="coverFile"> - Cover 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="coverFile" - type="file" - accept="image/*" + + <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> + )} /> - </div> - <div className="flex items-center justify-center"> - <button onClick={handleEditAlbum} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit"> - Edit - </button> - </div> - </form> + + <div className="mb-4"> + <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="coverFile"> + Cover 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="coverFile" + type="file" + accept="image/*" + onChange={(e) => { + const files = e.target.files; + if (files && files.length > 0) { + setCoverFile(files[0]); + }}} + /> + </div> + <div className="flex items-center justify-center"> + <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"> + Edit + </button> + </div> + </form> + </Form> </div> ); } diff --git a/src/types/premium-album-form.ts b/src/types/premium-album-form.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a34a43f31857c31ce6079dcd3d6e8d3afc9021e --- /dev/null +++ b/src/types/premium-album-form.ts @@ -0,0 +1,7 @@ +export type FormState = { + albumName: string; + releaseDate: string; + genre: string; + artist: string; + coverFile: File | null; +} diff --git a/src/types/premium-album.ts b/src/types/premium-album.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6c552531f08b17fe5cb2b3ab4ccc67e044b9207 --- /dev/null +++ b/src/types/premium-album.ts @@ -0,0 +1,9 @@ +export type PremiumAlbum = { + albumId: number; + albumName: string; + releaseDate: string; + genre: string; + artist: string; + coverFilename: string; +} + diff --git a/src/validations/premium-album-form-validation.ts b/src/validations/premium-album-form-validation.ts new file mode 100644 index 0000000000000000000000000000000000000000..152ef81ea513ec259f772efd37a399cd1a6f8b45 --- /dev/null +++ b/src/validations/premium-album-form-validation.ts @@ -0,0 +1,13 @@ +import {object, string} from "zod"; + +export const albumFormSchema = object({ + albumName: string().min(1, "Album Name cannot be empty"), + releaseDate: string().refine((str) => { + if (!str || isNaN(Date.parse(str))) { + return false; + } + return true; + }, "Release Date must be a date"), + genre: string().min(1, "Genre cannot be empty"), + artist: string().min(1, "Artist cannot be empty"), +});