added album

parent 140b0572
......@@ -7,6 +7,8 @@ import Tracks from './containers/Tracks';
import Login from './containers/Login';
import TrackHistory from './containers/TrackHistory';
import AddArtist from './containers/AddArtist';
import AddAlbum from './containers/AddAlbum';
import AddTrack from './containers/AddTrack';
const App: React.FunctionComponent = (): React.ReactElement => {
return (
......@@ -19,6 +21,8 @@ const App: React.FunctionComponent = (): React.ReactElement => {
<Route path="login" element={<Login />} />
<Route path="track-history" element={<TrackHistory />} />
<Route path="add-artist" element={<AddArtist />} />
<Route path="add-album" element={<AddAlbum />} />
<Route path="add-track" element={<AddTrack />} />
</Route>
</Routes>
</BrowserRouter>
......
......@@ -54,6 +54,18 @@ const Header = () => {
>
Add Artist
</Link>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
to={'add-album'}
>
Add Album
</Link>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
to={'add-track'}
>
Add Track
</Link>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
to={'/'}
......
import React, {ChangeEvent, Fragment, useEffect, useState} from 'react';
import IAlbum from '../interfaces/IAlbum';
import {useAppDispatch, useAppSelector} from '../store/hooks';
import {useNavigate} from 'react-router-dom';
import {getArtists} from '../features/artist/artistSlice';
import {addAlbum} from '../features/album/albumSlice';
const AddAlbum: React.FunctionComponent = (): React.ReactElement => {
const [newAlbum, setNewAlbum] = useState<IAlbum>({
name: '',
year: '',
} as IAlbum);
const [file, setFile] = useState<File>();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const {artists} = useAppSelector((state) => state.artist);
let [artist, setArtist] = useState<string>('');
useEffect(() => {
dispatch(getArtists());
}, [dispatch]);
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) =>
setNewAlbum((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}));
const fileHandler = (event: React.FormEvent) => {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
setFile(files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const album = new FormData();
album.append('name', newAlbum.name);
album.append('year', newAlbum.year);
album.append('image', file!);
album.append('artist', artist);
await dispatch(addAlbum(album));
navigate('/');
};
return (
<form
className="pt-10"
encType="multipart/form-data"
onSubmit={handleSubmit}
>
<select
required
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setArtist(e.target.value)
}
>
<option value="">Select Artist</option>
{artists.map((artist) => (
<Fragment key={artist._id}>
{artist.published ? (
<option value={artist._id}>{artist.name}</option>
) : null}
</Fragment>
))}
</select>
<label
htmlFor="small-input"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Small input
</label>
<input
required
onChange={handleChange}
value={newAlbum.name}
placeholder="Album"
name="name"
type="text"
id="small-input"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
/>
<label
htmlFor="message"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Your message
</label>
<input
type="number"
required
onChange={handleChange}
value={newAlbum.year}
id="message"
name="year"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Year"
></input>
<label
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
htmlFor="file_input"
>
Upload file
</label>
<input
required
onChange={fileHandler}
className="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400"
aria-describedby="file_input_help"
id="file_input"
type="file"
/>
<p
className="mt-1 text-sm text-gray-500 dark:text-gray-300"
id="file_input_help"
>
SVG, PNG, JPG or GIF (MAX. 800x400px).
</p>
<button type="submit">Submit</button>
</form>
);
};
export default AddAlbum;
......@@ -2,6 +2,7 @@ import React, {ChangeEvent, useState} from 'react';
import IArtist from '../interfaces/IArtist';
import {useAppDispatch} from '../store/hooks';
import {addArtist} from '../features/artist/artistSlice';
import {useNavigate} from 'react-router-dom';
const AddArtist: React.FunctionComponent = (): React.ReactElement => {
const [newArtist, setNewArtist] = useState<IArtist>({
......@@ -10,6 +11,7 @@ const AddArtist: React.FunctionComponent = (): React.ReactElement => {
} as IArtist);
const [file, setFile] = useState<File>();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
......@@ -27,7 +29,7 @@ const AddArtist: React.FunctionComponent = (): React.ReactElement => {
}
};
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const artist = new FormData();
artist.append('name', newArtist.name);
......@@ -35,7 +37,8 @@ const AddArtist: React.FunctionComponent = (): React.ReactElement => {
console.log(artist);
artist.append('photo', file!);
dispatch(addArtist(artist));
await dispatch(addArtist(artist));
navigate('/');
};
return (
<form encType="multipart/form-data" onSubmit={handleSubmit}>
......
import React, {ChangeEvent, Fragment, useEffect, useState} from 'react';
import ITrack from '../interfaces/ITrack';
import {useAppDispatch, useAppSelector} from '../store/hooks';
import {useNavigate, useParams} from 'react-router-dom';
import {getArtists} from '../features/artist/artistSlice';
// import {addTrack} from '../features/track/trackSlice';
import {getAlbums} from '../features/album/albumSlice';
import {addTrack} from '../features/track/trackSlice';
const AddTrack: React.FunctionComponent = (): React.ReactElement => {
const [newTrack, setNewTrack] = useState<ITrack>({
name: '',
duration: '',
} as ITrack);
const dispatch = useAppDispatch();
const navigate = useNavigate();
const {albums} = useAppSelector((state) => state.album);
let [album, setAlbum] = useState<string>('');
useEffect(() => {
dispatch(getAlbums());
}, [dispatch]);
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) =>
setNewTrack((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}));
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const data: ITrack = {
album,
duration: newTrack.duration,
name: newTrack.name,
};
console.log(data);
await dispatch(addTrack(data));
navigate('/');
};
return (
<form
className="pt-10"
encType="multipart/form-data"
onSubmit={handleSubmit}
>
<select
required
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setAlbum(e.target.value)
}
>
<option value="">Select Album</option>
{albums.map((album) => (
<Fragment key={album._id}>
{album.published ? (
<option value={album._id}>{album.name}</option>
) : null}
</Fragment>
))}
</select>
<label
htmlFor="small-input"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Small input
</label>
<input
required
onChange={handleChange}
value={newTrack.name}
placeholder="Track"
name="name"
type="text"
id="small-input"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 "
/>
<label
htmlFor="message"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
Your message
</label>
<input
type="number"
required
onChange={handleChange}
value={newTrack.duration}
id="message"
name="duration"
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Duration"
></input>
<p
className="mt-1 text-sm text-gray-500 dark:text-gray-300"
id="file_input_help"
>
SVG, PNG, JPG or GIF (MAX. 800x400px).
</p>
<button type="submit">Submit</button>
</form>
);
};
export default AddTrack;
import React, {useEffect} from 'react';
import {getAlbumsByQueryParams} from '../features/album/albumSlice';
import React, {Fragment, useEffect, useState} from 'react';
import {
deleteAlbum,
getAlbumsByQueryParams,
publishAlbum,
} from '../features/album/albumSlice';
import {useAppDispatch, useAppSelector} from '../store/hooks';
import {useLocation} from 'react-router-dom';
import {useNavigate} from 'react-router-dom';
import IUser from '../interfaces/IUser';
const Albums: React.FunctionComponent = (): React.ReactElement => {
const {albums} = useAppSelector((state) => state.album);
......@@ -11,11 +16,30 @@ const Albums: React.FunctionComponent = (): React.ReactElement => {
const artist = params.get('artist');
const {state} = useLocation();
const navigate = useNavigate();
const [role, setRole] = useState<string>('User');
useEffect(() => {
const userRole = localStorage.getItem('userRole');
if (userRole) {
setRole(userRole);
}
dispatch(getAlbumsByQueryParams(artist!));
}, [dispatch]);
const handlePublish = async (id: string) => {
await dispatch(publishAlbum(id));
await dispatch(getAlbumsByQueryParams(artist!));
};
const handleDelete = async (id: string) => {
const user = localStorage.getItem('user');
if (user) {
const foundUser: IUser = JSON.parse(user);
await dispatch(deleteAlbum({id, token: foundUser.token!}));
}
await dispatch(getAlbumsByQueryParams(artist!));
};
return (
<div>
<h2 className="mt-0 mb-2 text-4xl font-medium leading-tight ">Albums</h2>
......@@ -24,21 +48,42 @@ const Albums: React.FunctionComponent = (): React.ReactElement => {
{albums.length ? (
albums.map((album) => {
return (
<div key={album._id}>
<img
onClick={() =>
navigate(
{pathname: '/tracks', search: `?album=${album._id}`},
{state: {album: album.name, artist: state.artist}}
)
}
className="w-48 h-48 object-cover "
src={`${import.meta.env.VITE_MY_URL}/${album.image}`}
alt={album.name}
/>
<p>{album.name}</p>
<p>{album.year}</p>
</div>
<Fragment key={album._id}>
{role === 'User' && !album.published ? null : (
<div>
<img
onClick={() =>
navigate(
{pathname: '/tracks', search: `?album=${album._id}`},
{state: {album: album.name, artist: state.artist}}
)
}
className="w-48 h-48 object-cover "
src={`${import.meta.env.VITE_MY_URL}/${album.image}`}
alt={album.name}
/>
<p>{album.name}</p>
<p>{album.year}</p>
{role === 'Admin' ? (
<div>
<button onClick={() => handleDelete(album._id || '')}>
Delete
</button>
{!album.published && (
<>
<p>Unpublished</p>
<button
onClick={() => handlePublish(album._id || '')}
>
Publish
</button>
</>
)}
</div>
) : null}
</div>
)}
</Fragment>
);
})
) : (
......
import React, {useEffect, useState} from 'react';
import {getArtists} from '../features/artist/artistSlice';
import {
deleteArtist,
getArtists,
publishArtist,
} from '../features/artist/artistSlice';
import {useAppDispatch, useAppSelector} from '../store/hooks';
import {useNavigate} from 'react-router-dom';
import Preloader from '../components/UI/Preloader';
import IUser from '../interfaces/IUser';
const HomePage: React.FunctionComponent = (): React.ReactElement => {
const {loading, artists} = useAppSelector((state) => state.artist);
......@@ -11,6 +16,19 @@ const HomePage: React.FunctionComponent = (): React.ReactElement => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const handleDelete = async (id: string) => {
const user = localStorage.getItem('user');
if (user) {
const foundUser: IUser = JSON.parse(user);
await dispatch(deleteArtist({id, token: foundUser.token!}));
}
await dispatch(getArtists());
};
const handlePublish = async (id: string) => {
await dispatch(publishArtist(id));
await dispatch(getArtists());
};
useEffect(() => {
dispatch(getArtists());
}, [dispatch, navigate, userRole]);
......@@ -43,10 +61,16 @@ const HomePage: React.FunctionComponent = (): React.ReactElement => {
/>
<span className="block">{artist.name}</span>
{!artist.published && <p>Unpublished</p>}
{artist.published ? (
<button>Delete</button>
) : (
<button>Publish</button>
<button
className="block"
onClick={() => handleDelete(artist._id || '')}
>
Delete
</button>
{!artist.published && (
<button onClick={() => handlePublish(artist._id || '')}>
Publish
</button>
)}
</>
) : artist.published ? (
......@@ -66,7 +90,11 @@ const HomePage: React.FunctionComponent = (): React.ReactElement => {
alt={artist.name}
/>
<span className="block">{artist.name}</span>
<button>Delete</button>
{userRole === 'Admin' && (
<button onClick={() => handleDelete(artist._id || '')}>
Delete
</button>
)}
</>
) : null}
</div>
......
import React, {useEffect} from 'react';
import React, {Fragment, useEffect, useState} from 'react';
import {useLocation} from 'react-router-dom';
import {addTrackHistory} from '../features/track-history/trackHistorySlice';
import {getTracksByQuery} from '../features/track/trackSlice';
import {
deleteTrack,
getTracksByQuery,
publishTrack,
} from '../features/track/trackSlice';
import ITrackHistoryDto from '../interfaces/ITrackHistoryDto';
import {useAppDispatch, useAppSelector} from '../store/hooks';
import IUser from '../interfaces/IUser';
const Tracks: React.FunctionComponent = (): React.ReactElement => {
const {state} = useLocation();
......@@ -12,8 +17,13 @@ const Tracks: React.FunctionComponent = (): React.ReactElement => {
const params = new URLSearchParams(window.location.search);
const album = params.get('album');
const {userLoggedIn} = useAppSelector((state) => state.user);
const [role, setRole] = useState<string>('User');
useEffect(() => {
const userRole = localStorage.getItem('userRole');
if (userRole) {
setRole(userRole);
}
dispatch(getTracksByQuery(album!));
}, [dispatch]);
......@@ -27,6 +37,20 @@ const Tracks: React.FunctionComponent = (): React.ReactElement => {
await dispatch(addTrackHistory(data));
};
const handleDelete = async (id: string) => {
const user = localStorage.getItem('user');
if (user) {
const foundUser: IUser = JSON.parse(user);
await dispatch(deleteTrack({id, token: foundUser.token!}));
}
await dispatch(getTracksByQuery(album!));
};
const handlePublish = async (id: string) => {
await dispatch(publishTrack(id));
await dispatch(getTracksByQuery(album!));
};
return (
<div>
<h2 className="mt-0 mb-2 text-4xl font-medium leading-tight ">Tracks</h2>
......@@ -36,17 +60,37 @@ const Tracks: React.FunctionComponent = (): React.ReactElement => {
{tracks.length ? (
tracks.map((track) => {
return (
<div
onClick={() =>
userLoggedIn ? handleClick(track._id) : undefined
}
className="border border-solid color border-black p-4"
key={track._id}
>
<p>Track number: {track.seq.toString()}</p>
<p>Track name: {track.name}</p>
<p>Track duration: {track.duration}</p>
</div>
<Fragment key={track._id}>
{role === 'User' && !track.published ? null : (
<div
onClick={() =>
userLoggedIn ? handleClick(track._id!) : undefined
}
className="border border-solid color border-black p-4"
>
<p>Track number: {track.seq!.toString()}</p>
<p>Track name: {track.name}</p>
<p>Track duration: {track.duration}</p>
{role === 'Admin' ? (
<div>
<button onClick={() => handleDelete(track._id || '')}>
Delete
</button>
{!track.published && (
<>
<p>Unpublished</p>
<button
onClick={() => handlePublish(track._id || '')}
>
Publish
</button>
</>
)}
</div>
) : null}
</div>
)}
</Fragment>
);
})
) : (
......
......@@ -31,6 +31,50 @@ export const getAlbumsByQueryParams = createAsyncThunk(
}
);
export const addAlbum = createAsyncThunk(
'addAlbum',
async (newAlbum: FormData) => {
const response = await axios({
method: 'post',
url: `${import.meta.env.VITE_MY_URL}/albums`,
data: newAlbum,
headers: {
'Content-Type': `multipart/form-data; `,
},
});
return response.data;
}
);
export const publishAlbum = createAsyncThunk(
'publishAlbum',
async (id: string) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_MY_URL}/albums/${id}/publish`
);
return response.data;
} catch (err) {
console.log(err);
}
}
);
export const deleteAlbum = createAsyncThunk(
'deletAlbum',
async (data: {id: string; token: string}) => {
const response = await axios.delete(
`${import.meta.env.VITE_MY_URL}/albums/${data.id}`,
{
headers: {
Authorization: data.token,
},
}
);
return response.data;
}
);
export const albumSlice = createSlice({
name: 'artist',
initialState,
......@@ -48,7 +92,35 @@ export const albumSlice = createSlice({
(state, {payload}: PayloadAction<IAlbum[]>) => {
state.albums = payload;
}
);
)
.addCase(addAlbum.pending, (state, action) => {
state.loading = true;
})
.addCase(addAlbum.rejected, (state, action) => {
state.loading = false;
})
.addCase(addAlbum.fulfilled, (state, action) => {
state.loading = false;
})
.addCase(publishAlbum.pending, (state, action) => {
state.loading = true;
})
.addCase(publishAlbum.rejected, (state, action) => {
state.loading = false;
})
.addCase(publishAlbum.fulfilled, (state, action) => {
state.loading = false;
})
.addCase(getAlbums.pending, (state, action) => {
state.loading = true;
})
.addCase(getAlbums.rejected, (state, action) => {
state.loading = false;
})
.addCase(getAlbums.fulfilled, (state, action) => {
state.loading = false;
state.albums = action.payload;
});
},
});
......
......@@ -36,6 +36,35 @@ export const addArtist = createAsyncThunk(
}
);
export const deleteArtist = createAsyncThunk(
'deleteArtist',
async (data: {id: string; token: string}) => {
const response = await axios.delete(
`${import.meta.env.VITE_MY_URL}/artists/${data.id}`,
{
headers: {
Authorization: data.token,
},
}
);
return response.data;
}
);
export const publishArtist = createAsyncThunk(
'publishArtist',
async (id: string) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_MY_URL}/artists/${id}/publish`
);
return response.data;
} catch (err) {
console.log(err);
}
}
);
export const artistSlice = createSlice({
name: 'artist',
initialState,
......@@ -63,7 +92,24 @@ export const artistSlice = createSlice({
})
.addCase(addArtist.fulfilled, (state, action) => {
state.loading = false;
console.log(action.payload);
})
.addCase(deleteArtist.pending, (state, action) => {
state.loading = true;
})
.addCase(deleteArtist.rejected, (state, action) => {
state.loading = false;
})
.addCase(deleteArtist.fulfilled, (state, action) => {
state.loading = false;
})
.addCase(publishArtist.pending, (state, action) => {
state.loading = true;
})
.addCase(publishArtist.rejected, (state, action) => {
state.loading = false;
})
.addCase(publishArtist.fulfilled, (state, action) => {
state.loading = false;
});
},
});
......
......@@ -26,6 +26,43 @@ export const getTracksByQuery = createAsyncThunk(
}
);
export const addTrack = createAsyncThunk('addTrack', async (data: ITrack) => {
const response = await axios.post(
`${import.meta.env.VITE_MY_URL}/tracks`,
data
);
return response.data;
});
export const publishTrack = createAsyncThunk(
'publishTrack',
async (id: string) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_MY_URL}/tracks/${id}/publish`
);
return response.data;
} catch (err) {
console.log(err);
}
}
);
export const deleteTrack = createAsyncThunk(
'deletTrack',
async (data: {id: string; token: string}) => {
const response = await axios.delete(
`${import.meta.env.VITE_MY_URL}/tracks/${data.id}`,
{
headers: {
Authorization: data.token,
},
}
);
return response.data;
}
);
export const trackSlice = createSlice({
name: 'track',
initialState,
......@@ -43,7 +80,25 @@ export const trackSlice = createSlice({
(state, {payload}: PayloadAction<ITrack[]>) => {
state.tracks = payload;
}
);
)
.addCase(addTrack.pending, (state, action) => {
state.loading = true;
})
.addCase(addTrack.rejected, (state, action) => {
state.loading = false;
})
.addCase(addTrack.fulfilled, (state, action) => {
state.loading = false;
})
.addCase(publishTrack.pending, (state, action) => {
state.loading = true;
})
.addCase(publishTrack.rejected, (state, action) => {
state.loading = false;
})
.addCase(publishTrack.fulfilled, (state, action) => {
state.loading = false;
});
},
});
......
......@@ -74,6 +74,9 @@ export const userSlice = createSlice({
setUserRole: (state, action) => {
state.userRole = action.payload;
},
setUser: (state, action) => {
state.user = action.payload;
},
},
extraReducers: (builder) => {
builder
......@@ -132,6 +135,7 @@ export const userSlice = createSlice({
},
});
export const {setUserLoggedIn, setLogOut, setUserRole} = userSlice.actions;
export const {setUserLoggedIn, setLogOut, setUserRole, setUser} =
userSlice.actions;
export default userSlice.reducer;
......@@ -3,7 +3,8 @@ import IArtist from './IArtist';
export default interface IAlbum {
name: string;
artist: IArtist;
year: number;
year: string;
image: string;
_id: string;
_id?: string;
published: boolean;
}
......@@ -2,6 +2,7 @@ export default interface ITrack {
name: string;
album: string;
duration: string;
seq: Number;
_id: string;
seq?: Number;
_id?: string;
published?: boolean;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment