Refactor: responsive upload page, fix warnings, CI/CD setup
- Fix MUI v4/React 18 StrictMode warnings (justify→justifyContent, Button migration) - Responsive UploadDiary: footer visible without scroll, mobile layout centered, overview text hidden on mobile - Fix Redux selector memoization with createSelector - RSS sync: auto-refresh PosterSelector after sync, Snackbar feedback with film count - Add .gitea/workflows/deploy.yml for tag-based CI/CD - Fix nginx config (default.conf), increase Node heap for Docker build - Update README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
236
src/pages/UploadDiary.js
Normal file
236
src/pages/UploadDiary.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
Container,
|
||||
Grid,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
Box,
|
||||
Link,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
} from "@material-ui/core";
|
||||
import { Button } from "@mui/material";
|
||||
import { CloudUploadOutlined } from "@material-ui/icons";
|
||||
import CloudDoneOutlinedIcon from "@mui/icons-material/CloudDoneOutlined";
|
||||
import SyncIcon from "@material-ui/icons/Sync";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import api from "../services/api";
|
||||
import { setUsername, setDataSource } from "../services/action";
|
||||
const VERSION = require("../../package.json").version;
|
||||
|
||||
const UploadDiary = () => {
|
||||
const [file, setFile] = useState(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [usernameInput, setUsernameInput] = useState("");
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [syncError, setSyncError] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const storedUsername = useSelector((state) => state.username);
|
||||
|
||||
useEffect(() => {
|
||||
if (storedUsername) setUsernameInput(storedUsername);
|
||||
}, [storedUsername]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkCSVFile = async () => {
|
||||
try {
|
||||
const response = await api.get("/api/check-csv");
|
||||
if (response.data.fileExists) {
|
||||
navigate("/PosterSelector");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking CSV file:", error);
|
||||
}
|
||||
};
|
||||
checkCSVFile();
|
||||
}, [navigate]);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFile(acceptedFiles[0]);
|
||||
},
|
||||
maxFiles: 1,
|
||||
accept: ".csv",
|
||||
});
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
await api.post("/api/upload-csv", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
dispatch(setDataSource('csv'));
|
||||
navigate("/PosterSelector");
|
||||
} catch (error) {
|
||||
console.error("Error processing diary:", error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRssSync = async () => {
|
||||
const username = usernameInput.trim();
|
||||
if (!username) return;
|
||||
setSyncing(true);
|
||||
setSyncError(null);
|
||||
try {
|
||||
const res = await api.post("/api/sync-rss", { username });
|
||||
dispatch(setUsername(username));
|
||||
if (res.data.fresh) dispatch(setDataSource('rss'));
|
||||
navigate("/PosterSelector");
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.error || "Could not fetch RSS feed.";
|
||||
setSyncError(msg);
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="upload-page">
|
||||
<Container className="upload-diary">
|
||||
<div className="upload-card-wrapper">
|
||||
<div className="upload-card">
|
||||
<div className="logo-container">
|
||||
<img src="/icon.svg" alt="logo" className="logo" />
|
||||
</div>
|
||||
|
||||
{/* CSV upload */}
|
||||
<Typography variant="h4" className="title">
|
||||
Upload Letterboxd Diary
|
||||
</Typography>
|
||||
<div {...getRootProps()} className="dropzone">
|
||||
<input {...getInputProps()} />
|
||||
{file ? (
|
||||
<>
|
||||
<CloudDoneOutlinedIcon className="dropzone-icon" />
|
||||
<Typography variant="h6" className="dropzone-text">
|
||||
Your file has been uploaded: {file.name}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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 justifyContent="center" className="progress-container">
|
||||
{uploading ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<Button
|
||||
className="submit-button"
|
||||
variant="contained"
|
||||
onClick={handleUpload}
|
||||
disabled={!file}
|
||||
>
|
||||
SUBMIT
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="upload-divider">
|
||||
<span className="upload-divider-line" />
|
||||
<span className="upload-divider-text">or</span>
|
||||
<span className="upload-divider-line" />
|
||||
</div>
|
||||
|
||||
{/* RSS sync */}
|
||||
<Typography variant="body1" className="rss-label">
|
||||
Sync from Letterboxd RSS
|
||||
</Typography>
|
||||
<div className="rss-row">
|
||||
<TextField
|
||||
className="rss-input"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
placeholder="your-username"
|
||||
value={usernameInput}
|
||||
onChange={(e) => { setUsernameInput(e.target.value); setSyncError(null); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleRssSync()}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<span className="rss-url-prefix">letterboxd.com/</span>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="rss-sync-button"
|
||||
variant="contained"
|
||||
onClick={handleRssSync}
|
||||
disabled={syncing || !usernameInput.trim()}
|
||||
startIcon={syncing ? <CircularProgress size={14} color="inherit" /> : <SyncIcon />}
|
||||
>
|
||||
{syncing ? "Syncing…" : "Sync"}
|
||||
</Button>
|
||||
</div>
|
||||
{syncError && (
|
||||
<Typography className="rss-error">{syncError}</Typography>
|
||||
)}
|
||||
<Typography className="rss-note">
|
||||
Syncs your ~50 most recent diary entries via the public RSS feed.
|
||||
Use CSV export for full history.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 />
|
||||
<strong>1. Export your diary:</strong>{" "}
|
||||
<Link
|
||||
href="https://letterboxd.com/data/export/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link"
|
||||
>
|
||||
Export your Letterboxd data
|
||||
</Link>{" "}
|
||||
and extract the <code>diary.csv</code> file from the ZIP.
|
||||
<br />
|
||||
<strong>2. Upload the CSV:</strong> Drop your <code>diary.csv</code>{" "}
|
||||
file above or click to select it.
|
||||
<br />
|
||||
<strong>3. Poster Selection:</strong> After processing, you'll be
|
||||
redirected to the calendar where you can browse and choose your
|
||||
favorite poster for each movie.
|
||||
</Typography>
|
||||
</Container>
|
||||
<Box component="footer" className="footer">
|
||||
<Box className="footer-text">
|
||||
<Typography variant="body2" style={{ opacity: 0.7 }}>
|
||||
v{VERSION}
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ opacity: 0.5 }}>
|
||||
•
|
||||
</Typography>
|
||||
<Link
|
||||
href="https://github.com/Hugyouu/Letterboxd-Diary-Posters-Picker"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link"
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = 1)}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = 0.7)}
|
||||
>
|
||||
<span>Hugyouu</span>
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadDiary;
|
||||
Reference in New Issue
Block a user