update style and setup gh-pages

This commit is contained in:
2024-10-20 00:42:23 +02:00
parent 129fcb672c
commit ba881b1637
14 changed files with 1455 additions and 629 deletions

View File

@@ -1,5 +1,5 @@
import React from "react";
import "./styles/App.css";
import "./styles/App.scss";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";

View File

@@ -1,239 +1,282 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import {
Container, Typography, Card, CardMedia, CardContent,
Button, Checkbox, FormControlLabel, TextField, Dialog, DialogTitle, DialogContent, DialogActions,
Paper
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
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';
Container,
Typography,
Card,
CardMedia,
CardContent,
Button,
Checkbox,
FormControlLabel,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Paper,
} from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
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 useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(8),
display: 'flex',
},
posterList: {
flex: 2,
marginRight: theme.spacing(2),
maxHeight: 'calc(100vh - 200px)',
overflowY: 'auto',
},
actionPanel: {
flex: 1,
position: 'sticky',
top: theme.spacing(4),
height: 'fit-content',
},
card: {
display: 'flex',
marginBottom: theme.spacing(2),
backgroundColor: '#2c3e50',
},
cardMedia: {
width: 100,
},
cardContent: {
flex: '1 0 auto',
},
actions: {
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
},
slider: {
width: '100%',
marginTop: theme.spacing(2),
},
button: {
width: '100%',
},
typography: {
color: '#ecf0f1',
},
formControl: {
marginBottom: theme.spacing(2),
},
checkbox: {
color: '#3498db',
},
paper: {
padding: theme.spacing(3),
backgroundColor: '#34495e',
},
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(8),
display: "flex",
},
posterList: {
flex: 2,
marginRight: theme.spacing(2),
maxHeight: "calc(100vh - 200px)",
overflowY: "auto",
},
actionPanel: {
flex: 1,
position: "sticky",
top: theme.spacing(4),
height: "fit-content",
},
card: {
display: "flex",
marginBottom: theme.spacing(2),
backgroundColor: "#2c3e50",
},
cardMedia: {
width: 100,
},
cardContent: {
flex: "1 0 auto",
},
actions: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(2),
},
slider: {
width: "100%",
marginTop: theme.spacing(2),
},
button: {
width: "100%",
},
typography: {
color: "#ecf0f1",
},
formControl: {
marginBottom: theme.spacing(2),
},
checkbox: {
color: "#3498db",
},
paper: {
padding: theme.spacing(3),
backgroundColor: "#34495e",
},
}));
const Cart = () => {
const classes = useStyles();
const dispatch = useDispatch();
const selectedPosters = useSelector((state) => {
const selections = state.posterSelections;
return Object.entries(selections).flatMap(([movieId, posters]) =>
posters.map(posterId => ({ movieId, posterId }))
);
});
const [selectedForDownload, setSelectedForDownload] = useState(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) => {
dispatch(removePoster(selectedPosters[index].movieId, selectedPosters[index].posterId));
};
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 {
const response = await axios.post('http://localhost:5000/api/download-posters', {
posters: postersToDownload,
format: downloadFormat,
}, {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `posters.${downloadFormat}`;
link.click();
} 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 (
<Container className={classes.root}>
<div className={classes.posterList}>
<Typography variant="h4" gutterBottom className={classes.typography}>Your Cart</Typography>
{selectedPosters.map((poster, index) => (
<Card key={index} className={classes.card}>
<CardMedia
className={classes.cardMedia}
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`}
/>
<CardContent className={classes.cardContent}>
<Typography variant="h6"
className={classes.typography}>{`Movie ID: ${poster.movieId}`}</Typography>
<Typography variant="body2"
className={classes.typography}>{poster.posterId}</Typography>
<FormControlLabel
control={
<Checkbox
checked={selectedForDownload[index]}
onChange={(e) => {
const newSelected = [...selectedForDownload];
newSelected[index] = e.target.checked;
setSelectedForDownload(newSelected);
}}
className={classes.checkbox}
/>
}
label="Select for download"
className={classes.typography}
/>
<Button startIcon={<DeleteIcon/>} onClick={() => handleRemovePoster(index)}
className={classes.typography}>Remove</Button>
<Button startIcon={<EditIcon/>} onClick={() => handleRename(index)}
className={classes.typography}>Rename</Button>
</CardContent>
</Card>
))}
</div>
<Paper className={classes.actionPanel} elevation={3}>
<div className={classes.actions}>
<Typography gutterBottom>Download Format</Typography>
<TextField
select
value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value)}
SelectProps={{
native: true,
}}
className={classes.formControl}
>
<option value="zip">ZIP</option>
<option value="tar">TAR</option>
<option value="7z">7Z</option>
</TextField>
<Button
variant="contained"
color="primary"
startIcon={<GetAppIcon/>}
onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)}
className={classes.button}
>
Download Selected
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<ShareIcon/>}
onClick={handleShare}
className={classes.button}
>
Share Selection
</Button>
</div>
</Paper>
<Dialog open={renameDialogOpen} onClose={() => setRenameDialogOpen(false)}>
<DialogTitle>Rename Poster</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="New Name"
type="text"
fullWidth
value={newPosterName}
onChange={(e) => setNewPosterName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setRenameDialogOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={handleRenameConfirm} color="primary">
Rename
</Button>
</DialogActions>
</Dialog>
</Container>
const classes = useStyles();
const dispatch = useDispatch();
const selectedPosters = useSelector((state) => {
const selections = state.posterSelections;
return Object.entries(selections).flatMap(([movieId, posters]) =>
posters.map((posterId) => ({ movieId, posterId }))
);
});
const [selectedForDownload, setSelectedForDownload] = useState(
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) => {
dispatch(
removePoster(
selectedPosters[index].movieId,
selectedPosters[index].posterId
)
);
};
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 {
const response = await axios.post(
"http://localhost:5000/api/download-posters",
{
posters: postersToDownload,
format: downloadFormat,
},
{
responseType: "blob",
}
);
const blob = new Blob([response.data], {
type: response.headers["content-type"],
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = `posters.${downloadFormat}`;
link.click();
} 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 (
<Container className="cart-container">
<div className="poster-list">
<Typography variant="h4" gutterBottom className="cart-title">
Your Cart
</Typography>
{selectedPosters.map((poster, index) => (
<Card key={index} className="poster-card">
<CardMedia
className="poster-media"
image={`https://image.tmdb.org/t/p/w500${poster.posterId}`}
title={`Poster ${index + 1}`}
/>
<CardContent className="poster-content">
<Typography variant="h6" className="poster-title">
{`Movie ID: ${poster.movieId}`}
</Typography>
<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
</Button>
<Button
startIcon={<EditIcon />}
onClick={() => handleRename(index)}
className="action-button"
>
Rename
</Button>
</CardContent>
</Card>
))}
</div>
<Paper className="action-panel" elevation={3}>
<div className="action-buttons">
<Typography gutterBottom className="action-title">
Download Format
</Typography>
<TextField
select
value={downloadFormat}
onChange={(e) => setDownloadFormat(e.target.value)}
SelectProps={{
native: true,
}}
className="form-control"
>
<option value="zip">ZIP</option>
<option value="tar">TAR</option>
<option value="7z">7Z</option>
</TextField>
<Button
variant="contained"
color="primary"
startIcon={<GetAppIcon />}
onClick={handleDownload}
disabled={selectedForDownload.every((selected) => !selected)}
className="primary-button"
>
Download Selected
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<ShareIcon />}
onClick={handleShare}
className="secondary-button"
>
Share Selection
</Button>
</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>
);
};
export default Cart;
export default Cart;

View File

@@ -17,6 +17,8 @@ import {
import CheckIcon from "@material-ui/icons/Check";
import NavBar from "./NavBar";
const apiUrl = process.env.REACT_APP_API_URL;
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(4),
@@ -85,14 +87,12 @@ const PosterGallery = ({ movieId }) => {
(state) => state.posterSelections[movieId] || []
);
console.log(movieId);
useEffect(() => {
const fetchPosters = async () => {
try {
setLoading(true);
const response = await axios.get(
`http://localhost:5000/api/posters/${movieName}/${movieYear}`
`${apiUrl}/api/posters/${movieName}/${movieYear}`
);
setPosters(response.data.posters);
} catch (error) {
@@ -115,39 +115,39 @@ const PosterGallery = ({ movieId }) => {
return (
<>
<Container className={classes.root}>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h4" gutterBottom className={classes.title}>
<Container className="poster-gallery">
<Paper elevation={3} className="content-paper">
<Typography variant="h4" gutterBottom className="title">
Posters for {movieName} ({movieYear})
</Typography>
{loading ? (
<Box className={classes.loadingContainer}>
<CircularProgress />
</Box>
<Box className="loading-container">
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
{posters.map((poster, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<Card
className={`${classes.posterCard} ${
selectedPosters.includes(poster.file_path)
? classes.selectedPoster
: ""
}`}
onClick={() => handlePosterSelect(poster.file_path)}
>
<CardMedia
className={classes.posterImage}
image={`https://image.tmdb.org/t/p/w500${poster.file_path}`}
title={`${movieName} poster ${index + 1}`}
/>
{selectedPosters.includes(poster.file_path) && (
<CheckIcon className={classes.checkIcon} />
)}
</Card>
</Grid>
))}
</Grid>
<Grid container spacing={3}>
{posters.map((poster, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<Card
className={`poster-card ${
selectedPosters.includes(poster.file_path)
? "selected-poster"
: ""
}`}
onClick={() => handlePosterSelect(poster.file_path)}
>
<CardMedia
className="poster-image"
image={`https://image.tmdb.org/t/p/w500${poster.file_path}`}
title={`${movieName} poster ${index + 1}`}
/>
{selectedPosters.includes(poster.file_path) && (
<CheckIcon className="check-icon" />
)}
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
</Container>

View File

@@ -10,7 +10,6 @@ import {
LinearProgress,
Box,
Paper,
makeStyles,
Button,
} from "@material-ui/core";
import Pagination from "@mui/material/Pagination";
@@ -19,127 +18,7 @@ import queryString from "query-string";
import pulpGif from "../static/images/pulp.gif";
import NavBar from "./NavBar";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(4),
marginBottom: theme.spacing(8),
},
paper: {
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadius,
backgroundColor: "#1c1f23",
color: "white",
},
title: {
marginBottom: theme.spacing(3),
},
username: {
color: "#00A346",
marginBottom: theme.spacing(2),
},
list: {
maxHeight: "800px",
minHeight: "900px",
},
listItem: {
marginBottom: theme.spacing(2),
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
"&:not(:last-child)": {
borderBottom: `1px solid #667788`,
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
},
},
poster: {
width: "50px",
height: "75px",
objectFit: "cover",
marginRight: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
},
movieInfo: {
flexGrow: 1,
},
movieTitle: {
color: "white",
fontFamily: "TiemposTextWeb-Semibold, Georgia, serif",
fontSize: "1.38461538rem",
fontWeight: "400",
"&:hover": {
color: "var(--primary)",
},
},
movieYear: {
color: "#667788",
},
watchedDate: {
color: "#667788",
textAlign: "center",
},
watchedDay: {
fontSize: "2rem",
},
refreshButton: {
marginTop: theme.spacing(2),
},
paginationContainer: {
display: "flex",
justifyContent: "center",
marginTop: theme.spacing(3),
},
progressContainer: {
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: "200px",
},
progressLabel: {
marginTop: theme.spacing(2),
color: "white",
},
noMoviesContainer: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "400px",
},
gifContainer: {
display: "flex",
justifyContent: "center",
position: "relative",
width: "200px",
height: "200px",
marginBottom: theme.spacing(2),
},
circleBackground: {
position: "absolute",
width: "120%",
height: "120%",
borderRadius: "50%",
backgroundColor: "#ffffff",
clipPath: "inset(0 0 25% 0)",
bottom: "-28%",
},
gif: {
position: "absolute",
width: "100%",
height: "100%",
objectFit: "contain",
},
backButton: {
marginTop: theme.spacing(2),
backgroundColor: "#00A346",
color: "white",
"&:hover": {
backgroundColor: "#008036",
},
},
}));
const apiUrl = process.env.REACT_APP_API_URL;
const paginationTheme = createTheme({
palette: {
@@ -173,7 +52,6 @@ const paginationTheme = createTheme({
});
const PosterSelector = () => {
const classes = useStyles();
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
@@ -182,20 +60,19 @@ const PosterSelector = () => {
const [username, setUsername] = useState("");
const navigate = useNavigate();
const location = useLocation();
const moviesPerPage = 10;
const moviesPerPage = 8;
useEffect(() => {
const parsed = queryString.parse(location.search);
const pageNumber = parsed.page ? parseInt(parsed.page, 10) : 1;
setPage(pageNumber);
fetchMovies(pageNumber).then(r => console.log(r));
// fetchUsername();
fetchMovies(pageNumber).then((r) => console.log(r));
}, [location.search]);
const fetchUsername = async () => {
try {
const response = await axios.get("http://localhost:5000/api/username");
const response = await axios.get(`${apiUrl}/api/username`);
setUsername(response.data.username);
} catch (error) {
console.error("Error fetching username:", error);
@@ -208,7 +85,7 @@ const PosterSelector = () => {
setProgress(0);
const response = await axios.get(
`http://localhost:5000/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
`${apiUrl}/api/movies?page=${pageNumber}&limit=${moviesPerPage}`,
{
onDownloadProgress: (progressEvent) => {
const percentCompleted = Math.round(
@@ -240,7 +117,7 @@ const PosterSelector = () => {
const handleRefreshFiles = async () => {
try {
await axios.delete("http://localhost:5000/api/delete-csv");
await axios.delete(`${apiUrl}/api/delete-csv`);
navigate("/");
} catch (error) {
console.error("Error deleting CSV file:", error);
@@ -267,121 +144,116 @@ const PosterSelector = () => {
return (
<>
<Container className={classes.root}>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h4" gutterBottom className={classes.title}>
<Container className="poster-selector">
<Paper elevation={3} className="content-paper">
<Typography variant="h4" className="title">
Your diary
</Typography>
{username && (
<Typography variant="h6" className={classes.username}>
Letterboxd User: {username}
</Typography>
<Typography variant="h6" className="username">
Letterboxd User: {username}
</Typography>
)}
{loading ? (
<Box className={classes.progressContainer}>
<LinearProgress variant="determinate" value={progress} />
<Typography className={classes.progressLabel}>
Downloading movies...
</Typography>
</Box>
<Box className="progress-container">
<LinearProgress />
<Typography className="progress-label">
Downloading movies...
</Typography>
</Box>
) : movies.length > 0 ? (
<>
<List className={classes.list}>
{movies.map((movie, index) => (
<ListItem
button
key={index}
onClick={() => handleMovieClick(movie.Name, movie.Year)}
className={classes.listItem}
>
<Grid container alignItems="center">
<Grid item>
<img
src={
`https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` ||
`/api/placeholder/50/75`
}
alt={movie.Name}
className={classes.poster}
/>
</Grid>
<Grid item className={classes.movieInfo}>
<Typography
variant="subtitle1"
className={classes.movieTitle}
>
{movie.Name}
</Typography>
<Typography
variant="body2"
color="textSecondary"
className={classes.movieYear}
>
{movie.Year}
</Typography>
</Grid>
<Grid item>
<Box className={classes.watchedDate}>
{(() => {
const { day, month } = formatWatchedDate(
movie["Watched Date"]
);
return (
<>
<Typography
variant="body2"
className={classes.watchedDay}
>
{day}
</Typography>
<Typography
variant="body2"
className={classes.watchedMonth}
>
{month}
</Typography>
</>
);
})()}
</Box>
</Grid>
</Grid>
</ListItem>
))}
</List>
<Box className={classes.paginationContainer}>
<ThemeProvider theme={paginationTheme}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</ThemeProvider>
</Box>
</>
) : (
<Box className={classes.noMoviesContainer}>
<div className={classes.gifContainer}>
<div className={classes.circleBackground}></div>
<img
src={pulpGif}
alt="Confused reaction"
className={classes.gif}
<>
<List className="movies-list">
{movies.map((movie, index) => (
<ListItem
button
key={index}
onClick={() => handleMovieClick(movie.Name, movie.Year)}
className="movie-item"
>
<Grid container alignItems="center">
<Grid item>
<img
src={
`https://image.tmdb.org/t/p/w500${movie.Poster.file_path}` ||
`/api/placeholder/50/75`
}
alt={movie.Name}
className="movie-poster"
/>
</Grid>
<Grid item className="movie-info">
<Typography variant="subtitle1" className="movie-title">
{movie.Name}
</Typography>
<Typography variant="body2" className="movie-year">
{movie.Year}
</Typography>
</Grid>
<Grid item>
<Box className="watched-date">
{(() => {
const { day, month } = formatWatchedDate(
movie["Watched Date"]
);
return (
<>
<Typography
variant="body2"
className="watched-day"
>
{day}
</Typography>
<Typography
variant="body2"
className="watched-month"
>
{month}
</Typography>
</>
);
})()}
</Box>
</Grid>
</Grid>
</ListItem>
))}
</List>
<Box className="pagination-container">
<ThemeProvider theme={paginationTheme}>
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</div>
<Typography variant="body1" gutterBottom>
No movies found in your diary. Please check your Letterboxd
username or CSV file.
</Typography>
<Button
variant="contained"
className={classes.backButton}
onClick={handleGoBack}
>
Go Back
</Button>
</ThemeProvider>
</Box>
</>
) : (
<Box className="no-movies-container">
<div className="gif-container">
<div className="circle-background"></div>
<img
src={pulpGif}
alt="Confused reaction"
className="reaction-gif"
/>
</div>
<Typography variant="body1" gutterBottom>
No movies found in your diary. Please check your Letterboxd
username or CSV file.
</Typography>
<Button
variant="contained"
className="back-button"
onClick={handleGoBack}
>
Go Back
</Button>
</Box>
)}
</Paper>
</Container>

View File

@@ -7,103 +7,15 @@ import {
Button,
CircularProgress,
TextField,
makeStyles,
} from "@material-ui/core";
import { CloudUploadOutlined } from "@material-ui/icons";
import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined";
import { useDropzone } from "react-dropzone";
import axios from "axios";
const useStyles = makeStyles((theme) => ({
root: {
padding: theme.spacing(4),
backgroundColor: "#14181c",
minHeight: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
card: {
width: "100%",
maxWidth: "600px",
backgroundColor: "#1c1f23",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[3],
padding: theme.spacing(4),
},
title: {
marginBottom: theme.spacing(3),
fontWeight: "bold",
color: "#ffffff",
},
dropzone: {
border: `2px dashed #00A346`,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(4),
textAlign: "center",
cursor: "pointer",
"&:hover": {
backgroundColor: "#1f252a",
},
},
dropzoneIcon: {
marginBottom: theme.spacing(2),
color: "#00A346",
},
dropzoneText: {
marginBottom: theme.spacing(2),
color: "#ffffff",
},
progressContainer: {
marginTop: theme.spacing(2),
},
submitButton: {
color: "#fff",
fontSize: "0.8rem",
fontWeight: "900",
textTransform: "uppercase",
letterSpacing: "0.04em",
lineHeight: "2.8rem",
display: "inline-block",
cursor: "pointer",
padding: "0 1rem",
border: "0",
borderRadius: "4px",
outline: "none",
background: "#526e89",
transition: "background-color 0.3s ease",
"&:hover": {
backgroundColor: "#1caff2",
},
},
overviewText: {
color: "#456",
marginTop: theme.spacing(4),
padding: theme.spacing(3),
},
usernameField: {
marginBottom: theme.spacing(3),
"& .MuiOutlinedInput-root": {
color: "#ffffff",
"& fieldset": {
borderColor: "#00A346",
},
"&:hover fieldset": {
borderColor: "#00A346",
},
"&.Mui-focused fieldset": {
borderColor: "#1caff2",
},
},
"& .MuiInputLabel-root": {
color: "#ffffff",
},
},
}));
const apiUrl = process.env.REACT_APP_API_URL;
const UploadDiary = () => {
const classes = useStyles();
const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState("");
@@ -112,7 +24,7 @@ const UploadDiary = () => {
useEffect(() => {
const checkCSVFile = async () => {
try {
const response = await axios.get("http://localhost:5000/api/check-csv");
const response = await axios.get(`${apiUrl}/api/check-csv`);
if (response.data.fileExists) {
navigate("/PosterSelector");
}
@@ -136,16 +48,14 @@ const UploadDiary = () => {
setUploading(true);
try {
if (username) {
// Here you would implement the logic to fetch the diary from Letterboxd
// For now, we'll just simulate a successful fetch
console.log(`Fetching diary for user: ${username}`);
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
navigate("/PosterSelector");
} else if (file) {
const formData = new FormData();
formData.append("file", file);
await axios.post("http://localhost:5000/api/upload-csv", formData, {
await axios.post(`${apiUrl}/api/upload-csv`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
@@ -162,13 +72,13 @@ const UploadDiary = () => {
};
return (
<Container className={classes.root}>
<div className={classes.card}>
<Typography variant="h4" className={classes.title}>
<Container className="upload-diary">
<div className="upload-card">
<Typography variant="h4" className="title">
Upload Letterboxd Diary
</Typography>
<TextField
className={classes.usernameField}
className="username-field"
label="Letterboxd Username"
variant="outlined"
fullWidth
@@ -176,42 +86,33 @@ const UploadDiary = () => {
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your Letterboxd username"
/>
<Typography
variant="body2"
style={{ color: "#ffffff", marginBottom: "1rem" }}
>
<Typography variant="body2" className="or-text">
Or upload a CSV file:
</Typography>
<div {...getRootProps()} className={classes.dropzone}>
<div {...getRootProps()} className="dropzone">
<input {...getInputProps()} />
{file ? (
<>
<CloudDoneOutlinedIcon
className={classes.dropzoneIcon}
style={{ fontSize: "4rem" }}
/>
<Typography variant="h6" className={classes.dropzoneText}>
<CloudDoneOutlinedIcon className="dropzone-icon" />
<Typography variant="h6" className="dropzone-text">
Your file has been uploaded: {file.name}
</Typography>
</>
) : (
<>
<CloudUploadOutlined
className={classes.dropzoneIcon}
style={{ fontSize: "4rem" }}
/>
<Typography variant="h6" className={classes.dropzoneText}>
<CloudUploadOutlined className="dropzone-icon" />
<Typography variant="h6" className="dropzone-text">
Drag and drop a CSV file here or click to select
</Typography>
</>
)}
</div>
<Grid container justify="center" className={classes.progressContainer}>
<Grid container justify="center" className="progress-container">
{uploading ? (
<CircularProgress />
) : (
<Button
className={classes.submitButton}
className="submit-button"
variant="contained"
onClick={handleUpload}
disabled={!username && !file}
@@ -221,7 +122,7 @@ const UploadDiary = () => {
)}
</Grid>
</div>
<Typography variant="body1" className={classes.overviewText}>
<Typography variant="body1" className="overview-text">
This tool allows you to easily import your Letterboxd diary and select
your favorite movie poster. Follow these simple steps:
<br />

View File

@@ -1,3 +1,8 @@
@use "./scss/uploadDiary";
@use "./scss/posterSelector";
@use "./scss/posterGallery";
@use "./scss/cart";
.App {
text-align: center;
}

View File

@@ -16,6 +16,7 @@ code {
:root {
--background: #fff;
--bgComp: #1c1f23;
--foreground: #2c3e50;
--primary: #1caff2;
--secondary: #000;

166
src/styles/scss/_cart.scss Normal file
View File

@@ -0,0 +1,166 @@
// Variables
$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 {
margin-top: 2rem;
margin-bottom: 4rem;
display: flex;
gap: $gap-spacing;
.poster-list {
flex: 2;
margin-right: 2rem;
max-height: calc(100vh - 200px);
overflow-y: auto;
.cart-title {
color: $text-white;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 400;
}
.poster-card {
display: flex;
margin-bottom: 1rem;
background-color: $background-dark;
border-radius: 4px;
overflow: hidden;
.poster-media {
width: 120px;
height: 180px;
object-fit: cover;
border-radius: 4px 0 0 4px;
}
.poster-content {
flex-grow: 1;
padding: 1rem;
display: flex;
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;
align-items: center;
justify-content: center;
padding: 0.5rem;
border-radius: 4px;
background-color: transparent;
border: 1px solid $text-muted;
color: $text-white;
transition: background-color 0.3s ease;
&:hover {
background-color: $background-light;
cursor: pointer;
}
&:not(:last-child) {
margin-right: 0.5rem;
}
}
}
}
}
}
.action-panel {
flex: 1;
position: sticky;
top: 2rem;
height: fit-content;
padding: 1.5rem;
background-color: $background-light;
border-radius: 8px;
.action-title {
color: $text-white;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.form-control {
width: 100%;
margin-bottom: 1.5rem;
color: $text-white;
select {
background-color: $background-dark;
color: $text-white;
padding: 0.5rem;
border: 1px solid $text-muted;
border-radius: 4px;
}
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
.primary-button,
.secondary-button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&.primary-button {
background-color: $primary-color;
color: $text-white;
&:hover {
background-color: darken($primary-color, 10%);
}
&:disabled {
background-color: darken($primary-color, 20%);
cursor: not-allowed;
}
}
&.secondary-button {
background-color: $secondary-color;
color: $text-white;
&:hover {
background-color: darken($secondary-color, 10%);
}
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
// Variables
$primary-color: #00a346;
$background-dark: #1c1f23;
$text-white: #ffffff;
$hover-scale: 1.05;
$loading-height: 50vh;
.poster-gallery {
margin-top: 2rem;
margin-bottom: 2rem;
.content-paper {
padding: 1.5rem;
border-radius: 4px;
background-color: $background-dark;
}
.title {
color: $text-white;
margin-bottom: 1.5rem;
font-size: 2rem;
font-weight: 400;
}
.poster-card {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
background-color: transparent;
box-shadow: none;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale($hover-scale);
}
&.selected-poster {
border: 4px solid $primary-color;
border-radius: 4px;
}
.poster-image {
padding-top: 150%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
border-radius: 4px;
transition: transform 0.3s ease-in-out;
}
.check-icon {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background-color: $primary-color;
color: $text-white;
border-radius: 50%;
padding: 0.25rem;
}
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: $loading-height;
}
}

View File

@@ -0,0 +1,192 @@
// Variables
$primary-color: #00a346;
$secondary-color: #667788;
$background-dark: #1c1f23;
$text-white: #ffffff;
$text-gray: #667788;
$border-color: #667788;
$hover-color: rgba(102, 119, 136, 0.2);
.poster-selector {
padding: 0 2rem;
max-width: 100% !important; // Override MUI Container
margin: 2rem auto 4rem !important;
.content-paper {
padding: 1.5rem;
border-radius: 4px;
background-color: $background-dark !important;
}
.title {
margin-bottom: 1.5rem;
color: $text-white;
}
.username {
color: $primary-color;
margin-bottom: 1rem;
}
.movies-list {
max-height: 800px;
min-height: 900px;
padding: 0;
}
.movie-item {
margin-bottom: 1rem;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
&:hover {
background-color: $hover-color;
}
&:not(:last-child) {
border-bottom: 1px solid $border-color;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
// Override MUI ListItem styles
&.MuiListItem-root {
padding: 0.5rem !important;
}
.MuiGrid-container {
display: flex;
justify-content: space-between;
width: 100%;
}
.movie-poster {
width: 50px;
height: 75px;
object-fit: cover;
margin-right: 1rem;
border-radius: 4px;
}
.movie-info {
flex-grow: 1;
max-width: calc(100% - 110px);
}
.movie-title {
color: $text-white;
font-family: "TiemposTextWeb-Semibold, Georgia, serif";
font-size: 1.38461538rem;
font-weight: 400;
transition: color 0.3s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
color: $primary-color;
}
}
.movie-year {
color: $text-gray;
}
.watched-date {
color: $text-gray;
}
.watched-day {
font-size: 2rem;
line-height: 1;
margin-bottom: 0.25rem;
}
.watched-month {
font-size: 0.875rem;
}
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 1.5rem;
// Override MUI Pagination styles
.MuiPagination-root {
.MuiPaginationItem-root {
color: $text-white;
&.Mui-selected {
background-color: $secondary-color;
}
}
}
}
.progress-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 200px;
.MuiLinearProgress-root {
width: 100%;
margin-bottom: 1rem;
}
}
.progress-label {
margin-top: 1rem;
color: $text-white;
}
.no-movies-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
}
.gif-container {
display: flex;
justify-content: center;
position: relative;
width: 200px;
height: 200px;
margin-bottom: 1rem;
}
.circle-background {
position: absolute;
width: 120%;
height: 120%;
border-radius: 50%;
background-color: $text-white;
clip-path: inset(0 0 25% 0);
bottom: -28%;
}
.reaction-gif {
position: absolute;
width: 100%;
height: 100%;
object-fit: contain;
}
.back-button {
margin-top: 1rem;
background-color: $primary-color !important;
color: $text-white !important;
transition: background-color 0.3s ease;
&:hover {
background-color: darken($primary-color, 10%) !important;
}
}
}

View File

@@ -0,0 +1,112 @@
.upload-diary {
padding: 3rem;
background-color: #14181c;
min-height: 100vh;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
.upload-card {
width: 100%;
max-width: 600px;
background-color: #1c1f23;
border-radius: 4px;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2);
padding: 2rem;
.title {
margin-bottom: 1.5rem;
font-weight: bold;
color: #ffffff;
}
.username-field {
margin-bottom: 1.5rem;
.MuiOutlinedInput-root {
color: #ffffff;
fieldset {
border-color: #00a346 !important;
}
&:hover fieldset {
border-color: #00a346 !important;
}
&.Mui-focused fieldset {
border-color: #1caff2 !important;
}
}
.MuiInputLabel-root {
color: #ffffff !important;
}
}
.or-text {
color: #ffffff;
margin-bottom: 1rem;
}
.dropzone {
border: 2px dashed #00a346;
border-radius: 4px;
padding: 2rem;
text-align: center;
cursor: pointer;
&:hover {
background-color: #1f252a;
}
.dropzone-icon {
margin-bottom: 1rem;
color: #00a346;
font-size: 4rem;
}
.dropzone-text {
margin-bottom: 1rem;
color: #ffffff;
}
}
.progress-container {
margin-top: 1rem;
}
.submit-button {
color: #fff;
font-size: 0.8rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 2.8rem;
display: inline-block;
cursor: pointer;
padding: 0 1rem;
border: 0;
border-radius: 4px;
outline: none;
background: #526e89;
transition: background-color 0.3s ease;
&:hover {
background-color: #1caff2;
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
}
.overview-text {
color: #456;
margin-top: 2rem;
padding: 1.5rem;
}
}