Adding redux
This commit is contained in:
56
package-lock.json
generated
56
package-lock.json
generated
@@ -23,8 +23,11 @@
|
||||
"query-string": "^9.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
}
|
||||
},
|
||||
@@ -5556,6 +5559,12 @@
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
||||
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||
@@ -17203,6 +17212,29 @@
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
|
||||
"integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.3",
|
||||
"use-sync-external-store": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25",
|
||||
"react": "^18.0",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||
@@ -17393,6 +17425,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-persist": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
|
||||
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": ">4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
|
||||
@@ -19749,6 +19796,15 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
|
||||
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
"query-string": "^9.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
11
src/App.js
11
src/App.js
@@ -1,12 +1,19 @@
|
||||
import React from "react";
|
||||
import "./styles/App.css";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import { store, persistor } from "./services/store";
|
||||
import PosterSelector from "./components/PosterSelector";
|
||||
import PosterGallery from "./components/PosterGallery";
|
||||
import UploadDiary from "./components/UploadDiary";
|
||||
import GlobalDownloadButton from "./components/DownloadButton";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
{
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Routes>
|
||||
@@ -19,6 +26,10 @@ function App() {
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
}
|
||||
<GlobalDownloadButton />
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
45
src/components/DownloadButton.js
Normal file
45
src/components/DownloadButton.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Fab, makeStyles } from "@material-ui/core";
|
||||
import GetAppIcon from "@material-ui/icons/GetApp";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
downloadFab: {
|
||||
backgroundColor: "#1caff2",
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(4),
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 1000,
|
||||
},
|
||||
}));
|
||||
|
||||
const GlobalDownloadButton = () => {
|
||||
const classes = useStyles();
|
||||
const selectedPosters = useSelector((state) => {
|
||||
const selections = state.posterSelections;
|
||||
return Object.values(selections).flat();
|
||||
});
|
||||
|
||||
const totalSelected = selectedPosters.length;
|
||||
|
||||
if (totalSelected === 0) return null;
|
||||
|
||||
const handleDownload = () => {
|
||||
// Implement your download logic here
|
||||
console.log("Downloading", totalSelected, "posters");
|
||||
};
|
||||
|
||||
return (
|
||||
<Fab
|
||||
variant="extended"
|
||||
className={classes.downloadFab}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<GetAppIcon />
|
||||
Download ({totalSelected})
|
||||
</Fab>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalDownloadButton;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { selectPoster, deselectPoster } from "../services/action";
|
||||
import {
|
||||
Container,
|
||||
Typography,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
makeStyles,
|
||||
Paper,
|
||||
} from "@material-ui/core";
|
||||
import CheckIcon from "@material-ui/icons/Check";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -29,11 +32,30 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontWeight: "400",
|
||||
},
|
||||
posterCard: {
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
},
|
||||
selectedPoster: {
|
||||
border: `4px solid ${theme.palette.primary.main}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
checkIcon: {
|
||||
position: "absolute",
|
||||
top: theme.spacing(1),
|
||||
right: theme.spacing(1),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.common.white,
|
||||
borderRadius: "50%",
|
||||
padding: theme.spacing(0.5),
|
||||
},
|
||||
posterImage: {
|
||||
paddingTop: "150%",
|
||||
@@ -51,11 +73,15 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const PosterGallery = () => {
|
||||
const PosterGallery = ({ movieId }) => {
|
||||
const classes = useStyles();
|
||||
const [posters, setPosters] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { movieName, movieYear } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const selectedPosters = useSelector(
|
||||
(state) => state.posterSelections[movieId] || []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosters = async () => {
|
||||
@@ -75,6 +101,14 @@ const PosterGallery = () => {
|
||||
fetchPosters();
|
||||
}, [movieName, movieYear]);
|
||||
|
||||
const handlePosterSelect = (posterId) => {
|
||||
if (selectedPosters.includes(posterId)) {
|
||||
dispatch(deselectPoster(movieId, posterId));
|
||||
} else {
|
||||
dispatch(selectPoster(movieId, posterId));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<Paper elevation={3} className={classes.paper}>
|
||||
@@ -89,12 +123,22 @@ const PosterGallery = () => {
|
||||
<Grid container spacing={3}>
|
||||
{posters.map((poster, index) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
|
||||
<Card className={classes.posterCard}>
|
||||
<Card
|
||||
className={`${classes.posterCard} ${
|
||||
selectedPosters.includes(poster.file_path)
|
||||
? classes.selectedPoster
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handlePosterSelect(poster.file_path)}
|
||||
>
|
||||
<CardMedia
|
||||
className={classes.posterImage}
|
||||
image={`https://image.tmdb.org/t/p/w500${poster.file_path}`}
|
||||
title={`${movieName} poster ${index + 1}`}
|
||||
/>
|
||||
{selectedPosters.includes(poster.file_path) && (
|
||||
<CheckIcon className={classes.checkIcon} />
|
||||
)}
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
11
src/services/action.js
Normal file
11
src/services/action.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const selectPoster = (movieId, posterId) => ({
|
||||
type: "SELECT_POSTER",
|
||||
movieId,
|
||||
posterId,
|
||||
});
|
||||
|
||||
export const deselectPoster = (movieId, posterId) => ({
|
||||
type: "DESELECT_POSTER",
|
||||
movieId,
|
||||
posterId,
|
||||
});
|
||||
37
src/services/store.js
Normal file
37
src/services/store.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { legacy_createStore as createStore, combineReducers } from "redux";
|
||||
import { persistStore, persistReducer } from "redux-persist";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
|
||||
const posterSelectionReducer = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case "SELECT_POSTER":
|
||||
return {
|
||||
...state,
|
||||
[action.movieId]: [...(state[action.movieId] || []), action.posterId],
|
||||
};
|
||||
case "DESELECT_POSTER":
|
||||
return {
|
||||
...state,
|
||||
[action.movieId]: state[action.movieId].filter(
|
||||
(id) => id !== action.posterId
|
||||
),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
posterSelections: posterSelectionReducer,
|
||||
// other reducers...
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
export const store = createStore(persistedReducer);
|
||||
export const persistor = persistStore(store);
|
||||
Reference in New Issue
Block a user