Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.3 KiB
React
81 lines
2.3 KiB
React
import { useEffect, useState } from "react";
|
|
import { Link, useParams } from "react-router-dom";
|
|
import { api } from "../api.js";
|
|
|
|
function formatDate(value) {
|
|
if (!value) return "";
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) return value;
|
|
return parsed.toLocaleString();
|
|
}
|
|
|
|
export default function Message() {
|
|
const { id } = useParams();
|
|
const [message, setMessage] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError(null);
|
|
api(`/api/messages/${encodeURIComponent(id)}`)
|
|
.then((data) => {
|
|
if (!cancelled) setMessage(data);
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled) setError(err.message);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [id]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Link to="/" className="btn btn-ghost btn-sm">
|
|
← Inbox
|
|
</Link>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-16">
|
|
<span className="loading loading-spinner loading-lg" />
|
|
</div>
|
|
) : error ? (
|
|
<div role="alert" className="alert alert-error text-sm">
|
|
{error}
|
|
</div>
|
|
) : message ? (
|
|
<article className="card bg-base-100 shadow">
|
|
<div className="card-body gap-4">
|
|
<h1 className="card-title text-xl">{message.subject}</h1>
|
|
<dl className="grid gap-1 text-sm">
|
|
<div className="flex gap-2">
|
|
<dt className="opacity-60">From</dt>
|
|
<dd>{message.from}</dd>
|
|
</div>
|
|
{message.to ? (
|
|
<div className="flex gap-2">
|
|
<dt className="opacity-60">To</dt>
|
|
<dd>{message.to}</dd>
|
|
</div>
|
|
) : null}
|
|
<div className="flex gap-2">
|
|
<dt className="opacity-60">Date</dt>
|
|
<dd>{formatDate(message.date)}</dd>
|
|
</div>
|
|
</dl>
|
|
<div className="divider my-0" />
|
|
<div className="message-body text-sm">
|
|
{message.body || message.snippet || "(empty message)"}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|