Files
Letterboxd-Diary-Posters-Pi…/src/pages/UploadDiary.js
Hugo Pierret 0dbcdcfcf4 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>
2026-04-26 17:24:38 +02:00

237 lines
7.7 KiB
JavaScript

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;