Files
community-rule/app/components/navigation/Top/Top.container.tsx
T
adilalloandCursor 4bd9fa6dc6 Restore the folder header to Figma layout without cloning the nav, and page-center inner-page links with the home cluster.
Park the skip link in the Tailwind sheet so it cannot sit in document flow and push the yellow tab down.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 21:39:39 -06:00

227 lines
7.8 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.
"use client";
/**
* Figma: "Navigation / Top" (22078-808559); Folder-top 430639 (18582:22056),
* 6401023 (18580:17113).
*/
import { memo, useCallback } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useAuthModal } from "../../../contexts/AuthModalContext";
import { useTranslation } from "../../../contexts/MessagesContext";
import MenuItem from "../MenuItem";
import Button from "../../buttons/Button";
import AvatarContainer from "../../asset/AvatarContainer";
import Avatar from "../../asset/Avatar";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { TopView } from "./Top.view";
import type { TopProps } from "./Top.types";
export const avatarImageSources = [
getAssetPath(ASSETS.AVATAR_3),
getAssetPath(ASSETS.AVATAR_2),
getAssetPath(ASSETS.AVATAR_1),
] as const;
/** @deprecated Use `avatarImageSources` — alts are resolved in `TopContainer` via `topNav` messages. */
export const avatarImages = avatarImageSources.map((src, index) => ({
src,
alt: `Avatar ${3 - index}`,
}));
/** Padding/type that tracks lg/xl Menu sizes without cloning the items per breakpoint. */
const NAV_ITEM_RESPONSIVE_CLASS =
"lg:px-[var(--spacing-scale-016)] lg:py-[var(--spacing-scale-016)] lg:h-[44px] lg:text-medium-label xl:text-x-large-label";
const FOLDER_NAV_ITEM_RESPONSIVE_CLASS =
"md:px-[var(--spacing-scale-008)] md:py-[var(--spacing-scale-008)] md:h-[32px] md:text-x-small-label lg:px-[var(--spacing-scale-016)] lg:py-[var(--spacing-scale-016)] lg:h-[44px] lg:text-medium-label xl:text-x-large-label";
const CREATE_RULE_RESPONSIVE_CLASS =
"lg:p-[var(--spacing-scale-012)] lg:gap-[var(--spacing-scale-006)] lg:text-medium-label xl:p-[var(--spacing-scale-016)] xl:gap-[var(--spacing-scale-008)] xl:text-x-large-label";
const FOLDER_CREATE_RULE_RESPONSIVE_CLASS =
"md:p-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-002)] md:text-x-small-label lg:p-[var(--spacing-scale-012)] lg:gap-[var(--spacing-scale-006)] lg:text-medium-label xl:p-[var(--spacing-scale-016)] xl:gap-[var(--spacing-scale-008)] xl:text-x-large-label";
const TopContainer = memo<TopProps>(
({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
const pathname = usePathname();
const router = useRouter();
const { openLogin } = useAuthModal();
const t = useTranslation("header");
const tTopNav = useTranslation("topNav");
/**
* `Top` is hidden on `/create` routes by ConditionalNavigationClient, so
* this button is always clicked from outside the wizard. Clears anonymous
* `localStorage` synchronously and, when backend sync is on, fires the
* server `DELETE /api/drafts/me` in the background. `SignedInDraftHydration`
* reads the `create:fresh-entry-pending` sentinel and waits before fetching
* (see {@link prepareFreshCreateFlowEntrySync}).
*/
const handleCreateRuleClick = useCallback(() => {
prepareFreshCreateFlowEntrySync({ signedIn: loggedIn });
router.push("/create/informational");
}, [loggedIn, router]);
const schemaData = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "CommunityRule",
url: "https://communityrule.com",
...(folderTop && {
description: tTopNav("schemaDescription"),
}),
potentialAction: {
"@type": "SearchAction",
target: "https://communityrule.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
};
const logoSize = folderTop ? "topNavFolderTop" : "topNavHeader";
const navigationItems = [
{ href: "/use-cases", text: t("navigation.useCases"), extraPadding: true },
{ href: "/learn", text: t("navigation.learn") },
{ href: "/about", text: t("navigation.about") },
];
const renderNavigationItems = () => {
const mode = folderTop ? "inverse" : "default";
const sizeClass = folderTop
? FOLDER_NAV_ITEM_RESPONSIVE_CLASS
: NAV_ITEM_RESPONSIVE_CLASS;
return navigationItems.map((item) => {
const isUseCases = item.extraPadding === true;
return (
<MenuItem
key={item.href}
href={item.href}
size="X Small"
mode={mode}
state={pathname === item.href ? "selected" : "default"}
reducedPadding={isUseCases}
className={sizeClass}
ariaLabel={t("ariaLabels.navigateToPage").replace(
"{text}",
item.text,
)}
>
{item.text}
</MenuItem>
);
});
};
const renderLoginButton = () => {
const label = loggedIn ? t("buttons.profile") : t("buttons.logIn");
const ariaLabel = loggedIn
? t("ariaLabels.goToProfile")
: t("ariaLabels.logInToAccount");
const navSelected =
(loggedIn && pathname === "/profile") ||
(!loggedIn && pathname === "/login");
const loginClassName = folderTop
? FOLDER_NAV_ITEM_RESPONSIVE_CLASS
: NAV_ITEM_RESPONSIVE_CLASS;
if (loggedIn) {
return (
<MenuItem
href="/profile"
size="X Small"
mode="default"
state={navSelected ? "selected" : "default"}
className={loginClassName}
ariaLabel={ariaLabel}
>
{label}
</MenuItem>
);
}
return (
<MenuItem
buttonOnClick={() =>
openLogin({
variant: "default",
backdropVariant: "blurredYellow",
nextPath: pathname || "/",
})
}
href="/login"
size="X Small"
mode="default"
state={navSelected ? "selected" : "default"}
className={loginClassName}
ariaLabel={ariaLabel}
>
{label}
</MenuItem>
);
};
const renderCreateRuleButton = () => {
return (
<Button
size="xsmall"
buttonType="filled"
palette="inverse"
onClick={handleCreateRuleClick}
ariaLabel={t("ariaLabels.createNewRule")}
className={
folderTop
? FOLDER_CREATE_RULE_RESPONSIVE_CLASS
: CREATE_RULE_RESPONSIVE_CLASS
}
>
<AvatarContainer
size="small"
className={
folderTop
? "md:-space-x-[9px] lg:-space-x-[var(--spacing-scale-010)] xl:-space-x-[13px]"
: "lg:-space-x-[var(--spacing-scale-010)] xl:-space-x-[13px]"
}
>
{avatarImageSources.map((src, index) => (
<Avatar
key={src}
src={src}
alt={tTopNav(`avatarAlts.${3 - index}`)}
size="small"
className={
folderTop
? "md:h-[var(--spacing-scale-018)] md:w-[var(--spacing-scale-018)] lg:h-[var(--spacing-scale-024)] lg:w-[var(--spacing-scale-024)] xl:h-[var(--spacing-scale-032)] xl:w-[var(--spacing-scale-032)]"
: "lg:h-[var(--spacing-scale-024)] lg:w-[var(--spacing-scale-024)] xl:h-[var(--spacing-scale-032)] xl:w-[var(--spacing-scale-032)]"
}
/>
))}
</AvatarContainer>
<span>{t("buttons.createRule")}</span>
</Button>
);
};
return (
<TopView
folderTop={folderTop}
loggedIn={loggedIn}
profile={profile}
logIn={logIn}
schemaData={schemaData}
logoSize={logoSize}
renderNavigationItems={renderNavigationItems}
renderLoginButton={renderLoginButton}
renderCreateRuleButton={renderCreateRuleButton}
/>
);
},
);
TopContainer.displayName = "Top";
export default TopContainer;