Adding Cart page

This commit is contained in:
Pierret Hugo
2024-10-19 14:00:38 +02:00
parent 5516900b1b
commit 129fcb672c
7 changed files with 502 additions and 166 deletions

View File

@@ -7,7 +7,7 @@ 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";
import Cart from "./components/Cart";
function App() {
return (
@@ -23,11 +23,11 @@ function App() {
path="/posters/:movieName/:movieYear"
element={<PosterGallery />}
/>
<Route path="/Cart" element={<Cart />} />
</Routes>
</div>
</Router>
}
<GlobalDownloadButton />
</PersistGate>
</Provider>
);

239
src/components/Cart.js Normal file
View File

@@ -0,0 +1,239 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
Container, Typography, Card, CardMedia, CardContent,
Button, Checkbox, FormControlLabel, TextField, Dialog, DialogTitle, DialogContent, DialogActions,
Paper
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import DeleteIcon from '@material-ui/icons/Delete';
import GetAppIcon from '@material-ui/icons/GetApp';
import ShareIcon from '@material-ui/icons/Share';
import EditIcon from '@material-ui/icons/Edit';
import { removePoster } from '../services/action';
import axios from "axios";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(8),
display: 'flex',
},
posterList: {
flex: 2,
marginRight: theme.spacing(2),
maxHeight: 'calc(100vh - 200px)',
overflowY: 'auto',
},
actionPanel: {
flex: 1,
position: 'sticky',
top: theme.spacing(4),
height: 'fit-content',
},
card: {
display: 'flex',
marginBottom: theme.spacing(2),
backgroundColor: '#2c3e50',
},
cardMedia: {
width: 100,
},
cardContent: {
flex: '1 0 auto',
},
actions: {
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
},
slider: {
width: '100%',
marginTop: theme.spacing(2),
},
button: {
width: '100%',
},
typography: {
color: '#ecf0f1',
},
formControl: {
marginBottom: theme.spacing(2),
},
checkbox: {
color: '#3498db',
},
paper: {
padding: theme.spacing(3),
backgroundColor: '#34495e',
},
}));
const Cart = () => {
const classes = useStyles();
const dispatch = useDispatch();
const selectedPosters = useSelector((state) => {
const selections = state.posterSelections;
return Object.entries(selections).flatMap(([movieId, posters]) =>
posters.map(posterId => ({ movieId, posterId }))
);
});
const [selectedForDownload, setSelectedForDownload] = useState(selectedPosters.map(() => true));
const [downloadFormat, setDownloadFormat] = useState('zip');
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [currentPosterIndex, setCurrentPosterIndex] = useState(null);
const [newPosterName, setNewPosterName] = useState('');
const handleRemovePoster = (index) => {
dispatch(removePoster(selectedPosters[index].movieId, selectedPosters[index].posterId));
};
const handleDownload = async () => {
const postersToDownload = selectedPosters
.filter((_, index) => selectedForDownload[index])
.map((poster) => ({
movieId: poster.movieId,
posterId: poster.posterId,
movieName: poster.movieName,
movieYear: poster.movieYear
}));
console.log('Downloading posters:', postersToDownload);
try {
const response = await axios.post('http://localhost:5000/api/download-posters', {
posters: postersToDownload,
format: downloadFormat,
}, {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `posters.${downloadFormat}`;
link.click();
} catch (error) {
console.error('Failed to download posters:', error);
}
};
const handleShare = () => {
console.log('Sharing selected posters');
};
const handleRename = (index) => {
setCurrentPosterIndex(index);
setNewPosterName(selectedPosters[index].posterId);
setRenameDialogOpen(true);
};
const handleRenameConfirm = () => {
console.log(`Renaming poster ${currentPosterIndex} to ${newPosterName}`);
setRenameDialogOpen(false);
};
return (
<Container className={classes.root}>
<div className={classes.posterList}>
<Typography variant="h4" gutterBottom className={classes.typography}>Your Cart</Typography>
{selectedPosters.map((poster, index) => (
<Card key={index} className={classes.card}>
<CardMedia
className={classes.cardMedia}
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`}
/>
<CardContent className={classes.cardContent}>
<Typography variant="h6"
className={classes.typography}>{`Movie ID: ${poster.movieId}`}</Typography>
<Typography variant="body2"
className={classes.typography}>{poster.posterId}</Typography>
<FormControlLabel
control={
<Checkbox
checked={selectedForDownload[index]}
onChange={(e) => {
const newSelected = [...selectedForDownload];
newSelected[index] = e.target.checked;
setSelectedForDownload(newSelected);
}}
className={classes.checkbox}
/>
}
label="Select for download"
className={classes.typography}
/>
<Button startIcon={<DeleteIcon/>} onClick={() => handleRemovePoster(index)}
className={classes.typography}>Remove</Button>
<Button startIcon={<EditIcon/>} onClick={() => handleRename(index)}
className={classes.typography}>Rename</Button>
</CardContent>
</Card>
))}
</div>
<Paper className={classes.actionPanel} elevation={3}>
<div className={classes.actions}>
<Typography gutterBottom>Download Format</Typography>
<TextField
select
value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value)}
SelectProps={{
native: true,
}}
className={classes.formControl}
>
<option value="zip">ZIP</option>
<option value="tar">TAR</option>
<option value="7z">7Z</option>
</TextField>
<Button
variant="contained"
color="primary"
startIcon={<GetAppIcon/>}
onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)}
className={classes.button}
>
Download Selected
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<ShareIcon/>}
onClick={handleShare}
className={classes.button}
>
Share Selection
</Button>
</div>
</Paper>
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}>
<DialogTitle>Rename Poster</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="New Name"
type="text"
fullWidth
value={newPosterName}
onChange={(e) => setNewPosterName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setRenameDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={handleRenameConfirm} color="primary">
Rename
</Button>
</DialogActions>
</Dialog>
</Container>
);
};
export default Cart;

78
src/components/NavBar.js Normal file
View File

@@ -0,0 +1,78 @@
import React from "react";
import { useSelector } from "react-redux";
import { AppBar, Toolbar, IconButton, Badge, Button } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
import { useNavigate, useLocation } from 'react-router-dom';
const useStyles = makeStyles((theme) => ({
appBar: {
top: 'auto',
bottom: 0,
backgroundColor: '#1c1f23',
},
toolbar: {
justifyContent: 'space-between',
},
backButton: {
color: 'white',
},
cartButton: {
color: 'white',
},
refreshButton: {
color: 'white',
marginRight: theme.spacing(2),
},
}));
const NavBar = ({onRefreshFiles}) => {
const classes = useStyles();
const navigate = useNavigate();
const location = useLocation();
const selectedPosters = useSelector((state) => {
const selections = state.posterSelections;
return Object.values(selections).flat();
});
const totalSelected = selectedPosters.length;
const handleBack = () => {
navigate(-1);
};
const handleCart = () => {
navigate('/Cart');
};
const isPosterSelectorPage = location.pathname !== '/PosterSelector';
return (
<AppBar position="fixed" className={classes.appBar}>
<Toolbar className={classes.toolbar}>
{isPosterSelectorPage ? (
<IconButton edge="start" className={classes.backButton} onClick={handleBack}>
<ArrowBackIcon />
</IconButton>
) : (
<IconButton edge="start" className={classes.backButton} disabled>
{/* Un IconButton vide et désactivé */}
</IconButton>
)}
<div>
<Button className={classes.refreshButton} onClick={onRefreshFiles}>
Refresh Files
</Button>
<IconButton edge="end" className={classes.cartButton} onClick={handleCart}>
<Badge badgeContent={totalSelected} color="secondary">
<ShoppingCartIcon />
</Badge>
</IconButton>
</div>
</Toolbar>
</AppBar>
);
};
export default NavBar;

View File

@@ -15,10 +15,12 @@ import {
Paper,
} from "@material-ui/core";
import CheckIcon from "@material-ui/icons/Check";
import NavBar from "./NavBar";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(4),
},
paper: {
padding: theme.spacing(3),
@@ -83,6 +85,8 @@ const PosterGallery = ({ movieId }) => {
(state) => state.posterSelections[movieId] || []
);
console.log(movieId);
useEffect(() => {
const fetchPosters = async () => {
try {
@@ -110,42 +114,45 @@ const PosterGallery = ({ movieId }) => {
};
return (
<Container className={classes.root}>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h4" gutterBottom className={classes.title}>
Posters for {movieName} ({movieYear})
</Typography>
{loading ? (
<Box className={classes.loadingContainer}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
{posters.map((poster, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<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>
<>
<Container className={classes.root}>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h4" gutterBottom className={classes.title}>
Posters for {movieName} ({movieYear})
</Typography>
{loading ? (
<Box className={classes.loadingContainer}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
{posters.map((poster, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<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>
))}
</Grid>
))}
</Grid>
)}
</Paper>
</Container>
)}
</Paper>
</Container>
<NavBar />
</>
);
};

View File

@@ -17,10 +17,12 @@ import Pagination from "@mui/material/Pagination";
import { createTheme, ThemeProvider } from "@mui/material/styles";
import queryString from "query-string";
import pulpGif from "../static/images/pulp.gif";
import NavBar from "./NavBar";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(8),
},
paper: {
padding: theme.spacing(3),
@@ -37,7 +39,7 @@ const useStyles = makeStyles((theme) => ({
},
list: {
maxHeight: "800px",
minHeight: "1000px",
minHeight: "900px",
},
listItem: {
marginBottom: theme.spacing(2),
@@ -93,7 +95,7 @@ const useStyles = makeStyles((theme) => ({
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: "200px", // Adjust height as needed to keep component size consistent
height: "200px",
},
progressLabel: {
marginTop: theme.spacing(2),
@@ -186,8 +188,9 @@ const PosterSelector = () => {
const parsed = queryString.parse(location.search);
const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
setPage(pageNumber);
fetchMovies(pageNumber);
fetchUsername();
fetchMovies(pageNumber).then(r => console.log(r));
// fetchUsername();
}, [location.search]);
const fetchUsername = async () => {
@@ -231,7 +234,7 @@ const PosterSelector = () => {
navigate(
`/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(
movieYear
)}?page=${page}`
)}`
);
};
@@ -263,131 +266,127 @@ const PosterSelector = () => {
};
return (
<Container className={classes.root}>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h4" gutterBottom className={classes.title}>
Your diary
</Typography>
{username && (
<Typography variant="h6" className={classes.username}>
Letterboxd User: {username}
<>
<Container className={classes.root}>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h4" gutterBottom className={classes.title}>
Your diary
</Typography>
)}
{loading ? (
<Box className={classes.progressContainer}>
<LinearProgress variant="determinate" value={progress} />
<Typography className={classes.progressLabel}>
Downloading movies...
</Typography>
</Box>
) : movies.length > 0 ? (
<>
<List className={classes.list}>
{movies.map((movie, index) => (
<ListItem
button
key={index}
onClick={() => handleMovieClick(movie.Name, movie.Year)}
className={classes.listItem}
{username && (
<Typography variant="h6" className={classes.username}>
Letterboxd User: {username}
</Typography>
)}
{loading ? (
<Box className={classes.progressContainer}>
<LinearProgress variant="determinate" value={progress} />
<Typography className={classes.progressLabel}>
Downloading movies...
</Typography>
</Box>
) : movies.length > 0 ? (
<>
<List className={classes.list}>
{movies.map((movie, index) => (
<ListItem
button
key={index}
onClick={() => handleMovieClick(movie.Name, movie.Year)}
className={classes.listItem}
>
<Grid container alignItems="center">
<Grid item>
<img
src={
`https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` ||
`/api/placeholder/50/75`
}
alt={movie.Name}
className={classes.poster}
/>
</Grid>
<Grid item className={classes.movieInfo}>
<Typography
variant="subtitle1"
className={classes.movieTitle}
>
{movie.Name}
</Typography>
<Typography
variant="body2"
color="textSecondary"
className={classes.movieYear}
>
{movie.Year}
</Typography>
</Grid>
<Grid item>
<Box className={classes.watchedDate}>
{(() => {
const { day, month } = formatWatchedDate(
movie["Watched Date"]
);
return (
<>
<Typography
variant="body2"
className={classes.watchedDay}
>
{day}
</Typography>
<Typography
variant="body2"
className={classes.watchedMonth}
>
{month}
</Typography>
</>
);
})()}
</Box>
</Grid>
</Grid>
</ListItem>
))}
</List>
<Box className={classes.paginationContainer}>
<ThemeProvider theme={paginationTheme}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</ThemeProvider>
</Box>
</>
) : (
<Box className={classes.noMoviesContainer}>
<div className={classes.gifContainer}>
<div className={classes.circleBackground}></div>
<img
src={pulpGif}
alt="Confused reaction"
className={classes.gif}
/>
</div>
<Typography variant="body1" gutterBottom>
No movies found in your diary. Please check your Letterboxd
username or CSV file.
</Typography>
<Button
variant="contained"
className={classes.backButton}
onClick={handleGoBack}
>
<Grid container alignItems="center">
<Grid item>
<img
src={
`https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` ||
`/api/placeholder/50/75`
}
alt={movie.Name}
className={classes.poster}
/>
</Grid>
<Grid item className={classes.movieInfo}>
<Typography
variant="subtitle1"
className={classes.movieTitle}
>
{movie.Name}
</Typography>
<Typography
variant="body2"
color="textSecondary"
className={classes.movieYear}
>
{movie.Year}
</Typography>
</Grid>
<Grid item>
<Box className={classes.watchedDate}>
{(() => {
const { day, month } = formatWatchedDate(
movie["Watched Date"]
);
return (
<>
<Typography
variant="body2"
className={classes.watchedDay}
>
{day}
</Typography>
<Typography
variant="body2"
className={classes.watchedMonth}
>
{month}
</Typography>
</>
);
})()}
</Box>
</Grid>
</Grid>
</ListItem>
))}
</List>
<Box className={classes.paginationContainer}>
<ThemeProvider theme={paginationTheme}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</ThemeProvider>
</Box>
<Button
variant="contained"
className={classes.refreshButton}
onClick={handleRefreshFiles}
>
Refresh File
</Button>
</>
) : (
<Box className={classes.noMoviesContainer}>
<div className={classes.gifContainer}>
<div className={classes.circleBackground}></div>
<img
src={pulpGif}
alt="Confused reaction"
className={classes.gif}
/>
</div>
<Typography variant="body1" gutterBottom>
No movies found in your diary. Please check your Letterboxd
username or CSV file.
</Typography>
<Button
variant="contained"
className={classes.backButton}
onClick={handleGoBack}
>
Go Back
</Button>
</Box>
)}
</Paper>
</Container>
Go Back
</Button>
</Box>
)}
</Paper>
</Container>
<NavBar onRefreshFiles={handleRefreshFiles} />
</>
);
};

View File

@@ -9,3 +9,9 @@ export const deselectPoster = (movieId, posterId) => ({
movieId,
posterId,
});
export const removePoster = (movieId, posterId) => ({
type: "REMOVE_POSTER",
movieId,
posterId,
});

View File

@@ -13,7 +13,14 @@ const posterSelectionReducer = (state = {}, action) => {
return {
...state,
[action.movieId]: state[action.movieId].filter(
(id) => id !== action.posterId
(id) => id !== action.posterId
),
};
case "REMOVE_POSTER":
return {
...state,
[action.movieId]: state[action.movieId].filter(
(id) => id !== action.posterId
),
};
default: