#9 added track history slice

parent 0e3f49f7
......@@ -4,6 +4,8 @@ import {BrowserRouter, Routes, Route} from 'react-router-dom';
import Layout from './components/Layout/Layout';
import Albums from './containers/Albums';
import Tracks from './containers/Tracks';
import Login from './containers/Login';
import TrackHistory from './containers/TrackHistory';
const App = () => {
return (
......@@ -13,6 +15,8 @@ const App = () => {
<Route index element={<HomePage />} />
<Route path="albums" element={<Albums />} />
<Route path="tracks" element={<Tracks />} />
<Route path="login" element={<Login />} />
<Route path="track-history" element={<TrackHistory />} />
</Route>
</Routes>
</BrowserRouter>
......
import React from 'react';
import {Link, useNavigate} from 'react-router-dom';
import {useAppSelector} from '../store/hooks';
const Header = () => {
const navigate = useNavigate();
const {userLoggedIn} = useAppSelector((state) => state.user);
return (
<div>
{!userLoggedIn ? (
<Link
to={'login'}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
sign in
</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={'track-history'}
>
Track History
</Link>
)}
</div>
);
};
export default Header;
import React from 'react';
import {Outlet} from 'react-router-dom';
import Header from '../Header';
const Layout: React.FunctionComponent = (): React.ReactElement => {
return (
<div className="container mx-auto px-96">
<div className="container mx-auto px-96 pt-4">
<Header />
<Outlet />
</div>
);
......
import React, {useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import {getArtists} from '../features/artist/artistSlice';
import {useAppDispatch, useAppSelector} from '../store/hooks';
import {useNavigate} from 'react-router-dom';
import Preloader from '../components/UI/Preloader';
const HomePage: React.FunctionComponent = (): React.ReactElement => {
const {artists, loading} = useAppSelector((state) => state.artist);
const {loading, artists} = useAppSelector((state) => state.artist);
const dispatch = useAppDispatch();
const navigate = useNavigate();
......
import React, {useEffect, useState} from 'react';
import {Navigate, useNavigate} from 'react-router-dom';
import {createUser, loginUser} from '../features/user/userSlice';
import IUser from '../interfaces/IUser';
import {useAppDispatch, useAppSelector} from '../store/hooks';
const Login = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [user, setUser] = useState({} as IUser);
const {userLoggedIn} = useAppSelector((state) => state.user);
const handleRegister = async () => {
await dispatch(createUser(user));
};
const handleUserInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setUser((prevState) => ({...prevState, [e.target.name]: e.target.value}));
};
const handleSubmit = async (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault();
await dispatch(loginUser(user));
};
if (userLoggedIn) {
return <Navigate to={'/'} />;
}
return (
<div className="w-full max-w-xs m-auto h-screen mt-[200px]">
<form
onSubmit={handleSubmit}
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
>
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="username"
>
Username
</label>
<input
onChange={handleUserInput}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text"
placeholder="Username"
name="username"
/>
</div>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Password
</label>
<input
onChange={handleUserInput}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password"
placeholder="Password"
name="password"
/>
</div>
<div className="flex items-center justify-between">
<button
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"
>
Sign In
</button>
<a
className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"
href="#"
type="button"
onClick={handleRegister}
>
Register
</a>
</div>
</form>
<p className="text-center text-gray-500 text-xs">
&copy;2020 Acme Corp. All rights reserved.
</p>
</div>
);
};
export default Login;
import React, {useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import {getTracks} from '../features/track-history/trackHistorySlice';
import {useAppDispatch, useAppSelector} from '../store/hooks';
const TrackHistory = () => {
const dispatch = useAppDispatch();
const {tracks} = useAppSelector((state) => state.trackHistory);
const {userLoggedIn} = useAppSelector((state) => state.user);
const token = window.sessionStorage.getItem('token');
const navigate = useNavigate();
useEffect(() => {
if (!userLoggedIn) navigate('/');
dispatch(getTracks(token!));
}, []);
return (
<div className="flex gap-3 flex-col pt-8">
{tracks.map((track: any) => {
return (
<div className="p-6 border-black border" key={track._id}>
<p>Song Name: {track.track.name}</p>
<p>Artist Name: {track.track.album.artist.name}</p>
<p>Datetime: {track.datetime}</p>
</div>
);
})}
</div>
);
};
export default TrackHistory;
import React, {useEffect} from 'react';
import {useLocation} from 'react-router-dom';
import {addTrackHistory} from '../features/track-history/trackHistorySlice';
import {getTracksByQuery} from '../features/track/trackSlice';
import ITrackHistoryDto from '../interfaces/ITrackHistoryDto';
import {useAppDispatch, useAppSelector} from '../store/hooks';
const Tracks: React.FunctionComponent = (): React.ReactElement => {
......@@ -9,11 +11,22 @@ const Tracks: React.FunctionComponent = (): React.ReactElement => {
const dispatch = useAppDispatch();
const params = new URLSearchParams(window.location.search);
const album = params.get('album');
const {userLoggedIn} = useAppSelector((state) => state.user);
useEffect(() => {
dispatch(getTracksByQuery(album!));
}, [dispatch]);
const handleClick = async (trackId: string) => {
const token = window.sessionStorage.getItem('token');
const data: ITrackHistoryDto = {
track: trackId,
token: token!,
};
await dispatch(addTrackHistory(data));
};
return (
<div>
<h2 className="mt-0 mb-2 text-4xl font-medium leading-tight ">Tracks</h2>
......@@ -24,6 +37,9 @@ const Tracks: React.FunctionComponent = (): React.ReactElement => {
tracks.map((track) => {
return (
<div
onClick={() =>
userLoggedIn ? handleClick(track._id) : undefined
}
className="border border-solid color border-black p-4"
key={track._id}
>
......
import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';
import axios from 'axios';
import ITrackHistoryDto from '../../interfaces/ITrackHistoryDto';
import ITrackHistory from '../../interfaces/ITrackHistory';
interface TrackHistoryState {
track: string;
loading: boolean;
tracks: ITrackHistory[];
}
const initialState: TrackHistoryState = {
track: '',
loading: false,
tracks: [],
};
export const addTrackHistory = createAsyncThunk(
'addTrackHistory',
async (data: ITrackHistoryDto) => {
try {
await axios.post(
`${import.meta.env.VITE_MY_URL}/track_history`,
{
track: data.track,
},
{
headers: {
Authorization: `Bearer ${data.token}`,
},
}
);
} catch (err: any) {
console.log(err);
}
}
);
export const getTracks = createAsyncThunk(
'getTracks',
async (token: string) => {
try {
const response = await axios.get(
`${import.meta.env.VITE_MY_URL}/track_history`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
} catch (err: unknown) {
console.log(err);
}
}
);
export const trackHistorySlice = createSlice({
name: 'trackHistory',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(addTrackHistory.pending, (state, action) => {
state.loading = true;
})
.addCase(addTrackHistory.rejected, (state, action) => {
state.loading = false;
})
.addCase(addTrackHistory.fulfilled, (state, action) => {
state.loading = false;
})
.addCase(getTracks.pending, (state, action) => {
state.loading = true;
})
.addCase(getTracks.rejected, (state, action) => {
state.loading = false;
})
.addCase(getTracks.fulfilled, (state, action) => {
state.loading = false;
state.tracks = action.payload;
console.log(action.payload);
});
},
});
export const {} = trackHistorySlice.actions;
export default trackHistorySlice.reducer;
import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import type {PayloadAction} from '@reduxjs/toolkit';
import axios from 'axios';
import IUser from '../../interfaces/IUser';
interface UserState {
user: IUser;
loading: boolean;
userLoggedIn: boolean;
}
const initialState: UserState = {
user: {} as IUser,
loading: false,
userLoggedIn: false,
};
export const createUser = createAsyncThunk(
'createUser',
async (user: IUser) => {
try {
await axios.post(`${import.meta.env.VITE_MY_URL}/users`, {
username: user.username,
password: user.password,
});
} catch (err: any) {
console.log(err);
}
}
);
export const loginUser = createAsyncThunk('loginUser', async (user: IUser) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_MY_URL}/users/sessions`,
{
username: user.username,
password: user.password,
}
);
return response.data;
} catch (err: any) {
throw new Error(err);
}
});
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(createUser.pending, (state, action) => {
state.loading = true;
})
.addCase(createUser.rejected, (state, action) => {
state.loading = false;
})
.addCase(createUser.fulfilled, (state, action) => {
state.loading = false;
})
.addCase(loginUser.pending, (state, action) => {
state.loading = true;
})
.addCase(loginUser.rejected, (state, action) => {
state.loading = false;
console.log(action.error);
state.userLoggedIn = false;
})
.addCase(
loginUser.fulfilled,
(state, {payload}: PayloadAction<{token: string}>) => {
state.loading = false;
state.userLoggedIn = true;
window.sessionStorage.setItem('token', payload.token);
}
);
},
});
export const {} = userSlice.actions;
export default userSlice.reducer;
import ITrack from './ITrack';
import IUser from './IUser';
export default interface ITrackHistory {
_id: string;
user: string;
track: ITrack;
datetime: Date;
}
export default interface ITrackHistoryDto {
track: string;
token: string;
}
export default interface IUser {
username: string;
password: string;
token?: string;
}
import {configureStore} from '@reduxjs/toolkit';
import albumSlice from '../features/album/albumSlice';
import artistSlice from '../features/artist/artistSlice';
import trackHistorySlice from '../features/track-history/trackHistorySlice';
import trackSlice from '../features/track/trackSlice';
import userSlice from '../features/user/userSlice';
export const store = configureStore({
reducer: {
artist: artistSlice,
album: albumSlice,
track: trackSlice,
user: userSlice,
trackHistory: trackHistorySlice,
},
});
......
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