diff --git a/src/TonalityApp.tsx b/src/TonalityApp.tsx index 610f888fdb39d29a18ce0f2d4fa08f6622cb5ca4..8f7ecbabbe42718d8cd7e9c1e34fb729a9ef82bd 100644 --- a/src/TonalityApp.tsx +++ b/src/TonalityApp.tsx @@ -2,20 +2,20 @@ import "./App.css"; import React from "react"; import { RenderRoutes } from "@/routes/RenderRoutes.tsx"; import { routes } from "@/routes/routes.ts"; -import AuthProvider from "@/context/AuthProvider.tsx"; -export const AuthContext = React.createContext(null); +export type UserContext = {token: string | null, onLogin: (accessToken: string) => void, onLogout: () => void} +export const AuthContext = React.createContext< + UserContext>(null as unknown as UserContext); export const useAuth = () => React.useContext(AuthContext); +// @ts-expect-error error bg export const Routes: React.ReactNode = RenderRoutes(routes); const TonalityApp = () => { return ( - <AuthProvider> - {/*@ts-ignore*/} + // @ts-expect-error error bg <Routes /> - </AuthProvider> ); }; diff --git a/src/components/album-card.tsx b/src/components/album-card.tsx index 3d6986f0142d281493d24043b1c92a8ef43407f7..b2ebd86285d10ccf434420bd452d01c6a01a4cb1 100644 --- a/src/components/album-card.tsx +++ b/src/components/album-card.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Card } from "./ui/card"; import "../styles/Albums.css"; -import { useNavigate } from "react-router-dom"; +import {useNavigate} from "react-router-dom"; interface AlbumCard { albumId: number; @@ -12,22 +12,20 @@ interface AlbumCard { coverFilename: string; } -const AlbumCard: React.FC<AlbumCard> = ({ albumName, artist, coverFilename }) => { - const navigate = useNavigate(); +const staticFileUrl = import.meta.env.VITE_REST_STATIC_URL; - const toDetail = () => { - navigate('/1/songs'); - } +const AlbumCard: React.FC<AlbumCard> = ({ albumId, albumName, artist, coverFilename }) => { + const navigate = useNavigate(); return ( - <Card onClick={toDetail} style={{ width: "240px", height: "300px", 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={coverFilename} alt={albumName} className="album-image" /> - </div> - <div className="text-container text-left pl-6"> - <h3>{albumName}</h3> - <p>{artist}</p> - </div> - </Card> + <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" /> + </div> + <div className="text-container text-left pl-6"> + <h3>{albumName}</h3> + <p>{artist}</p> + </div> + </Card> ); }; diff --git a/src/components/songs-table.tsx b/src/components/songs-table.tsx index 863c523b685cc0166feb1c0ed0ab70526c670d06..37d70737882850d50db381d196422046a124b6dd 100644 --- a/src/components/songs-table.tsx +++ b/src/components/songs-table.tsx @@ -12,7 +12,7 @@ interface PremiumSong { audioFilename: string; } -export function TableSongs( data: PremiumSong[] ) { +export function TableSongs({data}: { data: PremiumSong[] } ) { const formatDuration = (seconds: number): string => { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; diff --git a/src/context/AuthProvider.tsx b/src/context/AuthProvider.tsx index f20c0439afef0023c2c82748508ae3430200b05e..3c0f9045f394ea0361b1df44e7a1f7f976f89f49 100644 --- a/src/context/AuthProvider.tsx +++ b/src/context/AuthProvider.tsx @@ -1,27 +1,23 @@ import React from "react"; import { AuthContext } from "@/TonalityApp.tsx"; -import { useNavigate } from "react-router-dom"; +import {Outlet, useNavigate} from "react-router-dom"; -const AuthProvider = ({ children }) => { +const AuthProvider = () => { const navigate = useNavigate(); - const [accessToken, setAccessToken] = React.useState(null); + const [accessToken, setAccessToken] = + React.useState<string | null>(sessionStorage.getItem("accessToken") ?? null); - if (sessionStorage.getItem("accessToken")) { - setAccessToken(sessionStorage.getItem("accessToken")); - navigate("/album"); - } - - const handleLogin = (accessToken) => { + const handleLogin = (accessToken: string) => { sessionStorage.setItem("accessToken", accessToken); setAccessToken(accessToken); - navigate("/album"); + navigate("/album", { replace: true }); }; const handleLogout = () => { sessionStorage.removeItem("accessToken"); setAccessToken(null); - navigate("/login"); + navigate("/login", { replace: true }); }; const value = { @@ -30,7 +26,9 @@ const AuthProvider = ({ children }) => { onLogout: handleLogout, }; - return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; + return <AuthContext.Provider value={value}> + <Outlet /> + </AuthContext.Provider>; }; export default AuthProvider; diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index f06545bdf397b739b4f9f804927c0ae384ffa5bb..ac0159f059c88082b4e6ae7b6b9c38bf152ba162 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -3,12 +3,14 @@ import { Outlet } from "react-router-dom"; const MainLayout = () => { return ( - <div className="wrapper"> - <Sidebar /> - <div className="ml-20 pr-10"> - <Outlet /> + <> + <div className="wrapper"> + <Sidebar /> + <div className="ml-20 pr-10"> + <Outlet /> + </div> </div> - </div> + </> ); }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c10ddbc6a064a0ed9db4b7cf78d02d813a60ce1e..0253a7188f825d463fcc34b0767abb9aa5fd4ff4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -12,7 +12,7 @@ interface Route { title: string; hasSiderLink?: boolean; routes?: Route[]; - component?: React.ComponentType<any>; + component?: React.ComponentType<unknown>; path?: string; isPublic?: boolean; } diff --git a/src/pages/AddAlbumPage.tsx b/src/pages/AddAlbumPage.tsx index 2fa1d3caa7f6c79aa9471648a1d621e15d3e66c2..3ac0fcb8d364cf1147748ed18d0a0d3587956f5f 100644 --- a/src/pages/AddAlbumPage.tsx +++ b/src/pages/AddAlbumPage.tsx @@ -1,6 +1,7 @@ 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"), @@ -32,7 +33,7 @@ export default function AddAlbum() { // Validate albumSchema.parse({ albumName, releaseDate, genre, artist }); - await axios.post("http://localhost:3000/api/premium-album", formData, { + await api.post("/premium-album", formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -46,7 +47,10 @@ export default function AddAlbum() { return ( <div className="w-full max-w-xs ml-[450px] mt-[50px]"> - <form id="addForm" className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> + <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> diff --git a/src/pages/AddSongPage.tsx b/src/pages/AddSongPage.tsx index bb3f976bb8b54e4857bc50cdd4a168e293147a88..ec654a93b884d31aa930c22127fcb3a06ee5e6a0 100644 --- a/src/pages/AddSongPage.tsx +++ b/src/pages/AddSongPage.tsx @@ -1,7 +1,9 @@ -import axios from "axios"; import { useState } from "react"; import { useParams } from "react-router"; -import { number, object, string, z } from "zod"; +import { number, object, string } from "zod"; +import {useForm} from "react-hook-form"; +import {zodResolver} from "@hookform/resolvers/zod"; +import api from "@/api/api.ts"; const songSchema = object({ title: string().min(1, "Title cannot be empty"), @@ -13,30 +15,35 @@ const songSchema = object({ const AddSong = () => { const { albumId }= useParams(); - const [title, setTitle] = useState(""); - const [artist, setArtist] = useState(""); - const [songNumber, setSongNumber] = useState(""); - const [discNumber, setDiscNumber] = useState(""); - const [duration, setDuration] = useState(""); - const [audioFile, setAudioFile] = useState(null); + const form = useForm({ + resolver: zodResolver(songSchema), + defaultValues: { + title: "", + artist: "", + songNumber: "", + discNumber: "", + duration: "", + } + }) + const [audioFile, setAudioFile] = useState<File | null>(null); - const handleAddSong = async () => { + const handleAddSong = 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("title", title); - formData.append("artist", artist); - formData.append("songNumber", songNumber); - formData.append("discNumber", discNumber); - formData.append("duration", duration); - if (audioFile !== null) { - formData.append("audioFile", audioFile); + 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); - songSchema.parse({ title, artist, songNumber, discNumber, duration }); - - await axios.post(`http://localhost:3000/api/premium-album/${albumId}`, formData, { + await api.post(`/premium-album/${albumId}`, formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -46,11 +53,11 @@ const AddSong = () => { } catch (error) { console.error("Error adding Song:", error); } - }; + }); return ( <div className="w-full max-w-xs ml-[450px] mt-[50px]"> - <form id="addForm" className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"> + <form id="addForm" className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={handleAddSong}> <div className="mb-2 text-2xl font-bold"> Add Song </div> @@ -123,11 +130,15 @@ const AddSong = () => { id="audioFile" type="file" accept="audio/*" + onChange={(e) => { + if (e.target.files) { + setAudioFile(e.target.files[0]); + }}} /> </div> <div className="flex items-center justify-center"> - <button onClick={handleAddSong} 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"> + <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> diff --git a/src/pages/AlbumPage.tsx b/src/pages/AlbumPage.tsx index 7f380d771c6bfad13c693e84a2a26226e21e4e67..c3f356392fd282ce0d89778fbaf3dd7456991f30 100644 --- a/src/pages/AlbumPage.tsx +++ b/src/pages/AlbumPage.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import AlbumCard from '@/components/album-card'; import "../styles/Albums.css"; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; import { Button } from '@/components/ui/button'; +import api from "@/api/api.ts"; interface PremiumAlbum { albumId: number; @@ -14,7 +14,7 @@ interface PremiumAlbum { coverFilename: string; } -const AlbumPage: React.FC = () => { +const AlbumPage = () => { const [dataAlbums, setDataAlbums] = useState<PremiumAlbum[]>([]); const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); @@ -23,7 +23,8 @@ const AlbumPage: React.FC = () => { const fetchData = async (page: number) => { try { - const response = await axios.get(`http://localhost:3000/api/premium-album?page=${page}`); + const response = await api.get( + `/premium-album?page=${page}`); console.log(response); setDataAlbums(response.data.data); diff --git a/src/pages/EditAlbumPage.tsx b/src/pages/EditAlbumPage.tsx index 76b8f6d26da66fb5230c98234ab06ca631487e1d..81b098699a77cb6892d1a88e93dda872c84d44b4 100644 --- a/src/pages/EditAlbumPage.tsx +++ b/src/pages/EditAlbumPage.tsx @@ -1,7 +1,7 @@ -import axios from 'axios'; -import { useEffect, useState } from 'react'; +import {ChangeEventHandler, useEffect, useState} from 'react'; import { useParams } from 'react-router-dom'; -import { date, object, string, z } from 'zod'; +import { date, object, string } from 'zod'; +import api from "@/api/api.ts"; const albumSchema = object({ albumName: string().min(1, "Album Name cannot be empty"), @@ -24,8 +24,8 @@ export default function EditAlbum() { useEffect(() => { const fetchAlbumData = async () => { try { - const response = await axios.get( - `http://localhost:3000/api/premium-album/${albumId}` + const response = await api.get( + `/premium-album/${albumId}` ); setAlbumData(response.data); } catch (error) { @@ -39,8 +39,8 @@ export default function EditAlbum() { const handleEditAlbum = async () => { try { albumSchema.parse(albumData); - await axios.patch( - `http://localhost:3000/api/premium-album/${albumId}`, + await api.patch( + `/premium-album/${albumId}`, albumData ); @@ -50,7 +50,7 @@ export default function EditAlbum() { } }; - const handleChange = (e: { target: { id: any; value: any; }; }) => { + const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => { setAlbumData({ ...albumData, [e.target.id]: e.target.value }); }; diff --git a/src/pages/EditSongPage.tsx b/src/pages/EditSongPage.tsx index 80d5f9965e753ce393ed3eb4afc8922cf42d735f..3b8f415c2d69a0a51fd6efb36060de816d165d49 100644 --- a/src/pages/EditSongPage.tsx +++ b/src/pages/EditSongPage.tsx @@ -1,7 +1,7 @@ -import axios from "axios"; -import { useState } from "react"; +import {ChangeEventHandler, useState} from "react"; import { useParams } from "react-router-dom"; -import { number, object, string, z } from "zod"; +import { number, object, string } from "zod"; +import api from "@/api/api.ts"; const songSchema = object({ title: string().min(1, "Title cannot be empty"), @@ -26,8 +26,8 @@ const EditSong = () => { const handleEditSong = async () => { try { songSchema.parse(songsData); - await axios.patch( - `/api/premium-album/${albumId}/${songId}`, + await api.patch( + `/premium-album/${albumId}/${songId}`, songsData ); @@ -37,7 +37,7 @@ const EditSong = () => { } }; - const handleChange = (e: { target: { id: any; value: any; }; }) => { + const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => { setSongsData({ ...songsData, [e.target.id]: e.target.value }); }; diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 0756d91b3a6242ac26c8d73345ab7f8a60209db4..9fa9bff228a148e9b86385ce8ea2385b203f9440 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -58,11 +58,11 @@ const LoginPage = () => { password: "", }, }); + const { onLogin } = useAuth(); // Define submit handler async function onSubmit(values: z.infer<typeof loginFormSchema>) { console.log("values", values); - const { onLogin } = useAuth(); try { const res = await api.post( "login", diff --git a/src/pages/SongsPage.tsx b/src/pages/SongsPage.tsx index 2c5706ea95fa2c9a68d397dc8eb7927ce464923a..84a36726d68b799bd35402c947d48c17c51a5a2c 100644 --- a/src/pages/SongsPage.tsx +++ b/src/pages/SongsPage.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; 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 axios from 'axios'; +import api from "@/api/api.ts"; interface PremiumSong { songId: number; @@ -16,7 +16,7 @@ interface PremiumSong { audioFilename: string; } -const SongsPage: React.FC = () => { +const SongsPage = () => { const { albumId } = useParams<{ albumId: string }>(); const [songsData, setSongsData] = useState<PremiumSong[]>([]); const [loading, setLoading] = useState(true); @@ -24,7 +24,8 @@ const SongsPage: React.FC = () => { useEffect(() => { const fetchData = async () => { try { - const response = await axios.get(`http://localhost:3000/api/premium-album/${albumId}`); + const response = await api.get( + `/premium-album/${albumId}`); console.log(response); setSongsData(response.data); @@ -84,7 +85,7 @@ const SongsPage: React.FC = () => { </div> <div> { - loading ? <div className="text-white">Loading . . .</div> : <TableSongs {...songsData} /> + loading ? <div className="text-white">Loading . . .</div> : <TableSongs data={songsData} /> } </div> </div> diff --git a/src/pages/SubscriptionPage.tsx b/src/pages/SubscriptionPage.tsx index e3981bac9774b32834f01b7846f15e5cf399a3d8..c4dd32fdb91e8a5067154b7f45916f07b0c2f7cc 100644 --- a/src/pages/SubscriptionPage.tsx +++ b/src/pages/SubscriptionPage.tsx @@ -1,5 +1,4 @@ -import React, {useEffect, useState} from 'react'; -import axios from "axios"; +import {useEffect, useState} from 'react'; import {SubscriptionTable} from "@/components/subscription-table.tsx"; import api from "@/api/api.ts"; @@ -15,7 +14,7 @@ interface Subscription { } const SubscriptionPage = () => { - const [subscriptionData, setSubscriptionData] : [Subscription[], (subscriptionData: Subscription[]) => void] = useState([]); + const [subscriptionData, setSubscriptionData] = useState<Subscription[]>([]); const [loading, setLoading] = useState(true); useEffect(() => { // get current page from query params diff --git a/src/routes/ProtectedRoute.tsx b/src/routes/ProtectedRoute.tsx index 5b4eb2cbd57502cd11fc6ab2134da2c6ba42e7f7..27f466624a2bd34cfeec10711ee8de3e8dbf3cef 100644 --- a/src/routes/ProtectedRoute.tsx +++ b/src/routes/ProtectedRoute.tsx @@ -1,8 +1,9 @@ import { Navigate, Outlet } from "react-router-dom"; import { useAuth } from "@/TonalityApp.tsx"; -const ProtectedRoute = ({ isPublic }) => { - const isValidUser: boolean = useAuth().token === null; + +const ProtectedRoute = ({ isPublic }: {isPublic: boolean}) => { + const isValidUser: boolean = useAuth().token !== null; return isValidUser || isPublic ? <Outlet /> : <Navigate to="/login" />; }; diff --git a/src/routes/RenderRoutes.tsx b/src/routes/RenderRoutes.tsx index 8eb823c2674fbf748af552345889ac80bf9df37a..4a36612325540e5c97eaa32db8212c0ec65f5e95 100644 --- a/src/routes/RenderRoutes.tsx +++ b/src/routes/RenderRoutes.tsx @@ -1,16 +1,18 @@ -import React from "react"; import { Route, Routes } from "react-router-dom"; import ProtectedRoute from "@/routes/ProtectedRoute.tsx"; import { generateFlattenRoutes } from "@/lib/utils.ts"; +import {RouteWithLayout} from "@/routes/routes.ts"; +import AuthProvider from "@/context/AuthProvider.tsx"; -export const RenderRoutes: React.FC = (mainRoutes) => { - return ({ isAuthorized }) => { +export const RenderRoutes = (mainRoutes: RouteWithLayout[]) => { + return () => { const layouts = mainRoutes.map(({ layout: Layout, routes }, index) => { const subRoutes = generateFlattenRoutes(routes); return ( <Route key={index} element={<Layout />}> {subRoutes.map( + // @ts-expect-error error bg ({ component: Component, path, name, isPublic }, index) => { const isPublics: boolean = typeof isPublic === "boolean" ? isPublic : false; @@ -23,7 +25,6 @@ export const RenderRoutes: React.FC = (mainRoutes) => { element={ <ProtectedRoute isPublic={isPublics} - isAuthorized={isAuthorized} /> } > @@ -38,7 +39,9 @@ export const RenderRoutes: React.FC = (mainRoutes) => { }); return ( <Routes> - <>{layouts}</> + <Route element={<AuthProvider />}> + {layouts} + </Route> </Routes> ); }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 8a1e6077ff9ecb33e0ec2aac99dac1e4314ff054..bc5b7bc41a0e9acd2ec48f2cba993d9b830a74e7 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -13,8 +13,22 @@ import NotMatch from "@/pages/NotMatch.tsx"; import DeleteAlbumDialog from "@/components/delete-dialog-album"; import DeleteSongDialog from "@/components/delete-dialog-song"; import SubscriptionPage from "@/pages/SubscriptionPage.tsx"; +import {ReactNode} from "react"; -export const routes = [ +export type Route = { + name: string; + title: string; + component?: () => ReactNode, + path?: string; + isPublic?: boolean; + hasSiderLink?: boolean; + routes?: Array<Route>; +} +export type RouteWithLayout = { + layout: () => ReactNode, + routes: Array<Route> +} +export const routes: RouteWithLayout[] = [ { layout: AnonymousLayout, routes: [