Improve session managment and cart section
This commit is contained in:
@@ -1,12 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { removeAllPosters, removePoster } from "../services/action";
|
||||
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 Cart = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const selectedPosters = useSelector((state) => {
|
||||
const selections = state.posterSelections;
|
||||
return Object.entries(selections).flatMap(([movieId, posters]) =>
|
||||
@@ -20,14 +32,28 @@ const Cart = () => {
|
||||
|
||||
const [downloadFormat, setDownloadFormat] = useState("zip");
|
||||
|
||||
// Nouvel état pour la confirmation de suppression globale
|
||||
const [openConfirmClear, setOpenConfirmClear] = useState(false);
|
||||
|
||||
const handleRemovePoster = (movieId, posterId) => {
|
||||
if (movieId && posterId) dispatch(removePoster(movieId, posterId));
|
||||
};
|
||||
|
||||
const handleRemoveAllPosters = () => {
|
||||
dispatch(removeAllPosters());
|
||||
// Ouvre la dialog de confirmation (appelé par le bouton "Clear All Posters")
|
||||
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 () => {
|
||||
try {
|
||||
@@ -67,7 +93,19 @@ const Cart = () => {
|
||||
<div className="cart-container">
|
||||
<div className="poster-grid">
|
||||
{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) => (
|
||||
<div
|
||||
@@ -82,7 +120,10 @@ const Cart = () => {
|
||||
<div className="delete-overlay">
|
||||
<button
|
||||
className="delete-button"
|
||||
onClick={() => handleRemovePoster(poster.movieId, poster.posterId)}
|
||||
onClick={() =>
|
||||
handleRemovePoster(poster.movieId, poster.posterId)
|
||||
}
|
||||
aria-label="Supprimer ce poster"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
@@ -112,12 +153,40 @@ const Cart = () => {
|
||||
</button>
|
||||
<button
|
||||
className="clear-button"
|
||||
onClick={handleRemoveAllPosters}
|
||||
onClick={handleOpenConfirmClear}
|
||||
disabled={selectedPosters.length === 0}
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
Clear All Posters
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { useMemo } from "react";
|
||||
import axios from "axios";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppBar, Toolbar, IconButton, Badge, Button } from '@material-ui/core';
|
||||
import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { AppBar, Toolbar, IconButton, Badge, Button } from "@material-ui/core";
|
||||
import ShoppingCartIcon from "@material-ui/icons/ShoppingCart";
|
||||
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_API_URL;
|
||||
|
||||
const selectPosterSelections = (state) => state.posterSelections;
|
||||
|
||||
const NavBar = ({ onRefreshFiles }) => {
|
||||
const NavBar = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -23,43 +26,37 @@ const NavBar = ({ onRefreshFiles }) => {
|
||||
};
|
||||
|
||||
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 (
|
||||
<AppBar position="fixed" className="navbar">
|
||||
<Toolbar className="toolbar">
|
||||
{isPosterSelectorPage ? (
|
||||
<IconButton
|
||||
edge="start"
|
||||
className="back-button"
|
||||
onClick={handleBack}
|
||||
>
|
||||
<IconButton edge="start" className="back-button" onClick={handleBack}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
edge="start"
|
||||
className="back-button"
|
||||
disabled
|
||||
>
|
||||
<IconButton edge="start" className="back-button" disabled>
|
||||
{/* Disabled IconButton */}
|
||||
</IconButton>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
className="refresh-button"
|
||||
onClick={onRefreshFiles}
|
||||
>
|
||||
Refresh Files
|
||||
<Button className="refresh-button" onClick={handleResetprofile}>
|
||||
Reset Profile
|
||||
</Button>
|
||||
<IconButton
|
||||
edge="end"
|
||||
className="cart-button"
|
||||
onClick={handleCart}
|
||||
>
|
||||
<IconButton edge="end" className="cart-button" onClick={handleCart}>
|
||||
<Badge
|
||||
badgeContent={totalSelected}
|
||||
color="secondary"
|
||||
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
CardMedia,
|
||||
CircularProgress,
|
||||
Box,
|
||||
Paper, DialogContent, DialogTitle, DialogActions, Button,
|
||||
Paper,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
import CheckIcon from "@material-ui/icons/Check";
|
||||
import NavBar from "./NavBar";
|
||||
import { Dialog } from "@mui/material";
|
||||
import matchers from "@testing-library/jest-dom/matchers";
|
||||
|
||||
const apiUrl = process.env.REACT_APP_API_URL;
|
||||
|
||||
@@ -66,18 +69,18 @@ const PosterGallery = () => {
|
||||
}
|
||||
}, [movieName, movieYear]);
|
||||
|
||||
const isPosterSelected = (posterId) => {
|
||||
return selectedPosters.some((poster) => poster.posterId === posterId);
|
||||
};
|
||||
|
||||
const handlePosterSelect = (posterId) => {
|
||||
const postersForCurrentMovie = selectedPosters.filter(
|
||||
(poster) => poster.movieId !== movieId
|
||||
);
|
||||
const isSelected = isPosterSelected(posterId);
|
||||
|
||||
const isPosterSelected = postersForCurrentMovie.some((poster) => poster.posterId === posterId)
|
||||
|
||||
if (postersForCurrentMovie.length > 0 && !isPosterSelected) {
|
||||
if (selectedPosters.length > 0 && !isSelected) {
|
||||
setDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
if (isPosterSelected) {
|
||||
if (isSelected) {
|
||||
dispatch(deselectPoster(movieName, movieYear, posterId));
|
||||
} else {
|
||||
dispatch(selectPoster(movieName, movieYear, posterId, watchedDate));
|
||||
@@ -86,7 +89,7 @@ const PosterGallery = () => {
|
||||
|
||||
const handleDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -114,13 +117,13 @@ const PosterGallery = () => {
|
||||
</Box>
|
||||
) : posters.length > 0 ? (
|
||||
<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}>
|
||||
<Card
|
||||
className={`poster-card ${
|
||||
selectedPosters.includes(poster.file_path)
|
||||
? "selected-poster"
|
||||
: ""
|
||||
isSelected ? "selected-poster" : ""
|
||||
}`}
|
||||
onClick={() => handlePosterSelect(poster.file_path)}
|
||||
>
|
||||
@@ -129,12 +132,11 @@ const PosterGallery = () => {
|
||||
image={`https://image.tmdb.org/t/p/original${poster.file_path}`}
|
||||
title={`${movieName} poster ${index + 1}`}
|
||||
/>
|
||||
{selectedPosters.includes(poster.file_path) && (
|
||||
<CheckIcon className="check-icon" />
|
||||
)}
|
||||
{isSelected && <CheckIcon className="check-icon" />}
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography align="center">
|
||||
@@ -148,7 +150,8 @@ const PosterGallery = () => {
|
||||
<DialogTitle>Poster already selected</DialogTitle>
|
||||
<DialogContent>
|
||||
<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>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -67,7 +67,7 @@ const PosterSelector = () => {
|
||||
const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
|
||||
setPage(pageNumber);
|
||||
|
||||
fetchMovies(pageNumber).then((r) => console.log(r));
|
||||
fetchMovies(pageNumber);
|
||||
}, [location.search]);
|
||||
|
||||
// const fetchUsername = async () => {
|
||||
@@ -86,14 +86,7 @@ const PosterSelector = () => {
|
||||
|
||||
const response = await axios.get(
|
||||
`${apiUrl}/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
|
||||
{
|
||||
// onDownloadProgress: (progressEvent) => {
|
||||
// const percentCompleted = Math.round(
|
||||
// (progressEvent.loaded * 100) / progressEvent.total
|
||||
// );
|
||||
// setProgress(percentCompleted);
|
||||
// },
|
||||
}
|
||||
{ withCredentials: true }
|
||||
);
|
||||
|
||||
if (response.data.movies) {
|
||||
@@ -109,20 +102,13 @@ const PosterSelector = () => {
|
||||
|
||||
const handleMovieClick = (movieName, movieYear, watchedDate) => {
|
||||
navigate(
|
||||
`/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(movieYear)}`,
|
||||
`/posters/${encodeURIComponent(movieName)}/${encodeURIComponent(
|
||||
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) => {
|
||||
setPage(value);
|
||||
navigate(`?page=${value}`);
|
||||
@@ -169,14 +155,20 @@ const PosterSelector = () => {
|
||||
<ListItem
|
||||
button
|
||||
key={index}
|
||||
onClick={() => handleMovieClick(movie.Name, movie.Year, movie["Watched Date"])}
|
||||
onClick={() =>
|
||||
handleMovieClick(
|
||||
movie.Name,
|
||||
movie.Year,
|
||||
movie["Watched Date"]
|
||||
)
|
||||
}
|
||||
className="movie-item"
|
||||
>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item>
|
||||
<img
|
||||
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`
|
||||
}
|
||||
alt={movie.Name}
|
||||
@@ -256,7 +248,7 @@ const PosterSelector = () => {
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
<NavBar onRefreshFiles={handleRefreshFiles} />
|
||||
<NavBar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,9 @@ const UploadDiary = () => {
|
||||
useEffect(() => {
|
||||
const checkCSVFile = async () => {
|
||||
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) {
|
||||
navigate("/PosterSelector");
|
||||
}
|
||||
@@ -49,16 +51,26 @@ const UploadDiary = () => {
|
||||
try {
|
||||
if (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");
|
||||
} else {
|
||||
console.error("Failed to fetch diary", response.data);
|
||||
}
|
||||
} else if (file) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
await axios.post(`${apiUrl}/api/upload-csv`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
navigate("/PosterSelector");
|
||||
@@ -84,6 +96,9 @@ const UploadDiary = () => {
|
||||
fullWidth
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === "Enter") handleUpload();
|
||||
}}
|
||||
placeholder="Enter your Letterboxd username"
|
||||
/>
|
||||
<Typography variant="body2" className="or-text">
|
||||
|
||||
@@ -17,7 +17,7 @@ const posterSelectionReducer = (state = {}, action) => {
|
||||
return {
|
||||
...state,
|
||||
[movieId]: state[movieId]?.filter(
|
||||
(id) => id !== posterId
|
||||
(poster) => poster.posterId !== posterId
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const posterSelectionReducer = (state = {}, action) => {
|
||||
return {
|
||||
...state,
|
||||
[movieId]: state[movieId]?.filter(
|
||||
(id) => id !== posterId
|
||||
(poster) => poster.posterId !== posterId
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
.poster-grid {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 1rem;
|
||||
max-height: calc(100vh - 4rem);
|
||||
padding-right: 1rem;
|
||||
overflow: visible;
|
||||
|
||||
.poster-card {
|
||||
position: relative;
|
||||
aspect-ratio: 2/3;
|
||||
min-height: 220px;
|
||||
|
||||
&:hover .delete-overlay {
|
||||
opacity: 1;
|
||||
@@ -23,6 +27,9 @@
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.delete-overlay {
|
||||
@@ -34,56 +41,86 @@
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
border-radius: 8px;
|
||||
|
||||
.delete-button {
|
||||
background-color: #e74c3c;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
padding: 0.65rem;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
transform: scale(1.1);
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: scale(1.05);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f6fa10;
|
||||
border: 2px #95a5a6;
|
||||
border-radius: 8px;
|
||||
// État vide simple
|
||||
.empty-cart-container {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #7f8c8d;
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
|
||||
svg {
|
||||
margin-bottom: 1rem;
|
||||
color: #95a5a6;
|
||||
.MuiTypography-body1 {
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
.back-button {
|
||||
color: #fff;
|
||||
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;
|
||||
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 {
|
||||
width: 300px;
|
||||
flex: 0 0 320px;
|
||||
max-width: 320px;
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
background-color: #34495e;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
height: fit-content;
|
||||
box-sizing: border-box;
|
||||
align-self: flex-start;
|
||||
|
||||
h2 {
|
||||
color: white;
|
||||
@@ -94,32 +131,181 @@
|
||||
.format-select {
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
background-color: #2c3e50;
|
||||
border: 1px solid #95a5a6;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.download-button, .clear-button {
|
||||
.download-button,
|
||||
.clear-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
padding: 0.85rem;
|
||||
background-color: #00a346;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
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 {
|
||||
background-color: darken(#00a346, 10%);
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: darken(#00a346, 20%);
|
||||
background-color: #7f8c8d;
|
||||
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