Change Cart disposition

This commit is contained in:
Pierret Hugo
2024-10-28 14:58:14 +01:00
parent 92c10f4f18
commit 0c5b2b4bbc
11 changed files with 195 additions and 357 deletions

View File

@@ -33,7 +33,7 @@ jobs:
run: npm run build --if-present run: npm run build --if-present
env: env:
CI: false CI: false
REACT_APP_APIUrl: ${{ secrets.REACT_APP_APIUrl }} REACT_APP_API_URL: ${{ secrets.REACT_APP_API_URL }}
- run: npm test -- --passWithNoTests - run: npm test -- --passWithNoTests

9
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.7.6", "axios": "^1.7.6",
"lucide-react": "^0.453.0",
"material-ui-dropzone": "^3.5.0", "material-ui-dropzone": "^3.5.0",
"query-string": "^9.1.0", "query-string": "^9.1.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -14800,6 +14801,14 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.453.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz",
"integrity": "sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/lz-string": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",

View File

@@ -14,6 +14,7 @@
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.7.6", "axios": "^1.7.6",
"lucide-react": "^0.453.0",
"material-ui-dropzone": "^3.5.0", "material-ui-dropzone": "^3.5.0",
"query-string": "^9.1.0", "query-string": "^9.1.0",
"react": "^18.3.1", "react": "^18.3.1",

View File

@@ -1,27 +1,9 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from 'react-redux';
import { import { removePoster } from '../services/action';
Container, import DeleteIcon from '@material-ui/icons/Delete';
Typography,
Card, const apiUrl = process.env.REACT_APP_API_URL;
CardMedia,
CardContent,
Button,
Checkbox,
FormControlLabel,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Paper,
} from "@material-ui/core";
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 Cart = () => { const Cart = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -32,192 +14,88 @@ const Cart = () => {
); );
}); });
const [selectedForDownload, setSelectedForDownload] = useState( const [downloadFormat, setDownloadFormat] = useState('zip');
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) => { const handleRemovePoster = (movieId, posterId) => {
dispatch( dispatch(removePoster(movieId, posterId));
removePoster(
selectedPosters[index].movieId,
selectedPosters[index].posterId
)
);
}; };
const handleDownload = async () => { 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 { try {
const response = await axios.post( const response = await fetch(`${apiUrl}/api/download-posters`, {
"http://localhost:5000/api/download-posters", method: 'POST',
{ headers: {
posters: postersToDownload, 'Content-Type': 'application/json',
format: downloadFormat,
}, },
{ body: JSON.stringify({
responseType: "blob", posters: selectedPosters,
} format: downloadFormat,
); }),
const blob = new Blob([response.data], {
type: response.headers["content-type"],
}); });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob); if (!response.ok) throw new Error('Download failed');
link.download = `posters.${downloadFormat}`;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/zip')) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'posters.zip';
document.body.appendChild(link);
link.click(); link.click();
} catch (error) { link.remove();
console.error("Failed to download posters:", error); window.URL.revokeObjectURL(url);
} else {
throw new Error('Invalid response format');
}
} 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 ( return (
<Container className="cart-container"> <div className="cart-container">
<div className="poster-list"> <div className="poster-grid">
<Typography variant="h4" gutterBottom className="cart-title"> {selectedPosters.map((poster) => (
Your Cart <div key={`${poster.movieId}-${poster.posterId}`} className="poster-card">
</Typography> <img
{selectedPosters.map((poster, index) => ( src={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
<Card key={index} className="poster-card"> alt="Movie poster"
<CardMedia className="poster-image"
className="poster-media"
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`}
/> />
<CardContent className="poster-content"> <div className="delete-overlay">
<Typography variant="h6" className="poster-title"> <button
{`Movie ID: ${poster.movieId}`} className="delete-button"
</Typography> onClick={() => handleRemovePoster(poster.movieId, poster.posterId)}
<Typography variant="body2" className="poster-id">
{poster.posterId}
</Typography>
<FormControlLabel
control={
<Checkbox
checked={selectedForDownload[index]}
onChange={(e) => {
const newSelected = [...selectedForDownload];
newSelected[index] = e.target.checked;
setSelectedForDownload(newSelected);
}}
className="checkbox"
/>
}
label="Select for download"
/>
<Button
startIcon={<DeleteIcon />}
onClick={() => handleRemovePoster(index)}
className="action-button"
> >
Remove <DeleteIcon />
</Button> </button>
<Button </div>
startIcon={<EditIcon />} </div>
onClick={() => handleRename(index)}
className="action-button"
>
Rename
</Button>
</CardContent>
</Card>
))} ))}
</div> </div>
<Paper className="action-panel" elevation={3}>
<div className="action-buttons"> <div className="action-panel">
<Typography gutterBottom className="action-title"> <h2>Download Options</h2>
Download Format <select
</Typography>
<TextField
select
value={downloadFormat} value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value)} onChange={(e) => setDownloadFormat(e.target.value)}
SelectProps={{ className="format-select"
native: true,
}}
className="form-control"
> >
<option value="zip">ZIP</option> <option value="zip">ZIP</option>
<option value="tar">TAR</option> <option value="tar">TAR</option>
<option value="7z">7Z</option> <option value="7z">7Z</option>
</TextField> </select>
<Button <button
variant="contained" className="download-button"
color="primary"
startIcon={<GetAppIcon />}
onClick={handleDownload} onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)} disabled={selectedPosters.length === 0}
className="primary-button"
> >
Download Selected Download Selected
</Button> </button>
<Button </div>
variant="contained"
color="secondary"
startIcon={<ShareIcon />}
onClick={handleShare}
className="secondary-button"
>
Share Selection
</Button>
</div> </div>
</Paper>
<Dialog
open={renameDialogOpen}
onClose={() => setRenameDialogOpen(false)}
className="rename-dialog"
>
<DialogTitle className="dialog-title">Rename Poster</DialogTitle>
<DialogContent className="dialog-content">
<TextField
autoFocus
margin="dense"
label="New Name"
type="text"
fullWidth
value={newPosterName}
onChange={(e) => setNewPosterName(e.target.value)}
/>
</DialogContent>
<DialogActions className="dialog-actions">
<Button onClick={() => setRenameDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={handleRenameConfirm} color="primary">
Rename
</Button>
</DialogActions>
</Dialog>
</Container>
); );
}; };

View File

@@ -1,34 +1,11 @@
import React from "react"; import React from "react";
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 { makeStyles } from '@material-ui/core/styles';
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 useStyles = makeStyles((theme) => ({ const NavBar = ({ onRefreshFiles }) => {
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 navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const selectedPosters = useSelector((state) => { const selectedPosters = useSelector((state) => {
@@ -49,22 +26,22 @@ const NavBar = ({onRefreshFiles}) => {
const isPosterSelectorPage = location.pathname !== '/PosterSelector'; const isPosterSelectorPage = location.pathname !== '/PosterSelector';
return ( return (
<AppBar position="fixed" className={classes.appBar}> <AppBar position="fixed" className="navbar">
<Toolbar className={classes.toolbar}> <Toolbar className="toolbar">
{isPosterSelectorPage ? ( {isPosterSelectorPage ? (
<IconButton edge="start" className={classes.backButton} onClick={handleBack}> <IconButton edge="start" className="back-button" onClick={handleBack}>
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
) : ( ) : (
<IconButton edge="start" className={classes.backButton} disabled> <IconButton edge="start" className="back-button" disabled>
{/* Un IconButton vide et désactivé */} {/* Un IconButton vide et désactivé */}
</IconButton> </IconButton>
)} )}
<div> <div>
<Button className={classes.refreshButton} onClick={onRefreshFiles}> <Button className="refresh-button" onClick={onRefreshFiles}>
Refresh Files Refresh Files
</Button> </Button>
<IconButton edge="end" className={classes.cartButton} onClick={handleCart}> <IconButton edge="end" className="cart-button" onClick={handleCart}>
<Badge badgeContent={totalSelected} color="secondary"> <Badge badgeContent={totalSelected} color="secondary">
<ShoppingCartIcon /> <ShoppingCartIcon />
</Badge> </Badge>

View File

@@ -18,7 +18,7 @@ import queryString from "query-string";
import pulpGif from "../static/images/pulp.gif"; import pulpGif from "../static/images/pulp.gif";
import NavBar from "./NavBar"; import NavBar from "./NavBar";
const apiUrl = process.env.REACT_APP_APIUrl; const apiUrl = process.env.REACT_APP_API_URL;
const paginationTheme = createTheme({ const paginationTheme = createTheme({
palette: { palette: {

View File

@@ -13,7 +13,7 @@ import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import axios from "axios"; import axios from "axios";
const apiUrl = process.env.REACT_APP_APIUrl; const apiUrl = process.env.REACT_APP_API_URL;
const UploadDiary = () => { const UploadDiary = () => {
const [file, setFile] = useState(null); const [file, setFile] = useState(null);

View File

@@ -2,6 +2,7 @@
@use "./scss/posterSelector"; @use "./scss/posterSelector";
@use "./scss/posterGallery"; @use "./scss/posterGallery";
@use "./scss/cart"; @use "./scss/cart";
@use "./scss/navbar";
.App { .App {
text-align: center; text-align: center;

View File

@@ -1,90 +1,55 @@
// Variables // _cart.scss
$primary-color: #00a346;
$secondary-color: #3498db;
$background-dark: #2c3e50;
$background-light: #34495e;
$text-white: #ecf0f1;
$text-muted: #95a5a6;
$hover-scale: 1.05;
$gap-spacing: 1rem;
.cart-container { .cart-container {
margin-top: 2rem; padding: 2rem;
margin-bottom: 4rem;
display: flex; display: flex;
gap: $gap-spacing; gap: 2rem;
.poster-list { .poster-grid {
flex: 2; flex: 1;
margin-right: 2rem; display: grid;
max-height: calc(100vh - 200px); grid-template-columns: repeat(4, 1fr);
gap: 1rem;
max-height: calc(100vh - 4rem);
overflow-y: auto; overflow-y: auto;
padding-right: 1rem;
.cart-title {
color: $text-white;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 400;
}
.poster-card { .poster-card {
display: flex; position: relative;
margin-bottom: 1rem; aspect-ratio: 2/3;
background-color: $background-dark; border-radius: 8px;
border-radius: 4px;
overflow: hidden; overflow: hidden;
.poster-media { &:hover .delete-overlay {
width: 120px; opacity: 1;
height: 180px; }
.poster-image {
width: 100%;
height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 4px 0 0 4px;
} }
.poster-content { .delete-overlay {
flex-grow: 1; position: absolute;
padding: 1rem; inset: 0;
display: flex; background-color: rgba(0, 0, 0, 0.7);
flex-direction: column;
justify-content: space-between;
.poster-title {
color: $text-white;
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.poster-id {
color: $text-muted;
font-size: 1rem;
margin-bottom: 1rem;
}
.poster-actions {
display: flex;
justify-content: space-between;
gap: 0.5rem;
.action-button {
flex-grow: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
.delete-button {
background-color: #e74c3c;
border: none;
color: white;
padding: 0.5rem; padding: 0.5rem;
border-radius: 4px; border-radius: 50%;
background-color: transparent; cursor: pointer;
border: 1px solid $text-muted; transition: transform 0.2s ease;
color: $text-white;
transition: background-color 0.3s ease;
&:hover { &:hover {
background-color: $background-light; transform: scale(1.1);
cursor: pointer;
}
&:not(:last-child) {
margin-right: 0.5rem;
}
} }
} }
} }
@@ -92,75 +57,49 @@ $gap-spacing: 1rem;
} }
.action-panel { .action-panel {
flex: 1; width: 300px;
position: sticky; position: sticky;
top: 2rem; top: 2rem;
height: fit-content; background-color: #34495e;
padding: 1.5rem;
background-color: $background-light;
border-radius: 8px; border-radius: 8px;
padding: 1.5rem;
height: fit-content;
.action-title { h2 {
color: $text-white; color: white;
margin-bottom: 1rem; margin-bottom: 1.5rem;
font-size: 1.5rem; font-size: 1.25rem;
} }
.form-control { .format-select {
width: 100%; width: 100%;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: $text-white;
select {
background-color: $background-dark;
color: $text-white;
padding: 0.5rem; padding: 0.5rem;
border: 1px solid $text-muted; background-color: #2c3e50;
border: 1px solid #95a5a6;
color: white;
border-radius: 4px; border-radius: 4px;
} }
}
.action-buttons { .download-button {
display: flex;
flex-direction: column;
gap: 1rem;
.primary-button,
.secondary-button {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
background-color: #00a346;
color: white;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
font-size: 1rem; margin-bottom: 1rem;
font-weight: bold; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
&.primary-button {
background-color: $primary-color;
color: $text-white;
&:hover { &:hover {
background-color: darken($primary-color, 10%); background-color: darken(#00a346, 10%);
} }
&:disabled { &:disabled {
background-color: darken($primary-color, 20%); background-color: darken(#00a346, 20%);
cursor: not-allowed; cursor: not-allowed;
} }
} }
&.secondary-button {
background-color: $secondary-color;
color: $text-white;
&:hover {
background-color: darken($secondary-color, 10%);
}
}
}
}
} }
} }

View File

@@ -0,0 +1,32 @@
// _navbar.scss
// Variables de couleur et autres constantes
$background-color: #1c1f23;
$icon-color: white;
$margin-right: 8px; // Correspondant à theme.spacing(2)
.navbar {
top: auto !important;
bottom: 0;
background-color: $background-color !important;
.toolbar {
display: flex;
justify-content: space-between;
}
.back-button,
.cart-button,
.refresh-button {
color: $icon-color;
}
.refresh-button {
margin-right: $margin-right;
}
.back-button[disabled] {
opacity: 0.3;
pointer-events: none;
}
}

View File

@@ -151,6 +151,7 @@ $hover-color: rgba(102, 119, 136, 0.2);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 400px; height: 400px;
color: $text-white;
} }
.gif-container { .gif-container {