- 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>
237 lines
7.7 KiB
JavaScript
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;
|