179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { getRole } from "../api/client";
|
|
import { PDFDownloadLink, Document, Page, Text, View, StyleSheet } from "@react-pdf/renderer";
|
|
import { api } from "../api/client";
|
|
|
|
interface LetterData {
|
|
id: number;
|
|
title: string | null;
|
|
content: string;
|
|
createdAt: string;
|
|
deliverAt: string | null;
|
|
}
|
|
|
|
const pdfStyles = StyleSheet.create({
|
|
page: {
|
|
padding: 60,
|
|
fontFamily: "Helvetica",
|
|
backgroundColor: "#fdf8f0",
|
|
},
|
|
header: {
|
|
marginBottom: 32,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: "#e8d5c0",
|
|
paddingBottom: 16,
|
|
},
|
|
title: {
|
|
fontSize: 22,
|
|
marginBottom: 6,
|
|
color: "#2c1810",
|
|
},
|
|
date: {
|
|
fontSize: 10,
|
|
color: "#6b4f3a",
|
|
},
|
|
content: {
|
|
fontSize: 12,
|
|
lineHeight: 1.8,
|
|
color: "#2c1810",
|
|
},
|
|
footer: {
|
|
position: "absolute",
|
|
bottom: 40,
|
|
left: 60,
|
|
right: 60,
|
|
textAlign: "center",
|
|
fontSize: 10,
|
|
color: "#c9727a",
|
|
},
|
|
});
|
|
|
|
function stripHtml(html: string): string {
|
|
return html
|
|
.replace(/<img[^>]*>/gi, "[image]")
|
|
.replace(/<br\s*\/?>/gi, "\n")
|
|
.replace(/<\/p>/gi, "\n\n")
|
|
.replace(/<\/?(h[1-6]|li|blockquote)[^>]*>/gi, "\n")
|
|
.replace(/<[^>]+>/g, "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/ /g, " ")
|
|
.replace(/\n{3,}/g, "\n\n")
|
|
.trim();
|
|
}
|
|
|
|
function LetterPDF({ letter }: { letter: LetterData }) {
|
|
const dateStr = new Date(letter.createdAt).toLocaleDateString("en-US", {
|
|
weekday: "long", year: "numeric", month: "long", day: "numeric",
|
|
});
|
|
|
|
return (
|
|
<Document>
|
|
<Page size="A4" style={pdfStyles.page}>
|
|
<View style={pdfStyles.header}>
|
|
{letter.title && <Text style={pdfStyles.title}>{letter.title}</Text>}
|
|
<Text style={pdfStyles.date}>{dateStr}</Text>
|
|
</View>
|
|
<Text style={pdfStyles.content}>{stripHtml(letter.content)}</Text>
|
|
<Text style={pdfStyles.footer}>hibb ♡</Text>
|
|
</Page>
|
|
</Document>
|
|
);
|
|
}
|
|
|
|
export default function LetterView() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [letter, setLetter] = useState<LetterData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
api
|
|
.getLetter(parseInt(id, 10))
|
|
.then(setLetter)
|
|
.catch(() => setError("Letter not found."))
|
|
.finally(() => setLoading(false));
|
|
}, [id]);
|
|
|
|
if (loading) {
|
|
return <div className="page" style={{ color: "var(--ink-light)", fontStyle: "italic" }}>Loading...</div>;
|
|
}
|
|
|
|
if (error || !letter) {
|
|
return (
|
|
<div className="page" style={{ color: "var(--ink-light)", fontStyle: "italic" }}>
|
|
{error || "Letter not found."}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const localDate = letter.letterDate ?? letter.createdAt.slice(0, 10);
|
|
const dateStr = new Date(`${localDate}T00:00:00`).toLocaleDateString("en-US", {
|
|
weekday: "long", year: "numeric", month: "long", day: "numeric",
|
|
});
|
|
|
|
const d = new Date();
|
|
const todayLocal = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
const isToday = localDate === todayLocal;
|
|
|
|
return (
|
|
<div className="page">
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 24, flexWrap: "wrap", gap: 12 }}>
|
|
<button className="btn btn-ghost" onClick={() => navigate(-1)} style={{ padding: "6px 14px", fontSize: "0.875rem" }}>
|
|
← Back
|
|
</button>
|
|
<div style={{ display: "flex", gap: 10 }}>
|
|
{isToday && getRole() === "writer" && (
|
|
<button className="btn btn-secondary" onClick={() => navigate("/")}>
|
|
Edit
|
|
</button>
|
|
)}
|
|
<PDFDownloadLink
|
|
document={<LetterPDF letter={letter} />}
|
|
fileName={`letter-${letter.createdAt.slice(0, 10)}.pdf`}
|
|
>
|
|
{({ loading: pdfLoading }) => (
|
|
<button className="btn btn-secondary">
|
|
{pdfLoading ? "Preparing PDF..." : "Download PDF"}
|
|
</button>
|
|
)}
|
|
</PDFDownloadLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card" style={{ padding: 40 }}>
|
|
<h1 style={{ fontFamily: "Lora, serif", fontSize: "1.8rem", color: "var(--ink)", marginBottom: 32 }}>
|
|
{dateStr}
|
|
</h1>
|
|
<div className="letter-body" dangerouslySetInnerHTML={{ __html: letter.content }} />
|
|
<style>{`
|
|
.letter-body {
|
|
font-family: "Lora", serif;
|
|
font-size: 1.05rem;
|
|
line-height: 1.8;
|
|
color: var(--ink);
|
|
}
|
|
.letter-body p { margin-bottom: 1em; }
|
|
.letter-body p:empty::after,
|
|
.letter-body p:has(br:only-child)::after { content: "\\00a0"; }
|
|
.letter-body img { max-width: 100%; border-radius: 8px; margin: 8px 0; }
|
|
.letter-body blockquote {
|
|
border-left: 3px solid var(--accent);
|
|
padding-left: 16px;
|
|
color: var(--ink-light);
|
|
font-style: italic;
|
|
margin: 12px 0;
|
|
}
|
|
`}</style>
|
|
<p style={{ textAlign: "right", color: "var(--heart)", marginTop: 40, fontFamily: "Lora, serif", fontStyle: "italic" }}>
|
|
♡
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|