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) => ( -
- Movie poster -
- -
-
- )) - )} -
- -
-

Download Options

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

Download Options

+ + + +
+ + + Confirm deletion + + + Are you sure you want to delete all the posters in your collection? + This action cannot be undone. + + + + + + + +
); }; 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" > {movie.Name} { )} - + ); }; 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; +}