Skip to content

Commit

Permalink
feat: history and playlist management
Browse files Browse the repository at this point in the history
  • Loading branch information
jalajcodes committed Apr 6, 2022
1 parent 685e9e8 commit 184fff8
Show file tree
Hide file tree
Showing 20 changed files with 845 additions and 23 deletions.
12 changes: 11 additions & 1 deletion frontend/src/components/AppProviders.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { ReactQueryDevtools } from "react-query/devtools";
import SnackbarProvider from "react-simple-snackbar";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";
import { HistoryProvider } from "../context/historyContext";
import { MenuItemsProvider } from "../context/menuItemsContext";
import { PlaylistProvider } from "../context/playlistContext";
import { ModalProvider } from "../context/modalContext";

const queryClient = new QueryClient({
queries: {
Expand All @@ -28,7 +32,13 @@ function AppProviders({ children }) {
<ThemeProvider theme={darkTheme}>
<GlobalStyles />
<ReactQueryDevtools />
{children}
<HistoryProvider>
<PlaylistProvider>
<ModalProvider>
<MenuItemsProvider>{children}</MenuItemsProvider>
</ModalProvider>
</PlaylistProvider>
</HistoryProvider>
</ThemeProvider>
</SnackbarProvider>
</Router>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/GoogleAuth.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Button from "../styles/Auth";
import { ButtonGhost } from "../styles/Button";
import { SignInIcon } from "./Icons";

function GoogleAuth() {
return (
<Button tabIndex={0} type="button">
<ButtonGhost tabIndex={0} type="button">
<span className="outer">
<span className="inner">
<SignInIcon />
</span>
sign in
</span>
</Button>
</ButtonGhost>
);
}

Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/PlaylistModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import { useModal } from "../context/modalContext";
import { usePlaylist } from "../context/playlistContext";
import { ButtonGhost } from "../styles/Button";
import Wrapper from "../styles/PlaylistModal";
import { CloseIcon } from "./Icons";

function PlaylistModal({ video }) {
const { playlistData } = usePlaylist();
const {
toggleModal,
modalData,
handlePlaylistFormSubmit,
handlePlaylistsCheckboxClick,
} = useModal();

return (
<Wrapper showModal={modalData.showModal}>
<div className={`create-playlist`}>
<form onSubmit={handlePlaylistFormSubmit}>
<div className="modal-header">
<h3>
<span>Create New Playlist</span>
<CloseIcon onClick={toggleModal} />
</h3>
</div>

<input
type="text"
placeholder="Enter playlist name"
id="playlistname"
autoComplete="off"
required
/>
<ButtonGhost type="submit">Create</ButtonGhost>
</form>

{playlistData.length > 0 && (
<div className="playlists">
<h3>
<span>Playlists</span>
</h3>
<ul>
{playlistData.map((p) => (
<li key={p.name}>
<label htmlFor={p.name}>
<input
id={p.name}
value={p.name}
type="checkbox"
onChange={handlePlaylistsCheckboxClick}
checked={modalData.selectedPlaylists.includes(p.name)}
/>
{p.name}
</label>
</li>
))}
</ul>
</div>
)}
</div>
</Wrapper>
);
}

export default PlaylistModal;
18 changes: 10 additions & 8 deletions frontend/src/components/VideoCard.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import { useMenuItems } from "../context/menuItemsContext";
import useClickOutside from "../hooks/useClickOutside";
import Wrapper from "../styles/VideoCard";
import { IMAGE_BASE_URL, POSTER_SIZE } from "../utils/tmdb";
import { DislikeIcon, LikeIcon, MenuIcon, SubIcon } from "./Icons";
import { LikeIcon, MenuIcon } from "./Icons";

const VideoCard = ({ details }) => {
const VideoCard = ({ details, page }) => {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef();
const { getMenuItems } = useMenuItems();
const menuItems = getMenuItems(page);

useClickOutside(ref, () => setIsOpen(false));

Expand All @@ -33,12 +36,11 @@ const VideoCard = ({ details }) => {
{isOpen && (
<div className="video-menu" ref={ref}>
<ul>
<li>
<SubIcon /> Add to Playlist
</li>
<li>
<SubIcon /> Save to watch later
</li>
{menuItems.map((item, idx) => (
<li onClick={() => item.onClick(details.id, details)} key={idx}>
{item.icon} {item.name}
</li>
))}
</ul>
</div>
)}
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/context/historyContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createContext, useContext, useReducer } from "react";
import { useSnackbar } from "react-simple-snackbar";
import {
sharedInitialReducerState,
sharedReducer,
} from "../reducer/sharedReducer";

const HistoryContext = createContext({
historyData: { ...sharedInitialReducerState },
dispatch: () => {},
addToHistory: () => {},
deleteHistory: () => {},
deleteFromHistory: () => {},
});

const HistoryProvider = ({ children }) => {
const [state, dispatch] = useReducer(
sharedReducer,
sharedInitialReducerState
);

const [showSnackbar] = useSnackbar({ position: "bottom-right" });

const addToHistory = (video) => {
const currentHistory = state.data.history;
const videoInCurrentHistory = currentHistory.find(
(data) => data.id === video.id
);
if (videoInCurrentHistory) return;
const newStateData = { ...state.data, history: [...currentHistory, video] };
localStorage.setItem("maxVideoUserData", JSON.stringify(newStateData));
dispatch({ type: "ACTION_TYPE_SUCCESS", payload: newStateData });
};

const deleteHistory = () => {
const newStateData = { ...state.data, history: [] };
localStorage.setItem("maxVideoUserData", JSON.stringify(newStateData));
dispatch({ type: "ACTION_TYPE_SUCCESS", payload: newStateData });
showSnackbar("History has been deleted");
};

const deleteFromHistory = (id) => {
if (!id) return;
const newStateData = {
...state.data,
history: state.data.history.filter((video) => video.id !== id),
};
localStorage.setItem("maxVideoUserData", JSON.stringify(newStateData));
dispatch({ type: "ACTION_TYPE_SUCCESS", payload: newStateData });
showSnackbar("Video has been deleted from history");
};

const value = {
historyData: state.data.history,
dispatch,
addToHistory,
deleteHistory,
deleteFromHistory,
};

return (
<HistoryContext.Provider value={value}>{children}</HistoryContext.Provider>
);
};

const useHistory = () => useContext(HistoryContext);

export { HistoryProvider, useHistory };
55 changes: 55 additions & 0 deletions frontend/src/context/menuItemsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createContext, useContext } from "react";
import { DeleteIcon, LibIcon, SubIcon } from "../components/Icons";
import { useHistory } from "./historyContext";
import { useModal } from "./modalContext";

const MenuItemsContext = createContext({
getMenuItems: () => {},
});

const MenuItemsProvider = ({ children }) => {
const { deleteFromHistory } = useHistory();
const { toggleModal } = useModal();

const getMenuItems = (page) => {
let menuItems = [
{
name: "Add to Playlist",
icon: <SubIcon />,
onClick: (_id, video) => toggleModal(video),
},
{
name: "Save to watch later",
icon: <LibIcon />,
onClick: () => console.log("Save to watch later"),
},
];

if (page === "history") {
menuItems = [
...menuItems,
{
name: "Remove from History",
icon: <DeleteIcon />,
onClick: (id) => deleteFromHistory(id),
},
];
}

return menuItems;
};

const value = {
getMenuItems,
};

return (
<MenuItemsContext.Provider value={value}>
{children}
</MenuItemsContext.Provider>
);
};

const useMenuItems = () => useContext(MenuItemsContext);

export { MenuItemsProvider, useMenuItems };
89 changes: 89 additions & 0 deletions frontend/src/context/modalContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { createContext, useContext, useReducer } from "react";
import { useSnackbar } from "react-simple-snackbar";
import PlaylistModal from "../components/PlaylistModal";
import { initialModalState, modalReducer } from "../reducer/modalReducer";
import { usePlaylist } from "./playlistContext";

const ModalContext = createContext({
modalData: { ...initialModalState },
dispatch: () => {},
toggleModal: () => {},
handlePlaylistFormSubmit: () => {},
handlePlaylistsCheckboxClick: () => {},
});

const ModalProvider = ({ children }) => {
const [openSnackbar] = useSnackbar();
const { playlistData, createPlaylist, addToPlaylist, deleteFromPlaylist } =
usePlaylist();
const [modal, dispatch] = useReducer(modalReducer, initialModalState);
const toggleModal = (video) => {
dispatch({ type: "TOGGLE_MODAL", payload: video });

const videoInPlaylists = playlistData.reduce(
(acc, { name, videos }) =>
videos.some(({ id }) => id === video.id) ? [...acc, name] : acc,
[]
);

dispatch({
type: "SET_SELECTED_PLAYLISTS",
payload: videoInPlaylists,
});
};

const handlePlaylistFormSubmit = (event) => {
event.preventDefault();
const name = event.target.elements.playlistname.value;

if (!name.trim()) {
return openSnackbar("Please enter a playlist name");
}

createPlaylist(name, modal.selectedVideo);

event.target.elements.playlistname.value = "";
dispatch({
type: "SET_SELECTED_PLAYLISTS",
payload: [...modal.selectedPlaylists, name],
});
};

const handlePlaylistsCheckboxClick = (event) => {
const { checked, value: name } = event.target;

const newSelectedPlaylists = checked
? [...modal.selectedPlaylists, name]
: modal.selectedPlaylists.filter((playlistName) => playlistName !== name);

dispatch({
type: "SET_SELECTED_PLAYLISTS",
payload: newSelectedPlaylists,
});

if (checked) {
addToPlaylist(name, modal.selectedVideo);
} else {
deleteFromPlaylist(name, modal.selectedVideo);
}
};

const value = {
modalData: modal,
dispatch,
toggleModal,
handlePlaylistFormSubmit,
handlePlaylistsCheckboxClick,
};

return (
<ModalContext.Provider value={value}>
{children}
<PlaylistModal />
</ModalContext.Provider>
);
};

const useModal = () => useContext(ModalContext);

export { ModalProvider, useModal };
Loading

0 comments on commit 184fff8

Please sign in to comment.