diff --git a/src/components/Cart.js b/src/components/Cart.js
index 8dfa2b0..a61c993 100644
--- a/src/components/Cart.js
+++ b/src/components/Cart.js
@@ -1,33 +1,59 @@
import React, { useState } from "react";
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 {
+ 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]) =>
- posters.map((poster) => ({
- movieId,
- posterId: typeof poster === "string" ? poster : poster.posterId,
- watchedDate: poster.watchedDate,
- }))
+ posters.map((poster) => ({
+ movieId,
+ posterId: typeof poster === "string" ? poster : poster.posterId,
+ watchedDate: poster.watchedDate,
+ }))
);
});
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 {
@@ -64,61 +90,104 @@ const Cart = () => {
};
return (
-
-
- {selectedPosters.length === 0 ? (
-
No posters selected
- ) : (
- selectedPosters.map((poster) => (
-
-

-
-
-
-
- ))
- )}
-
-
-
-
Download Options
-
-
-
-
+
+
+ {selectedPosters.length === 0 ? (
+
+
+ Your poster collection is empty. Start exploring movies to add
+ some posters!
+
+
+
+ ) : (
+ selectedPosters.map((poster) => (
+
+

+
+
+
+
+ ))
+ )}
+
+
+
Download Options
+
+
+
+
+
+
+
);
};
diff --git a/src/components/NavBar.js b/src/components/NavBar.js
index 816f92f..4c51c06 100644
--- a/src/components/NavBar.js
+++ b/src/components/NavBar.js
@@ -1,77 +1,74 @@
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 navigate = useNavigate();
- const location = useLocation();
+const NavBar = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
- const posterSelections = useSelector(selectPosterSelections);
- const selectedPosters = useMemo(() => {
- return Object.values(posterSelections).flat();
- }, [posterSelections]);
+ const posterSelections = useSelector(selectPosterSelections);
+ const selectedPosters = useMemo(() => {
+ return Object.values(posterSelections).flat();
+ }, [posterSelections]);
- const totalSelected = selectedPosters.length;
+ const totalSelected = selectedPosters.length;
- const handleBack = () => {
- navigate(-1);
- };
+ const handleBack = () => {
+ navigate(-1);
+ };
- const handleCart = () => {
- navigate('/Cart');
- };
+ const handleCart = () => {
+ 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);
+ }
+ };
- return (
-
-
- {isPosterSelectorPage ? (
-
-
-
- ) : (
-
- {/* Disabled IconButton */}
-
- )}
-
-
-
-
-
-
-
-
-
-
- );
+ const isPosterSelectorPage = location.pathname !== "/PosterSelector";
+
+ return (
+
+
+ {isPosterSelectorPage ? (
+
+
+
+ ) : (
+
+ {/* Disabled IconButton */}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
};
-export default NavBar;
\ No newline at end of file
+export default NavBar;
diff --git a/src/components/PosterGallery.js b/src/components/PosterGallery.js
index bbf35e7..c2f1169 100644
--- a/src/components/PosterGallery.js
+++ b/src/components/PosterGallery.js
@@ -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";
+import { Dialog } from "@mui/material";
const apiUrl = process.env.REACT_APP_API_URL;
@@ -46,7 +49,7 @@ const PosterGallery = () => {
const encodedYear = encodeURIComponent(movieYear);
const response = await axios.get(
- `${apiUrl}/api/posters/${encodedName}/${encodedYear}`
+ `${apiUrl}/api/posters/${encodedName}/${encodedYear}`
);
if (response.data && response.data.posters) {
@@ -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,31 +117,30 @@ const PosterGallery = () => {
) : posters.length > 0 ? (
- {posters.map((poster, index) => (
-
- handlePosterSelect(poster.file_path)}
- >
-
- {selectedPosters.includes(poster.file_path) && (
-
- )}
-
-
- ))}
+ {posters.map((poster, index) => {
+ const isSelected = isPosterSelected(poster.file_path);
+ return (
+
+ handlePosterSelect(poster.file_path)}
+ >
+
+ {isSelected && }
+
+
+ );
+ })}
) : (
- No posters found for this movie.
+ No posters found for this movie.
)}
@@ -148,7 +150,8 @@ const PosterGallery = () => {
Poster already selected
- 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.
diff --git a/src/components/PosterSelector.js b/src/components/PosterSelector.js
index 718e492..c583e04 100644
--- a/src/components/PosterSelector.js
+++ b/src/components/PosterSelector.js
@@ -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)}`,
- {state: {watchedDate}}
+ `/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 = () => {
handleMovieClick(movie.Name, movie.Year, movie["Watched Date"])}
+ onClick={() =>
+ handleMovieClick(
+ movie.Name,
+ movie.Year,
+ movie["Watched Date"]
+ )
+ }
className="movie-item"
>
{
)}
-
+
>
);
};
diff --git a/src/components/UploadDiary.js b/src/components/UploadDiary.js
index 0899f90..b76b1e4 100644
--- a/src/components/UploadDiary.js
+++ b/src/components/UploadDiary.js
@@ -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));
- navigate("/PosterSelector");
+ // 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"
/>
diff --git a/src/services/store.js b/src/services/store.js
index 402ca73..210f953 100644
--- a/src/services/store.js
+++ b/src/services/store.js
@@ -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
),
};
}
diff --git a/src/styles/scss/_cart.scss b/src/styles/scss/_cart.scss
index 12a473d..83472be 100644
--- a/src/styles/scss/_cart.scss
+++ b/src/styles/scss/_cart.scss
@@ -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;
+}