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",
|
"query-string": "^9.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-redux": "^9.1.2",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5556,6 +5559,12 @@
|
|||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.12",
|
"version": "8.5.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
|
||||||
@@ -17203,6 +17212,29 @@
|
|||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
@@ -17393,6 +17425,21 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
|
||||||
@@ -19749,6 +19796,15 @@
|
|||||||
"requires-port": "^1.0.0"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -18,8 +18,11 @@
|
|||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-redux": "^9.1.2",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
35
src/App.js
35
src/App.js
@@ -1,24 +1,35 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import "./styles/App.css";
|
import "./styles/App.css";
|
||||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
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 PosterSelector from "./components/PosterSelector";
|
||||||
import PosterGallery from "./components/PosterGallery";
|
import PosterGallery from "./components/PosterGallery";
|
||||||
import UploadDiary from "./components/UploadDiary";
|
import UploadDiary from "./components/UploadDiary";
|
||||||
|
import GlobalDownloadButton from "./components/DownloadButton";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Provider store={store}>
|
||||||
<div className="App">
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<Routes>
|
{
|
||||||
<Route path="/" element={<UploadDiary />} />
|
<Router>
|
||||||
<Route path="/PosterSelector" element={<PosterSelector />} />
|
<div className="App">
|
||||||
<Route
|
<Routes>
|
||||||
path="/posters/:movieName/:movieYear"
|
<Route path="/" element={<UploadDiary />} />
|
||||||
element={<PosterGallery />}
|
<Route path="/PosterSelector" element={<PosterSelector />} />
|
||||||
/>
|
<Route
|
||||||
</Routes>
|
path="/posters/:movieName/:movieYear"
|
||||||
</div>
|
element={<PosterGallery />}
|
||||||
</Router>
|
/>
|
||||||
|
</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 React, { useState, useEffect } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import { selectPoster, deselectPoster } from "../services/action";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -12,6 +14,7 @@ import {
|
|||||||
makeStyles,
|
makeStyles,
|
||||||
Paper,
|
Paper,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
import CheckIcon from "@material-ui/icons/Check";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -29,11 +32,30 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
fontWeight: "400",
|
fontWeight: "400",
|
||||||
},
|
},
|
||||||
posterCard: {
|
posterCard: {
|
||||||
|
position: "relative",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
boxShadow: "none",
|
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: {
|
posterImage: {
|
||||||
paddingTop: "150%",
|
paddingTop: "150%",
|
||||||
@@ -51,11 +73,15 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const PosterGallery = () => {
|
const PosterGallery = ({ movieId }) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [posters, setPosters] = useState([]);
|
const [posters, setPosters] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { movieName, movieYear } = useParams();
|
const { movieName, movieYear } = useParams();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const selectedPosters = useSelector(
|
||||||
|
(state) => state.posterSelections[movieId] || []
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPosters = async () => {
|
const fetchPosters = async () => {
|
||||||
@@ -75,6 +101,14 @@ const PosterGallery = () => {
|
|||||||
fetchPosters();
|
fetchPosters();
|
||||||
}, [movieName, movieYear]);
|
}, [movieName, movieYear]);
|
||||||
|
|
||||||
|
const handlePosterSelect = (posterId) => {
|
||||||
|
if (selectedPosters.includes(posterId)) {
|
||||||
|
dispatch(deselectPoster(movieId, posterId));
|
||||||
|
} else {
|
||||||
|
dispatch(selectPoster(movieId, posterId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={classes.root}>
|
<Container className={classes.root}>
|
||||||
<Paper elevation={3} className={classes.paper}>
|
<Paper elevation={3} className={classes.paper}>
|
||||||
@@ -89,12 +123,22 @@ const PosterGallery = () => {
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{posters.map((poster, index) => (
|
{posters.map((poster, index) => (
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3} key={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
|
<CardMedia
|
||||||
className={classes.posterImage}
|
className={classes.posterImage}
|
||||||
image={`https://image.tmdb.org/t/p/w500${poster.file_path}`}
|
image={`https://image.tmdb.org/t/p/w500${poster.file_path}`}
|
||||||
title={`${movieName} poster ${index + 1}`}
|
title={`${movieName} poster ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
|
{selectedPosters.includes(poster.file_path) && (
|
||||||
|
<CheckIcon className={classes.checkIcon} />
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</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