thread-master/apps/web/src/pages/TodayPage.tsx

832 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { PageHeader } from "../components/layout/PageHeader";
import { Badge, Button, Card, EmptyState } from "../components/ui";
import { useData, useRepos } from "../data/DataContext";
import type {
MentionItem,
OutcomeSummary,
OutboxBundle,
OwnPost,
RadarToday,
ScoutPost,
ThreadsAccount,
TrendItem,
WeeklyCheckup,
} from "../domain/types";
import { useI18n } from "../i18n/I18nContext";
import { useFormatApiError } from "../lib/apiErrors";
import { dismissRadarOnboarding, isRadarOnboardingDismissed } from "../lib/radarOnboarding";
import { loadScoutToday } from "../lib/scoutToday";
function isPendingScout(p: ScoutPost): boolean {
return p.outreach_status === "new" || p.outreach_status === "drafted";
}
function startOfLocalDayNano(): number {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d.getTime() * 1_000_000;
}
type AccountPulse = {
account: ThreadsAccount;
posts: number;
views: number;
likes: number;
replies: number;
topInsight?: string;
};
/**
* 今日儀表板:海巡待回、今日目標、發送、話題、帳號成效。
* 全部接 live repos話題可刷新點話題進靈感 tab。
*/
export function TodayPage() {
const repos = useRepos();
const { tick, refresh } = useData();
const { t, locale } = useI18n();
const formatApiError = useFormatApiError();
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [trendMessage, setTrendMessage] = useState("");
const [refreshingTrends, setRefreshingTrends] = useState(false);
const [syncingPosts, setSyncingPosts] = useState(false);
const [scoutPosts, setScoutPosts] = useState<ScoutPost[]>([]);
const [outbox, setOutbox] = useState<OutboxBundle[]>([]);
const [trends, setTrends] = useState<TrendItem[]>([]);
const [ownPosts, setOwnPosts] = useState<OwnPost[]>([]);
const [mentions, setMentions] = useState<MentionItem[]>([]);
const [accounts, setAccounts] = useState<ThreadsAccount[]>([]);
const [scoutDone, setScoutDone] = useState(0);
const [scoutGoal, setScoutGoal] = useState(8);
const [outcomeSummary, setOutcomeSummary] = useState<OutcomeSummary | null>(null);
const [checkup, setCheckup] = useState<WeeklyCheckup | null>(null);
const [radarToday, setRadarToday] = useState<RadarToday | null>(null);
const [onboardingDismissed, setOnboardingDismissed] = useState(() => isRadarOnboardingDismissed());
const dateLocale = locale === "en" ? "en-US" : "zh-TW";
const todayLabel = new Date().toLocaleDateString(dateLocale, {
month: "numeric",
day: "numeric",
weekday: "short",
});
const load = useCallback(async () => {
setLoading(true);
setError("");
try {
const scoutLocal = loadScoutToday();
setScoutGoal(scoutLocal.goalValue);
const [posts, box, trendList, acc, owns, mentionList, outcomes, latestCheckup, radar] =
await Promise.all([
repos.scout.listPosts(),
repos.outbox.list(),
repos.inspiration.listTrends("all").catch(() => [] as TrendItem[]),
repos.accounts.list(),
repos.ownPosts.list().catch(() => [] as OwnPost[]),
repos.mentions.list().catch(() => [] as MentionItem[]),
repos.growth.getOutcomeSummary("week").catch(() => null),
repos.growth.getLatestCheckup().catch(() => null),
repos.radar.getToday().catch(() => null),
]);
setOutcomeSummary(outcomes);
setCheckup(latestCheckup);
setRadarToday(radar);
const usable = acc.filter((a) => a.is_usable);
setAccounts(usable.length ? usable : acc);
setScoutPosts(posts);
setOutbox(box);
setTrends(
[...trendList]
.sort((a, b) => (b.heat || 0) - (a.heat || 0))
.slice(0, 12),
);
setOwnPosts(owns);
setMentions(mentionList);
// 舊資料沒有發佈時間時不可拿歷史 published 總數冒充今日完成量。
const dayStart = startOfLocalDayNano();
const publishedN = posts.filter(
(p) =>
p.outreach_status === "published" &&
p.published_at != null &&
p.published_at >= dayStart,
).length;
setScoutDone(Math.max(scoutLocal.done, publishedN));
// 舊 API 若不支援全帳列表,分帳 fallback 在背景補齊,不阻塞首屏。
if (!owns.length && usable.length) {
void Promise.all(
usable.slice(0, 6).map((a) => repos.ownPosts.list(a.id).catch(() => [] as OwnPost[])),
).then((chunks) => setOwnPosts(chunks.flat()));
}
if (!mentionList.length && usable[0]?.id) {
void repos.mentions.list(usable[0].id).then(setMentions).catch(() => undefined);
}
} catch (e) {
setError(formatApiError(e, "today.loadFail"));
} finally {
setLoading(false);
}
}, [repos, formatApiError]);
useEffect(() => {
void load();
}, [load, tick]);
useEffect(() => {
const onStore = () => refresh();
window.addEventListener("harbor:store", onStore);
return () => window.removeEventListener("harbor:store", onStore);
}, [refresh]);
const dayStart = startOfLocalDayNano();
const pendingScout = useMemo(
() =>
scoutPosts
.filter(isPendingScout)
.slice()
.sort((a, b) => (b.score || 0) - (a.score || 0)),
[scoutPosts],
);
const pendingMentions = useMemo(
() => mentions.filter((m) => m.status === "pending"),
[mentions],
);
const failedOutbox = useMemo(
() => outbox.filter((o) => o.status === "partial_failed"),
[outbox],
);
const runningOutbox = useMemo(
() => outbox.filter((o) => o.status === "scheduling" || o.status === "active"),
[outbox],
);
const sentToday = useMemo(() => {
return outbox.filter((o) => {
if (o.status !== "completed") return false;
return o.updated_at >= dayStart;
}).length;
}, [outbox, dayStart]);
const goalPct = Math.min(100, Math.round((scoutDone / Math.max(1, scoutGoal)) * 100));
const topicCards = useMemo(() => trends.slice(0, 6), [trends]);
const accountPulses = useMemo((): AccountPulse[] => {
const byAcc = new Map<string, OwnPost[]>();
for (const p of ownPosts) {
const list = byAcc.get(p.account_id) || [];
list.push(p);
byAcc.set(p.account_id, list);
}
const rows: AccountPulse[] = [];
for (const acc of accounts) {
const posts = byAcc.get(acc.id) || [];
if (!posts.length && accounts.length > 3) continue;
const views = posts.reduce((s, p) => s + (p.view_count || 0), 0);
const likes = posts.reduce((s, p) => s + (p.like_count || 0), 0);
const replies = posts.reduce((s, p) => s + (p.reply_count || 0), 0);
const top = posts.slice().sort((a, b) => (b.view_count || 0) - (a.view_count || 0))[0];
rows.push({
account: acc,
posts: posts.length,
views,
likes,
replies,
topInsight: top?.insight || top?.formula_summary,
});
}
if (!rows.length) {
for (const [accId, posts] of byAcc) {
const acc =
accounts.find((a) => a.id === accId) ||
({
id: accId,
username: accId,
display_name: accId,
connection: "connected",
is_usable: true,
avatar_color: "var(--hb-muted)",
} as ThreadsAccount);
rows.push({
account: acc,
posts: posts.length,
views: posts.reduce((s, p) => s + (p.view_count || 0), 0),
likes: posts.reduce((s, p) => s + (p.like_count || 0), 0),
replies: posts.reduce((s, p) => s + (p.reply_count || 0), 0),
topInsight: posts[0]?.insight,
});
}
}
return rows
.sort((a, b) => b.views - a.views || b.likes - a.likes)
.slice(0, 4);
}, [accounts, ownPosts]);
const pendingPreview = pendingScout.slice(0, 5);
const hasOutcome = Boolean(
outcomeSummary &&
(outcomeSummary.reach ||
outcomeSummary.conversations ||
outcomeSummary.follows_possible ||
outcomeSummary.follows_confirmed ||
outcomeSummary.conversions),
);
// 三步引導只從 radarToday.empty_reason 推斷,不額外打 API
// 有結果或曾巡過(含暫停/失敗/沒命中)代表訂閱已建立。
const onboardingProfileDone = Boolean(
radarToday && radarToday.empty_reason !== "no_profile",
);
const onboardingWatchDone = Boolean(
radarToday &&
(radarToday.stats.total > 0 ||
["not_swept_yet", "sweep_failed", "no_hit", "all_watches_paused"].includes(
radarToday.empty_reason || "",
)),
);
const onboardingOpportunityDone = Boolean(radarToday && radarToday.stats.total > 0);
const onboardingAllDone =
onboardingProfileDone && onboardingWatchDone && onboardingOpportunityDone;
const showOnboarding = Boolean(radarToday) && !onboardingAllDone && !onboardingDismissed;
useEffect(() => {
if (onboardingAllDone && !onboardingDismissed) {
dismissRadarOnboarding();
setOnboardingDismissed(true);
}
}, [onboardingAllDone, onboardingDismissed]);
async function onRefreshTrends() {
setRefreshingTrends(true);
setError("");
setTrendMessage("");
try {
const list = await repos.inspiration.refreshTrends("all");
if (!list.length) {
throw new Error(t("today.trendsFail"));
}
setTrends(
[...list].sort((a, b) => (b.heat || 0) - (a.heat || 0)).slice(0, 12),
);
setTrendMessage(t("today.trendsUpdated", { n: list.length }));
refresh();
} catch (e) {
setError(formatApiError(e, "today.trendsFail"));
} finally {
setRefreshingTrends(false);
}
}
async function onSyncOwnPosts() {
const acc = accounts[0];
if (!acc) {
setError(t("today.needAccount"));
return;
}
setSyncingPosts(true);
setError("");
try {
const list = await repos.ownPosts.sync(acc.id);
setOwnPosts((prev) => {
const others = prev.filter((p) => p.account_id !== acc.id);
return [...list, ...others];
});
refresh();
} catch (e) {
setError(formatApiError(e, "today.syncPostsFail"));
} finally {
setSyncingPosts(false);
}
}
function topicHref(label: string) {
const q = new URLSearchParams({ tab: "inspire", topic: label });
return `/app/studio?${q.toString()}`;
}
if (loading && !scoutPosts.length && !trends.length && !outbox.length) {
return (
<>
<PageHeader title={t("nav.today")} description={todayLabel} />
<p className="text-muted">{t("common.loading")}</p>
</>
);
}
return (
<>
<PageHeader title={t("nav.today")} description={todayLabel} />
{error ? (
<p className="hb-form-error" role="alert">
{error}
</p>
) : null}
{showOnboarding ? (
<Card title={t("today.onboarding.title")} className="hb-stack" style={{ marginBottom: "1rem" }}>
<ul className="hb-today-list">
{[
{
key: "profile",
done: onboardingProfileDone,
to: "/app/policy",
label: t("today.onboarding.step.profile"),
hint: t("today.onboarding.step.profileHint"),
},
{
key: "watch",
done: onboardingWatchDone,
to: "/app/radar/watches",
label: t("today.onboarding.step.watch"),
hint: t("today.onboarding.step.watchHint"),
},
{
key: "opportunity",
done: onboardingOpportunityDone,
to: "/app/radar",
label: t("today.onboarding.step.opportunity"),
hint: t("today.onboarding.step.opportunityHint"),
},
].map((step) => (
<li key={step.key}>
<Link to={step.to} className="hb-today-list__item">
<span className="hb-today-list__meta">
<Badge tone={step.done ? "success" : "neutral"}>
{step.done ? t("today.onboarding.step.done") : t("today.onboarding.step.go")}
</Badge>{" "}
{step.label}
</span>
<span className="hb-today-list__text">{step.hint}</span>
</Link>
</li>
))}
</ul>
<Button
type="button"
variant="ghost"
onClick={() => {
dismissRadarOnboarding();
setOnboardingDismissed(true);
}}
>
{t("today.onboarding.dismiss")}
</Button>
</Card>
) : null}
<Card title={t("today.radar.title")} className="hb-stack" style={{ marginBottom: "1rem" }}>
{radarToday && radarToday.stats.total > 0 ? (
<>
<div className="hb-today-metrics" role="group" aria-label={t("today.radar.title")}>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.radar.total")}</span>
<span className="hb-today-metric__value">{radarToday.stats.total}</span>
</div>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.radar.high")}</span>
<span className="hb-today-metric__value">{radarToday.stats.high}</span>
</div>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.radar.mid")}</span>
<span className="hb-today-metric__value">{radarToday.stats.mid}</span>
</div>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.radar.low")}</span>
<span className="hb-today-metric__value">{radarToday.stats.low}</span>
</div>
</div>
<Link className="hb-btn hb-btn--secondary" to="/app/radar">
{t("today.radar.open")}
</Link>
</>
) : (
<p className="text-muted" style={{ margin: 0, fontSize: "var(--hb-text-sm)" }}>
{radarToday?.empty_hint || t("today.radar.empty")}{" "}
<Link
to={
radarToday?.empty_reason === "no_profile"
? "/app/policy"
: "/app/radar/watches"
}
>
{radarToday?.empty_reason === "no_profile"
? t("today.radar.goProfile")
: t("today.radar.goWatches")}
</Link>
</p>
)}
</Card>
<Card title={t("today.outcome.title")} className="hb-stack" style={{ marginBottom: "1rem" }}>
<div className="hb-today-metrics" role="group" aria-label={t("today.outcome.title")}>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.outcome.reach")}</span>
<span className="hb-today-metric__value">{outcomeSummary?.reach ?? 0}</span>
</div>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.outcome.conversations")}</span>
<span className="hb-today-metric__value">{outcomeSummary?.conversations ?? 0}</span>
</div>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.outcome.follows")}</span>
<span className="hb-today-metric__value">
{outcomeSummary?.follows_possible ?? 0}
{outcomeSummary?.follows_confirmed ? (
<span className="hb-today-metric__den">
{t("today.outcome.followsConfirmedHint", { n: outcomeSummary.follows_confirmed })}
</span>
) : null}
</span>
<span className="hb-today-metric__hint">{t("today.outcome.followsHint")}</span>
</div>
<div className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.outcome.conversions")}</span>
<span className="hb-today-metric__value">{outcomeSummary?.conversions ?? 0}</span>
{outcomeSummary?.conversion_amount ? (
<span className="hb-today-metric__hint">
${Math.round(outcomeSummary.conversion_amount)}
</span>
) : null}
</div>
</div>
{!hasOutcome ? (
<p className="text-muted" style={{ margin: 0, fontSize: "var(--hb-text-sm)" }}>
{t("today.outcome.emptyHint")}{" "}
<Link to="/app/scout">{t("today.goScout")}</Link>
</p>
) : null}
{checkup ? (
<div style={{ marginTop: "0.75rem" }}>
<p className="text-muted" style={{ margin: 0 }}>
{t("today.checkup.prefix")}
{checkup.summary}
</p>
<ul style={{ margin: "0.5rem 0 0", paddingLeft: "1.2rem" }}>
{checkup.actions.slice(0, 3).map((a) => (
<li key={a.title}>
<Link
to={
a.deeplink === "scout"
? "/app/scout"
: a.deeplink === "studio_compose"
? "/app/studio?tab=compose"
: a.deeplink === "studio_inspire"
? "/app/studio?tab=inspire"
: a.deeplink === "crew_persona"
? "/app/crew"
: a.deeplink === "outbox"
? "/app/outbox"
: "/app/today"
}
>
{a.title}
</Link>
<span className="text-muted"> {a.reason}</span>
</li>
))}
</ul>
</div>
) : (
<p className="text-muted" style={{ margin: "0.75rem 0 0", fontSize: "var(--hb-text-sm)" }}>
{t("today.checkup.empty")}
</p>
)}
</Card>
<div className="hb-today-actions">
<Link to="/app/scout">
<Button type="button" variant={pendingScout.length ? "primary" : "ghost"}>
{pendingScout.length
? t("today.pendingRepliesN", { n: pendingScout.length })
: t("today.pendingReplies")}
</Button>
</Link>
<Link to="/app/studio?tab=compose">
<Button type="button" variant="ghost">
{t("today.newThread")}
</Button>
</Link>
<Button type="button" variant="ghost" onClick={() => void load()} disabled={loading}>
{t("today.reload")}
</Button>
</div>
{/* 數值列 */}
<div className="hb-today-metrics" role="group" aria-label={t("today.metricsAria")}>
<Link to="/app/scout" className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.metric.pending")}</span>
<span className="hb-today-metric__value">{pendingScout.length}</span>
<span className="hb-today-metric__hint">{t("today.metric.pendingHint")}</span>
</Link>
<div className="hb-today-metric" title={t("today.metric.doneGoalHint")}>
<span className="hb-today-metric__label">{t("today.metric.doneGoal")}</span>
<span className="hb-today-metric__value">
{scoutDone}
<span className="hb-today-metric__den">/{scoutGoal}</span>
</span>
<div className="hb-progress hb-progress--sm" aria-hidden>
<div className="hb-progress__bar" style={{ width: `${goalPct}%` }} />
</div>
</div>
<Link to="/app/outbox" className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.metric.sentToday")}</span>
<span className="hb-today-metric__value">{sentToday}</span>
<span className="hb-today-metric__hint">
{runningOutbox.length
? t("today.metric.running", { n: runningOutbox.length })
: t("today.metric.sentDone")}
</span>
</Link>
<Link
to="/app/outbox"
className={`hb-today-metric${failedOutbox.length ? " is-alert" : ""}`}
>
<span className="hb-today-metric__label">{t("today.metric.failed")}</span>
<span className="hb-today-metric__value">{failedOutbox.length}</span>
<span className="hb-today-metric__hint">
{failedOutbox.length ? t("today.metric.needAction") : t("today.metric.ok")}
</span>
</Link>
<Link to="/app/studio?tab=mentions" className="hb-today-metric">
<span className="hb-today-metric__label">{t("today.metric.mentions")}</span>
<span className="hb-today-metric__value">{pendingMentions.length}</span>
<span className="hb-today-metric__hint">{t("today.metric.mentionsHint")}</span>
</Link>
</div>
{/* 待回覆 · 海巡 */}
<Card title={t("today.pending.title", { n: pendingScout.length })}>
{pendingPreview.length === 0 ? (
<EmptyState
title={t("today.pending.empty")}
action={
<Link to="/app/scout">
<Button type="button">{t("today.goScout")}</Button>
</Link>
}
/>
) : (
<div className="hb-stack">
<ul className="hb-today-list">
{pendingPreview.map((p) => (
<li key={p.id}>
<Link to="/app/scout" className="hb-today-list__item">
<span className="hb-today-list__meta">
@{p.author}
{p.search_tag ? ` · ${p.search_tag}` : ""}
{typeof p.score === "number" ? ` · ${Math.round(p.score)}` : ""}
{p.outreach_status === "drafted" ? (
<>
{" · "}
<Badge tone="brand">{t("today.badge.drafted")}</Badge>
</>
) : null}
</span>
<span className="hb-today-list__text">
{p.text.slice(0, 96)}
{p.text.length > 96 ? "…" : ""}
</span>
{p.opportunity ? (
<span className="hb-today-list__meta">{p.opportunity}</span>
) : null}
</Link>
</li>
))}
</ul>
{pendingScout.length > pendingPreview.length ? (
<p className="text-muted" style={{ margin: 0, fontSize: "var(--hb-text-sm)" }}>
{t("today.pending.more", { n: pendingScout.length - pendingPreview.length })}{" "}
<Link to="/app/scout">{t("today.pending.handle")}</Link>
</p>
) : (
<Link to="/app/scout">
<Button type="button" variant="ghost">
{t("today.pending.start")}
</Button>
</Link>
)}
</div>
)}
</Card>
{/* 找話題 */}
<Card title={t("today.topics.title")}>
<div className="hb-today-actions" style={{ margin: "0 0 0.5rem" }}>
<Button
type="button"
variant="ghost"
disabled={refreshingTrends}
onClick={() => void onRefreshTrends()}
>
{refreshingTrends ? t("common.loading") : t("today.refreshTopics")}
</Button>
{trendMessage ? (
<span className="text-muted" role="status">
{trendMessage}
</span>
) : null}
</div>
{topicCards.length === 0 ? (
<EmptyState
title={t("today.topics.empty")}
action={
<div className="hb-today-actions" style={{ margin: 0 }}>
<Button
type="button"
onClick={() => void onRefreshTrends()}
disabled={refreshingTrends}
>
{refreshingTrends ? t("common.loading") : t("today.refreshTopics")}
</Button>
<Link to="/app/studio?tab=inspire">
<Button type="button" variant="ghost">
{t("today.goStudio")}
</Button>
</Link>
</div>
}
/>
) : (
<div className="hb-stack">
<ul className="hb-today-list">
{topicCards.map((item) => (
<li key={item.id}>
<Link to={topicHref(item.label)} className="hb-today-list__item">
<span className="hb-today-list__meta">
{item.label}
{typeof item.heat === "number" && item.heat > 0 ? (
<Badge tone="brand">{t("today.heat", { n: item.heat })}</Badge>
) : null}
{item.source_label ? (
<span className="text-muted"> · {item.source_label}</span>
) : null}
</span>
<span className="hb-today-list__text">
{item.summary?.slice(0, 100) ||
item.samples?.[0] ||
t("today.topicAngle")}
{(item.summary?.length || 0) > 100 ? "…" : ""}
</span>
</Link>
</li>
))}
</ul>
<div className="hb-today-actions" style={{ margin: 0 }}>
<Link to="/app/studio?tab=inspire">
<Button type="button" variant="ghost">
{t("today.moreInspire")}
</Button>
</Link>
{topicCards[0] ? (
<Link to={topicHref(topicCards[0].label)}>
<Button type="button" variant="ghost">
{t("today.useTopic")}
</Button>
</Link>
) : null}
</div>
</div>
)}
</Card>
{/* 發送摘要 */}
<Card title={t("today.outbox.title")}>
{failedOutbox.length === 0 && runningOutbox.length === 0 && sentToday === 0 ? (
<p className="text-muted" style={{ margin: 0 }}>
{t("today.outbox.empty")}{" "}
<Link to="/app/studio?tab=compose">{t("today.newThread")}</Link>
{t("today.outbox.emptyMid")}{" "}
<Link to="/app/outbox">{t("nav.outbox")}</Link>
{t("today.outbox.emptyEnd")}
</p>
) : (
<div className="hb-stack" style={{ gap: "0.5rem" }}>
<p style={{ margin: 0, fontSize: "var(--hb-text-sm)" }}>
{t("today.outbox.summary", {
sent: sentToday,
running: runningOutbox.length,
failed: failedOutbox.length,
})}
</p>
{failedOutbox.slice(0, 2).map((o) => (
<p key={o.id} style={{ margin: 0, fontSize: "var(--hb-text-sm)" }}>
<Badge tone="danger">{t("today.badge.failed")}</Badge>{" "}
<Link to={`/app/outbox/${o.id}`}>{o.title || o.id.slice(0, 8)}</Link>
</p>
))}
{runningOutbox.slice(0, 2).map((o) => (
<p key={o.id} style={{ margin: 0, fontSize: "var(--hb-text-sm)" }}>
<Badge tone="brand">
{o.status === "scheduling"
? t("today.badge.scheduling")
: t("today.badge.sending")}
</Badge>{" "}
<Link to={`/app/outbox/${o.id}`}>{o.title || o.id.slice(0, 8)}</Link>
</p>
))}
<Link to="/app/outbox">
<Button type="button" variant="ghost">
{t("today.openOutbox")}
</Button>
</Link>
</div>
)}
</Card>
{/* 帳號成效 */}
<Card title={t("today.accounts.title")}>
{accountPulses.length === 0 ? (
<EmptyState
title={t("today.accounts.empty")}
action={
<div className="hb-today-actions" style={{ margin: 0 }}>
{accounts.length > 0 ? (
<Button
type="button"
onClick={() => void onSyncOwnPosts()}
disabled={syncingPosts}
>
{syncingPosts ? t("common.loading") : t("today.syncPosts")}
</Button>
) : (
<Link to="/app/crew">
<Button type="button" variant="ghost">
{t("nav.crew")}
</Button>
</Link>
)}
</div>
}
/>
) : (
<div className="hb-stack">
<ul className="hb-today-account-list">
{accountPulses.map((row) => (
<li key={row.account.id} className="hb-today-account">
<div className="hb-today-account__head">
<strong>@{row.account.username}</strong>
<span className="text-muted" style={{ fontSize: "var(--hb-text-xs)" }}>
{t("today.postsCount", { n: row.posts })}
</span>
</div>
<div className="hb-today-account__stats">
<span>
{t("today.views")}{" "}
<strong>{row.views.toLocaleString(dateLocale)}</strong>
</span>
<span>
{t("today.likes")} <strong>{row.likes}</strong>
</span>
<span>
{t("today.repliesShort")} <strong>{row.replies}</strong>
</span>
</div>
{row.topInsight ? (
<p className="hb-today-account__insight">{row.topInsight}</p>
) : null}
</li>
))}
</ul>
<div className="hb-today-actions" style={{ margin: 0 }}>
<Link to="/app/insights">
<Button type="button">{t("today.fullInsights")}</Button>
</Link>
<Link to="/app/studio?tab=posts">
<Button type="button" variant="ghost">
{t("today.viewPosts")}
</Button>
</Link>
<Button
type="button"
variant="ghost"
onClick={() => void onSyncOwnPosts()}
disabled={syncingPosts || !accounts.length}
>
{syncingPosts ? t("common.loading") : t("today.syncPosts")}
</Button>
<Link to="/app/crew">
<Button type="button" variant="ghost">
{t("today.manageAccounts")}
</Button>
</Link>
</div>
</div>
)}
</Card>
</>
);
}