Adding redux

This commit is contained in:
2024-09-22 21:55:57 +02:00
parent 9ee066b10c
commit 5516900b1b
7 changed files with 221 additions and 14 deletions

56
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

View File

@@ -1,12 +1,19 @@
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 (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{
<Router> <Router>
<div className="App"> <div className="App">
<Routes> <Routes>
@@ -19,6 +26,10 @@ function App() {
</Routes> </Routes>
</div> </div>
</Router> </Router>
}
<GlobalDownloadButton />
</PersistGate>
</Provider>
); );
} }

View 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;

View File

@@ -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
View 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
View 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);