Improve session managment and cart section
This commit is contained in:
@@ -1,12 +1,24 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import {removeAllPosters, removePoster} from "../services/action";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { removeAllPosters, removePoster } from "../services/action";
|
||||||
import DeleteIcon from "@material-ui/icons/Delete";
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogActions,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL;
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
const Cart = () => {
|
const Cart = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const selectedPosters = useSelector((state) => {
|
const selectedPosters = useSelector((state) => {
|
||||||
const selections = state.posterSelections;
|
const selections = state.posterSelections;
|
||||||
return Object.entries(selections).flatMap(([movieId, posters]) =>
|
return Object.entries(selections).flatMap(([movieId, posters]) =>
|
||||||
@@ -20,14 +32,28 @@ const Cart = () => {
|
|||||||
|
|
||||||
const [downloadFormat, setDownloadFormat] = useState("zip");
|
const [downloadFormat, setDownloadFormat] = useState("zip");
|
||||||
|
|
||||||
|
// Nouvel état pour la confirmation de suppression globale
|
||||||
|
const [openConfirmClear, setOpenConfirmClear] = useState(false);
|
||||||
|
|
||||||
const handleRemovePoster = (movieId, posterId) => {
|
const handleRemovePoster = (movieId, posterId) => {
|
||||||
if (movieId && posterId) dispatch(removePoster(movieId, posterId));
|
if (movieId && posterId) dispatch(removePoster(movieId, posterId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAllPosters = () => {
|
// Ouvre la dialog de confirmation (appelé par le bouton "Clear All Posters")
|
||||||
dispatch(removeAllPosters());
|
const handleOpenConfirmClear = () => {
|
||||||
|
setOpenConfirmClear(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ferme la dialog sans supprimer
|
||||||
|
const handleCloseConfirmClear = () => {
|
||||||
|
setOpenConfirmClear(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Confirme et supprime tous les posters
|
||||||
|
const handleConfirmClearAllPosters = () => {
|
||||||
|
dispatch(removeAllPosters());
|
||||||
|
setOpenConfirmClear(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +93,19 @@ const Cart = () => {
|
|||||||
<div className="cart-container">
|
<div className="cart-container">
|
||||||
<div className="poster-grid">
|
<div className="poster-grid">
|
||||||
{selectedPosters.length === 0 ? (
|
{selectedPosters.length === 0 ? (
|
||||||
<p>No posters selected</p>
|
<Box className="empty-cart-container">
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
Your poster collection is empty. Start exploring movies to add
|
||||||
|
some posters!
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className="back-button"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
>
|
||||||
|
Browse Movies
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
selectedPosters.map((poster) => (
|
selectedPosters.map((poster) => (
|
||||||
<div
|
<div
|
||||||
@@ -82,9 +120,12 @@ const Cart = () => {
|
|||||||
<div className="delete-overlay">
|
<div className="delete-overlay">
|
||||||
<button
|
<button
|
||||||
className="delete-button"
|
className="delete-button"
|
||||||
onClick={() => handleRemovePoster(poster.movieId, poster.posterId)}
|
onClick={() =>
|
||||||
|
handleRemovePoster(poster.movieId, poster.posterId)
|
||||||
|
}
|
||||||
|
aria-label="Supprimer ce poster"
|
||||||
>
|
>
|
||||||
<DeleteIcon/>
|
<DeleteIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,12 +153,40 @@ const Cart = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="clear-button"
|
className="clear-button"
|
||||||
onClick={handleRemoveAllPosters}
|
onClick={handleOpenConfirmClear}
|
||||||
disabled={selectedPosters.length === 0}
|
disabled={selectedPosters.length === 0}
|
||||||
|
aria-haspopup="dialog"
|
||||||
>
|
>
|
||||||
Clear All Posters
|
Clear All Posters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={openConfirmClear}
|
||||||
|
onClose={handleCloseConfirmClear}
|
||||||
|
aria-labelledby="confirm-clear-title"
|
||||||
|
aria-describedby="confirm-clear-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="confirm-clear-title">Confirm deletion</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="confirm-clear-description">
|
||||||
|
Are you sure you want to delete all the posters in your collection?
|
||||||
|
This action cannot be undone.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseConfirmClear} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirmClearAllPosters}
|
||||||
|
color="secondary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Delete all
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import axios from "axios";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { AppBar, Toolbar, IconButton, Badge, Button } from '@material-ui/core';
|
import { AppBar, Toolbar, IconButton, Badge, Button } from "@material-ui/core";
|
||||||
import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
|
import ShoppingCartIcon from "@material-ui/icons/ShoppingCart";
|
||||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
const selectPosterSelections = (state) => state.posterSelections;
|
const selectPosterSelections = (state) => state.posterSelections;
|
||||||
|
|
||||||
const NavBar = ({ onRefreshFiles }) => {
|
const NavBar = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -23,43 +26,37 @@ const NavBar = ({ onRefreshFiles }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCart = () => {
|
const handleCart = () => {
|
||||||
navigate('/Cart');
|
navigate("/Cart");
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPosterSelectorPage = location.pathname !== '/PosterSelector';
|
const handleResetprofile = async () => {
|
||||||
|
try {
|
||||||
|
await axios.delete(`${apiUrl}/api/delete-csv`, { withCredentials: true });
|
||||||
|
navigate("/");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting CSV file:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPosterSelectorPage = location.pathname !== "/PosterSelector";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="fixed" className="navbar">
|
<AppBar position="fixed" className="navbar">
|
||||||
<Toolbar className="toolbar">
|
<Toolbar className="toolbar">
|
||||||
{isPosterSelectorPage ? (
|
{isPosterSelectorPage ? (
|
||||||
<IconButton
|
<IconButton edge="start" className="back-button" onClick={handleBack}>
|
||||||
edge="start"
|
|
||||||
className="back-button"
|
|
||||||
onClick={handleBack}
|
|
||||||
>
|
|
||||||
<ArrowBackIcon />
|
<ArrowBackIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) : (
|
) : (
|
||||||
<IconButton
|
<IconButton edge="start" className="back-button" disabled>
|
||||||
edge="start"
|
|
||||||
className="back-button"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{/* Disabled IconButton */}
|
{/* Disabled IconButton */}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button className="refresh-button" onClick={handleResetprofile}>
|
||||||
className="refresh-button"
|
Reset Profile
|
||||||
onClick={onRefreshFiles}
|
|
||||||
>
|
|
||||||
Refresh Files
|
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
<IconButton edge="end" className="cart-button" onClick={handleCart}>
|
||||||
edge="end"
|
|
||||||
className="cart-button"
|
|
||||||
onClick={handleCart}
|
|
||||||
>
|
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={totalSelected}
|
badgeContent={totalSelected}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import {
|
|||||||
CardMedia,
|
CardMedia,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Box,
|
Box,
|
||||||
Paper, DialogContent, DialogTitle, DialogActions, Button,
|
Paper,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import CheckIcon from "@material-ui/icons/Check";
|
import CheckIcon from "@material-ui/icons/Check";
|
||||||
import NavBar from "./NavBar";
|
import NavBar from "./NavBar";
|
||||||
import {Dialog} from "@mui/material";
|
import { Dialog } from "@mui/material";
|
||||||
import matchers from "@testing-library/jest-dom/matchers";
|
|
||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL;
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
@@ -66,18 +69,18 @@ const PosterGallery = () => {
|
|||||||
}
|
}
|
||||||
}, [movieName, movieYear]);
|
}, [movieName, movieYear]);
|
||||||
|
|
||||||
|
const isPosterSelected = (posterId) => {
|
||||||
|
return selectedPosters.some((poster) => poster.posterId === posterId);
|
||||||
|
};
|
||||||
|
|
||||||
const handlePosterSelect = (posterId) => {
|
const handlePosterSelect = (posterId) => {
|
||||||
const postersForCurrentMovie = selectedPosters.filter(
|
const isSelected = isPosterSelected(posterId);
|
||||||
(poster) => poster.movieId !== movieId
|
|
||||||
);
|
|
||||||
|
|
||||||
const isPosterSelected = postersForCurrentMovie.some((poster) => poster.posterId === posterId)
|
if (selectedPosters.length > 0 && !isSelected) {
|
||||||
|
|
||||||
if (postersForCurrentMovie.length > 0 && !isPosterSelected) {
|
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPosterSelected) {
|
if (isSelected) {
|
||||||
dispatch(deselectPoster(movieName, movieYear, posterId));
|
dispatch(deselectPoster(movieName, movieYear, posterId));
|
||||||
} else {
|
} else {
|
||||||
dispatch(selectPoster(movieName, movieYear, posterId, watchedDate));
|
dispatch(selectPoster(movieName, movieYear, posterId, watchedDate));
|
||||||
@@ -86,7 +89,7 @@ const PosterGallery = () => {
|
|||||||
|
|
||||||
const handleDialogClose = () => {
|
const handleDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@@ -114,13 +117,13 @@ const PosterGallery = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
) : posters.length > 0 ? (
|
) : posters.length > 0 ? (
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{posters.map((poster, index) => (
|
{posters.map((poster, index) => {
|
||||||
|
const isSelected = isPosterSelected(poster.file_path);
|
||||||
|
return (
|
||||||
<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
|
<Card
|
||||||
className={`poster-card ${
|
className={`poster-card ${
|
||||||
selectedPosters.includes(poster.file_path)
|
isSelected ? "selected-poster" : ""
|
||||||
? "selected-poster"
|
|
||||||
: ""
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handlePosterSelect(poster.file_path)}
|
onClick={() => handlePosterSelect(poster.file_path)}
|
||||||
>
|
>
|
||||||
@@ -129,12 +132,11 @@ const PosterGallery = () => {
|
|||||||
image={`https://image.tmdb.org/t/p/original${poster.file_path}`}
|
image={`https://image.tmdb.org/t/p/original${poster.file_path}`}
|
||||||
title={`${movieName} poster ${index + 1}`}
|
title={`${movieName} poster ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
{selectedPosters.includes(poster.file_path) && (
|
{isSelected && <CheckIcon className="check-icon" />}
|
||||||
<CheckIcon className="check-icon" />
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Typography align="center">
|
<Typography align="center">
|
||||||
@@ -148,7 +150,8 @@ const PosterGallery = () => {
|
|||||||
<DialogTitle>Poster already selected</DialogTitle>
|
<DialogTitle>Poster already selected</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Typography>
|
<Typography>
|
||||||
You can only select one poster per movie. Please deselect the current poster first.
|
You can only select one poster per movie. Please deselect the
|
||||||
|
current poster first.
|
||||||
</Typography>
|
</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const PosterSelector = () => {
|
|||||||
const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
|
const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
|
||||||
setPage(pageNumber);
|
setPage(pageNumber);
|
||||||
|
|
||||||
fetchMovies(pageNumber).then((r) => console.log(r));
|
fetchMovies(pageNumber);
|
||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
|
|
||||||
// const fetchUsername = async () => {
|
// const fetchUsername = async () => {
|
||||||
@@ -86,14 +86,7 @@ const PosterSelector = () => {
|
|||||||
|
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${apiUrl}/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
|
`${apiUrl}/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
|
||||||
{
|
{ withCredentials: true }
|
||||||
// onDownloadProgress: (progressEvent) => {
|
|
||||||
// const percentCompleted = Math.round(
|
|
||||||
// (progressEvent.loaded * 100) / progressEvent.total
|
|
||||||
// );
|
|
||||||
// setProgress(percentCompleted);
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data.movies) {
|
if (response.data.movies) {
|
||||||
@@ -109,20 +102,13 @@ const PosterSelector = () => {
|
|||||||
|
|
||||||
const handleMovieClick = (movieName, movieYear, watchedDate) => {
|
const handleMovieClick = (movieName, movieYear, watchedDate) => {
|
||||||
navigate(
|
navigate(
|
||||||
`/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(movieYear)}`,
|
`/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(
|
||||||
{state: {watchedDate}}
|
movieYear
|
||||||
|
)}`,
|
||||||
|
{ state: { watchedDate } }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshFiles = async () => {
|
|
||||||
try {
|
|
||||||
await axios.delete(`${apiUrl}/api/delete-csv`);
|
|
||||||
navigate("/");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting CSV file:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageChange = (event, value) => {
|
const handlePageChange = (event, value) => {
|
||||||
setPage(value);
|
setPage(value);
|
||||||
navigate(`?page=${value}`);
|
navigate(`?page=${value}`);
|
||||||
@@ -169,14 +155,20 @@ const PosterSelector = () => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
button
|
button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => handleMovieClick(movie.Name, movie.Year, movie["Watched Date"])}
|
onClick={() =>
|
||||||
|
handleMovieClick(
|
||||||
|
movie.Name,
|
||||||
|
movie.Year,
|
||||||
|
movie["Watched Date"]
|
||||||
|
)
|
||||||
|
}
|
||||||
className="movie-item"
|
className="movie-item"
|
||||||
>
|
>
|
||||||
<Grid container alignItems="center">
|
<Grid container alignItems="center">
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
`https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` ||
|
`https://image.tmdb.org/t/p/w500${movie.Poster}` ||
|
||||||
`/api/placeholder/50/75`
|
`/api/placeholder/50/75`
|
||||||
}
|
}
|
||||||
alt={movie.Name}
|
alt={movie.Name}
|
||||||
@@ -256,7 +248,7 @@ const PosterSelector = () => {
|
|||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Container>
|
</Container>
|
||||||
<NavBar onRefreshFiles={handleRefreshFiles} />
|
<NavBar />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ const UploadDiary = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkCSVFile = async () => {
|
const checkCSVFile = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${apiUrl}/api/check-csv`);
|
const response = await axios.get(`${apiUrl}/api/check-csv`, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
if (response.data.fileExists) {
|
if (response.data.fileExists) {
|
||||||
navigate("/PosterSelector");
|
navigate("/PosterSelector");
|
||||||
}
|
}
|
||||||
@@ -49,16 +51,26 @@ const UploadDiary = () => {
|
|||||||
try {
|
try {
|
||||||
if (username) {
|
if (username) {
|
||||||
console.log(`Fetching diary for user: ${username}`);
|
console.log(`Fetching diary for user: ${username}`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
// Call backend to fetch diary from username
|
||||||
|
const response = await axios.post(
|
||||||
|
`${apiUrl}/api/fetch-diary`,
|
||||||
|
{ username },
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
navigate("/PosterSelector");
|
navigate("/PosterSelector");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch diary", response.data);
|
||||||
|
}
|
||||||
} else if (file) {
|
} else if (file) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
||||||
await axios.post(`${apiUrl}/api/upload-csv`, formData, {
|
await axios.post(`${apiUrl}/api/upload-csv`, formData, {
|
||||||
headers: {
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
"Content-Type": "multipart/form-data",
|
withCredentials: true,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate("/PosterSelector");
|
navigate("/PosterSelector");
|
||||||
@@ -84,6 +96,9 @@ const UploadDiary = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key === "Enter") handleUpload();
|
||||||
|
}}
|
||||||
placeholder="Enter your Letterboxd username"
|
placeholder="Enter your Letterboxd username"
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" className="or-text">
|
<Typography variant="body2" className="or-text">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const posterSelectionReducer = (state = {}, action) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[movieId]: state[movieId]?.filter(
|
[movieId]: state[movieId]?.filter(
|
||||||
(id) => id !== posterId
|
(poster) => poster.posterId !== posterId
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ const posterSelectionReducer = (state = {}, action) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[movieId]: state[movieId]?.filter(
|
[movieId]: state[movieId]?.filter(
|
||||||
(id) => id !== posterId
|
(poster) => poster.posterId !== posterId
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,22 @@
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.poster-grid {
|
.poster-grid {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
max-height: calc(100vh - 4rem);
|
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
.poster-card {
|
.poster-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 2/3;
|
aspect-ratio: 2/3;
|
||||||
|
min-height: 220px;
|
||||||
|
|
||||||
&:hover .delete-overlay {
|
&:hover .delete-overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -23,6 +27,9 @@
|
|||||||
.poster-image {
|
.poster-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-overlay {
|
.delete-overlay {
|
||||||
@@ -34,56 +41,86 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
.delete-button {
|
.delete-button {
|
||||||
background-color: #e74c3c;
|
background-color: #e74c3c;
|
||||||
border: none;
|
border: none;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5rem;
|
padding: 0.65rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
transform: scale(1.1);
|
&:focus {
|
||||||
|
transform: scale(1.05);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-poster {
|
// État vide simple
|
||||||
width: 100%;
|
.empty-cart-container {
|
||||||
height: 100%;
|
grid-column: 1 / -1;
|
||||||
background-color: #f5f6fa10;
|
|
||||||
border: 2px #95a5a6;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #7f8c8d;
|
min-height: 60vh;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
svg {
|
.MuiTypography-body1 {
|
||||||
margin-bottom: 1rem;
|
color: #7f8c8d;
|
||||||
color: #95a5a6;
|
margin-bottom: 2rem;
|
||||||
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.back-button {
|
||||||
font-size: 0.875rem;
|
color: #fff;
|
||||||
text-align: center;
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
background: #526e89;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
min-width: 160px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #1caff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-panel {
|
.action-panel {
|
||||||
width: 300px;
|
flex: 0 0 320px;
|
||||||
|
max-width: 320px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 2rem;
|
top: 2rem;
|
||||||
background-color: #34495e;
|
background-color: #34495e;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: white;
|
color: white;
|
||||||
@@ -94,32 +131,181 @@
|
|||||||
.format-select {
|
.format-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
padding: 0.5rem;
|
padding: 0.6rem;
|
||||||
background-color: #2c3e50;
|
background-color: #2c3e50;
|
||||||
border: 1px solid #95a5a6;
|
border: 1px solid #95a5a6;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-button, .clear-button {
|
.download-button,
|
||||||
|
.clear-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.85rem;
|
||||||
background-color: #00a346;
|
background-color: #00a346;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease, transform 0.08s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
|
||||||
&:hover {
|
&:active {
|
||||||
background-color: darken(#00a346, 10%);
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
filter: brightness(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background-color: darken(#00a346, 20%);
|
background-color: #7f8c8d;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tablette */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.cart-container {
|
||||||
|
padding: 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.poster-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
max-height: calc(100vh - 3rem);
|
||||||
|
|
||||||
|
.poster-card {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-panel {
|
||||||
|
width: 260px;
|
||||||
|
top: 1.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small devices */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cart-container {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.poster-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-right: 0;
|
||||||
|
max-height: none;
|
||||||
|
|
||||||
|
.poster-card {
|
||||||
|
aspect-ratio: 2/3;
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cart-container {
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-panel {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.cart-container {
|
||||||
|
padding: 0.75rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
.poster-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.6rem;
|
||||||
|
|
||||||
|
.poster-card {
|
||||||
|
aspect-ratio: 2/3;
|
||||||
|
min-height: 260px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.poster-image {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-overlay .delete-button {
|
||||||
|
padding: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cart-container {
|
||||||
|
padding: 1rem 0;
|
||||||
|
min-height: 40vh;
|
||||||
|
|
||||||
|
.MuiTypography-body1 {
|
||||||
|
max-width: 320px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
min-width: 140px;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-panel {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.9rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-select {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button,
|
||||||
|
.clear-button {
|
||||||
|
padding: 0.85rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus states (accessibility) */
|
||||||
|
.poster-card .delete-button:focus,
|
||||||
|
.action-panel .download-button:focus,
|
||||||
|
.action-panel .clear-button:focus,
|
||||||
|
.action-panel .format-select:focus,
|
||||||
|
.empty-cart-container .back-button:focus {
|
||||||
|
outline: 3px solid rgba(255, 255, 255, 0.12);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user