277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Logo from "../../../components/asset/Logo";
|
|
import Button from "../../../components/buttons/Button";
|
|
import Alert from "../../../components/modals/Alert";
|
|
import CommunityRule from "../../../components/type/CommunityRule";
|
|
import { useTranslation } from "../../../contexts/MessagesContext";
|
|
import { makeCopyPublishedRule } from "../../../../lib/create/api";
|
|
import { communityPossessive } from "../../../../lib/create/ruleInviteCardMeta";
|
|
import { CREATE_FLOW_COPIED_QUERY, CREATE_FLOW_COPIED_VALUE } from "../../../../lib/create/invitePaths";
|
|
import { formatInviteLastChanged } from "../../../../lib/create/inviteLastChanged";
|
|
import { writeLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
|
import { downloadStoredRuleAsPdf } from "../../../../lib/create/ruleExport";
|
|
import { createCompletedPath } from "../../create/utils/createFlowPaths";
|
|
import { CreateFlowHeaderLockup } from "../../create/components/CreateFlowHeaderLockup";
|
|
import { useCreateFlowMdUp } from "../../create/hooks/useCreateFlowMdUp";
|
|
import {
|
|
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
|
|
CREATE_FLOW_PAGE_GUTTER_CLASS,
|
|
CREATE_FLOW_SCROLL_REGION_CLASS,
|
|
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
|
CREATE_FLOW_VIEWPORT_FRAME_CLASS,
|
|
} from "../../create/components/createFlowLayoutTokens";
|
|
import { parsePublishedDocumentForCommunityRuleDisplay } from "../../../../lib/create/publishedDocumentToDisplaySections";
|
|
import { MakeACopyDialog } from "./MakeACopyDialog";
|
|
import type { CommunityRuleSection } from "../../../components/type/CommunityRule/CommunityRule.types";
|
|
|
|
const outlineButtonClass =
|
|
"md:!text-x-small-label !text-xx-small-label !px-[var(--space-150)] md:!px-[var(--space-200)] !py-[6px] md:!py-[8px] !border md:!border-[1.5px]";
|
|
|
|
const SURFACE = "bg-[var(--color-teal-teal50,#c9fef9)]";
|
|
|
|
export type InvitedRuleViewProps = {
|
|
ruleId: string;
|
|
title: string;
|
|
summary: string | null;
|
|
document: Record<string, unknown>;
|
|
updatedAt: string;
|
|
inviterEmail: string | null;
|
|
viewerEmail: string;
|
|
countsSpoken: string;
|
|
openCopyDialog?: boolean;
|
|
};
|
|
|
|
export function InvitedRuleView({
|
|
ruleId,
|
|
title,
|
|
summary,
|
|
document,
|
|
updatedAt,
|
|
inviterEmail,
|
|
viewerEmail,
|
|
countsSpoken,
|
|
openCopyDialog = false,
|
|
}: InvitedRuleViewProps) {
|
|
const t = useTranslation("pages.invite");
|
|
const router = useRouter();
|
|
const mdUp = useCreateFlowMdUp();
|
|
const [copyOpen, setCopyOpen] = useState(openCopyDialog);
|
|
const [copyBusy, setCopyBusy] = useState(false);
|
|
const [copyError, setCopyError] = useState<string | null>(null);
|
|
const [exportError, setExportError] = useState(false);
|
|
|
|
const sections: CommunityRuleSection[] =
|
|
parsePublishedDocumentForCommunityRuleDisplay(document);
|
|
const possessive = communityPossessive(title);
|
|
const lastChanged = formatInviteLastChanged(updatedAt);
|
|
const bannerTitle = inviterEmail
|
|
? t("bannerTitle").replace("{email}", inviterEmail)
|
|
: t("bannerTitleNoEmail");
|
|
const whoCanEdit = inviterEmail ?? t("whoCanEditUnknown");
|
|
|
|
useEffect(() => {
|
|
if (!openCopyDialog) return;
|
|
router.replace(`/invite/${encodeURIComponent(ruleId)}`);
|
|
}, [openCopyDialog, ruleId, router]);
|
|
|
|
const handlePdf = useCallback(() => {
|
|
try {
|
|
downloadStoredRuleAsPdf({
|
|
id: ruleId,
|
|
title,
|
|
summary,
|
|
document,
|
|
});
|
|
setExportError(false);
|
|
} catch {
|
|
setExportError(true);
|
|
}
|
|
}, [document, ruleId, summary, title]);
|
|
|
|
const handleCopy = useCallback(
|
|
async (communityName: string) => {
|
|
setCopyBusy(true);
|
|
setCopyError(null);
|
|
const result = await makeCopyPublishedRule(ruleId, communityName);
|
|
setCopyBusy(false);
|
|
if (result.ok === false) {
|
|
setCopyError(t("copyFailed"));
|
|
return;
|
|
}
|
|
writeLastPublishedRule({
|
|
id: result.id,
|
|
title: result.title,
|
|
summary: result.summary,
|
|
document: result.document,
|
|
copiedFromTitle: result.copiedFromTitle,
|
|
copiedNameHit: result.copiedNameHit,
|
|
});
|
|
router.push(
|
|
createCompletedPath({
|
|
[CREATE_FLOW_COPIED_QUERY]: CREATE_FLOW_COPIED_VALUE,
|
|
}),
|
|
);
|
|
},
|
|
[ruleId, router, t],
|
|
);
|
|
|
|
return (
|
|
<div className={`${CREATE_FLOW_VIEWPORT_FRAME_CLASS} ${SURFACE}`}>
|
|
<header
|
|
className={`${SURFACE} w-full print:hidden`}
|
|
role="banner"
|
|
>
|
|
<nav
|
|
className={`mx-auto flex w-full max-w-[1920px] items-center justify-between ${CREATE_FLOW_PAGE_GUTTER_CLASS} py-[var(--space-300)] md:py-[var(--space-400)]`}
|
|
aria-label={t("navAria")}
|
|
>
|
|
<Logo size="createFlow" wordmark palette="inverse" />
|
|
<Button
|
|
buttonType="outline"
|
|
palette="inverse"
|
|
size="xsmall"
|
|
type="button"
|
|
ariaLabel={t("makeACopyAria")}
|
|
className={outlineButtonClass}
|
|
onClick={() => {
|
|
setCopyError(null);
|
|
setCopyOpen(true);
|
|
}}
|
|
>
|
|
{t("makeACopy")}
|
|
</Button>
|
|
</nav>
|
|
</header>
|
|
|
|
<div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden">
|
|
<div
|
|
className={`mx-auto grid min-h-0 w-full grid-cols-1 gap-4 ${CREATE_FLOW_PAGE_GUTTER_CLASS} max-md:h-full max-md:grid-rows-[auto_minmax(0,1fr)] max-md:overflow-hidden max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:grid-cols-2 md:grid-rows-1 md:items-stretch md:justify-items-center md:gap-[var(--space-1200)] md:overflow-hidden md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
|
|
>
|
|
<div
|
|
className={`flex shrink-0 flex-col justify-start min-h-0 overflow-hidden md:justify-center md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
|
>
|
|
<CreateFlowHeaderLockup
|
|
title={title}
|
|
description={summary ?? undefined}
|
|
justification="left"
|
|
size="L"
|
|
palette="inverse"
|
|
/>
|
|
</div>
|
|
<div
|
|
className={`relative flex min-h-0 flex-col overflow-x-hidden overflow-y-auto md:max-h-full md:self-stretch ${CREATE_FLOW_SCROLL_REGION_CLASS} ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
|
|
>
|
|
<div className="flex w-full min-w-0 flex-col gap-[var(--space-600)] py-0 md:pb-8">
|
|
<div className="print:hidden">
|
|
<Alert
|
|
type="banner"
|
|
status="default"
|
|
title={bannerTitle}
|
|
description={t("bannerDescription")}
|
|
hasLeadingIcon
|
|
hasBodyText
|
|
hasTrailingIcon={false}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-wrap gap-[var(--space-300)] print:hidden">
|
|
<Button
|
|
buttonType="outline"
|
|
palette="inverse"
|
|
size="xsmall"
|
|
type="button"
|
|
className={outlineButtonClass}
|
|
onClick={handlePdf}
|
|
>
|
|
{t("downloadPdf")}
|
|
</Button>
|
|
<Button
|
|
buttonType="outline"
|
|
palette="inverse"
|
|
size="xsmall"
|
|
type="button"
|
|
className={outlineButtonClass}
|
|
onClick={() => window.print()}
|
|
>
|
|
{t("print")}
|
|
</Button>
|
|
</div>
|
|
{exportError ? (
|
|
<p role="alert" className="text-small-paragraph print:hidden">
|
|
{t("exportFailedDescription")}
|
|
</p>
|
|
) : null}
|
|
<CommunityRule
|
|
sections={sections}
|
|
useCardStyle={!mdUp}
|
|
className={mdUp ? "min-w-0" : "w-full min-w-0 p-4"}
|
|
/>
|
|
<div className="flex w-full flex-col border-t border-[var(--color-content-invert-secondary)] print:hidden">
|
|
<h2 className="sr-only">{t("factsHeading")}</h2>
|
|
<FactRow label={t("whoCanEdit")} value={whoCanEdit} />
|
|
<FactRow label={t("whoCanSee")} value={t("whoCanSeeValue")} />
|
|
<FactRow label={t("lastChanged")} value={lastChanged} />
|
|
</div>
|
|
<div className="flex w-full min-w-0 flex-col gap-[var(--space-400)] rounded-[var(--radius-400)] bg-[var(--color-surface-invert-brand-primary)] p-[var(--space-600)] text-[var(--color-content-invert-primary)] print:hidden">
|
|
<p className="font-bricolage text-x-small-heading">
|
|
{t("copyCardTitle")}
|
|
</p>
|
|
<p className="text-small-paragraph">
|
|
{t("copyCardBody").replace("{counts}", countsSpoken)}
|
|
</p>
|
|
<Button
|
|
buttonType="outline"
|
|
palette="inverse"
|
|
size="small"
|
|
type="button"
|
|
className="self-start"
|
|
onClick={() => {
|
|
setCopyError(null);
|
|
setCopyOpen(true);
|
|
}}
|
|
>
|
|
{t("makeACopy")}
|
|
</Button>
|
|
</div>
|
|
<p className="text-small-paragraph text-[var(--color-content-invert-secondary)] print:hidden">
|
|
{t("privacy")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<MakeACopyDialog
|
|
isOpen={copyOpen}
|
|
onClose={() => {
|
|
if (!copyBusy) setCopyOpen(false);
|
|
}}
|
|
possessive={possessive}
|
|
countsSpoken={countsSpoken}
|
|
viewerEmail={viewerEmail}
|
|
busy={copyBusy}
|
|
error={copyError}
|
|
onConfirm={(communityName) => {
|
|
void handleCopy(communityName);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FactRow({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="flex items-baseline justify-between gap-[var(--space-400)] border-b border-[var(--color-content-invert-secondary)] py-[var(--space-300)]">
|
|
<p className="text-small-label text-[var(--color-content-invert-secondary)]">
|
|
{label}
|
|
</p>
|
|
<p className="text-right text-small-paragraph text-[var(--color-content-invert-primary)]">
|
|
{value}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|