Files
Jyotisha/frontend/src/components/starter-home.tsx
T

127 lines
5.1 KiB
TypeScript

"use client";
import { Clock3, MoonStar } from "lucide-react";
import { useCurrentSubject } from "@/lib/current-subject";
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
/**
* The empty state is split across two slots on purpose.
*
* `StarterGreeting` sits in the conversation area, bottom-aligned, so it lands
* directly above the composer. `StarterEntries` sits inside `.composer-wrap`,
* below the composer. The composer itself never moves: it keeps the one DOM
* position it has in a live session, so nothing remounts when the first message
* arrives and an in-flight draft, focus or queued card survives the transition.
*
* Before this, the empty state was a bordered hero card plus two 132px product
* cards, and the composer sat under roughly 800px of content — the primary
* action was the last thing on the page.
*/
export type StarterGreetingProps = {
readonly starterGreeting: { readonly text: string };
/* Today's trend, one quiet line under the greeting. `null` hides it (no natal
minute). Before the card is ready the caller passes the static sentence,
never a placeholder or a spinner. */
readonly dailyTrend?: string | null;
readonly dailyTrendSettled?: boolean;
};
/* One line. The salutation/question pair it replaces asked the user's question
for them; the composer placeholder asks now. The optional sub-line is today's
trend, moved up from the fine print under the entry pills where it was glued
to an unrelated rectification hint with a middle dot. */
export function StarterGreeting({ starterGreeting, dailyTrend = null, dailyTrendSettled = true }: StarterGreetingProps) {
return (
<section className="starter-greeting-block" aria-labelledby="starter-heading">
<h1 id="starter-heading">{starterGreeting.text}</h1>
{dailyTrend ? (
<p className="starter-greeting-sub" role={dailyTrendSettled ? undefined : "status"}>
{dailyTrend}
</p>
) : null}
</section>
);
}
/* The line under the pills only appears when there is something to do about
it: a non-self subject cannot start a rectification. A resumable case says
nothing here any more (2026-09-30, product: 「这个提示可以去掉」); the case
is still in the history list. */
export function rectificationEntryHint(rectificationForSelf: boolean): string | null {
if (!rectificationForSelf) return "生时校正暂时只支持本人。";
return null;
}
export type StarterEntriesProps = {
readonly natalMinuteAvailable: boolean;
readonly dailyStarlanguageQuestion: string;
readonly productEntrypointsDisabled: boolean;
readonly startDailyStarlanguageConsultation: () => void;
readonly rectificationCardLabel: string;
readonly rectificationLoading: boolean;
readonly rectificationMutationPending: boolean;
readonly openRectificationFromHomepage: () => void;
/** Still passed by the page; the hint no longer reads it (2026-09-30). */
readonly rectificationEntrySummary: RectificationEntrySummary | null;
readonly rectificationError: string;
readonly rectificationSurfaceOpen: boolean;
readonly rectificationErrorMessage: string;
};
export function StarterEntries({
natalMinuteAvailable,
dailyStarlanguageQuestion,
productEntrypointsDisabled,
startDailyStarlanguageConsultation,
rectificationCardLabel,
rectificationLoading,
rectificationMutationPending,
openRectificationFromHomepage,
rectificationError,
rectificationSurfaceOpen,
rectificationErrorMessage,
}: StarterEntriesProps) {
const subjectId = useCurrentSubject();
const rectificationForSelf = subjectId === "self";
const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势";
const rectificationHint = rectificationEntryHint(rectificationForSelf);
return (
<div className="starter-entries">
<div className="starter-entry-row">
<button
className="starter-entry"
type="button"
aria-label={natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
disabled={productEntrypointsDisabled}
onClick={startDailyStarlanguageConsultation}
>
<MoonStar aria-hidden="true" />
<span>{dailyLabel}</span>
</button>
<button
className="starter-entry"
type="button"
aria-label={rectificationCardLabel}
data-opening={rectificationLoading ? "true" : undefined}
disabled={productEntrypointsDisabled || rectificationLoading || rectificationMutationPending || !rectificationForSelf}
onClick={() => void openRectificationFromHomepage()}
>
<Clock3 aria-hidden="true" />
<span>生时校正</span>
</button>
</div>
{rectificationHint && <p className="starter-entry-hint">{rectificationHint}</p>}
{!natalMinuteAvailable && (
<p className="starter-entry-boundary">出生时间不足以支持的判断我会说明,不会补造具体分钟。</p>
)}
{rectificationError && !rectificationSurfaceOpen && (
<p className="form-error rectification-entry-error" role="alert">
{rectificationErrorMessage}
</p>
)}
</div>
);
}