From 8d9849e13f79dcedd9aa4e329ca3dd58d06da74c Mon Sep 17 00:00:00 2001 From: boojack Date: Tue, 14 Jul 2026 20:54:29 +0800 Subject: [PATCH] chore: redesign about page as identity hero Rebuild /about around an identity hero: instance branding with custom profile fallbacks, a demo badge, and version/commit chips linked to the GitHub release and commit. Dev builds show a plain version chip and hide the unknown-commit chip. Project links become an annotated directory (homepage, docs, API docs, GitHub repo), followed by an MIT license note. The bird sprite gallery moves out of the page, and the version/commit block leaves the settings sidebar now that /about owns build info. --- web/src/pages/About.tsx | 146 +++++++++++++++++++--------------- web/src/pages/Setting.tsx | 26 +----- web/tests/about-page.test.tsx | 97 +++++++++++++++++++--- 3 files changed, 171 insertions(+), 98 deletions(-) diff --git a/web/src/pages/About.tsx b/web/src/pages/About.tsx index da87b6b9..854fa995 100644 --- a/web/src/pages/About.tsx +++ b/web/src/pages/About.tsx @@ -1,82 +1,100 @@ import { ExternalLinkIcon } from "lucide-react"; -import TileSpriteStrip from "@/components/Placeholder/TileSpriteStrip"; -import { TILE_SPRITES, type TileSprite } from "@/components/Placeholder/tileSprites"; -import SettingGroup from "@/components/Settings/SettingGroup"; -import SettingSection from "@/components/Settings/SettingSection"; -import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useInstance } from "@/contexts/InstanceContext"; +import { useTranslate } from "@/utils/i18n"; -const SPRITE_SCALE = 2; +const GITHUB_COMMIT_URL_PREFIX = "https://github.com/usememos/memos/commit/"; +const GITHUB_RELEASE_URL_PREFIX = "https://github.com/usememos/memos/releases/tag/v"; -const PRODUCT_LINKS = [ - { label: "Website", href: "https://usememos.com/" }, - { label: "GitHub", href: "https://github.com/usememos/memos" }, - { label: "Docs", href: "https://usememos.com/docs" }, -]; +const DEFAULT_TITLE = "Memos"; +const DEFAULT_TAGLINE = "Capture first. Keep it yours."; +const DEFAULT_LOGO = "/logo.webp"; -const PRODUCT_POINTS = ["Open. Write. Done.", "Markdown-native.", "Fully yours."]; +const isCommitSha = (commit: string) => /^[0-9a-f]{7,40}$/i.test(commit); +const isSemver = (version: string) => /^\d+\.\d+\.\d+/.test(version); -const BirdSprite = ({ sprite }: { sprite: TileSprite }) => { - return ( -
- -
-

{sprite.name}

-
-
- ); +const Chip = ({ href, children }: { href?: string; children: React.ReactNode }) => { + const className = "inline-flex items-center rounded-full bg-muted px-2.5 py-0.5 font-mono text-xs text-muted-foreground"; + if (href) { + return ( + + {children} + + ); + } + return {children}; }; const About = () => { + const t = useTranslate(); + const { profile, generalSetting } = useInstance(); + + // Instance identity: custom branding when the admin has set it, Memos defaults otherwise. + const customProfile = generalSetting.customProfile; + const instanceTitle = customProfile?.title || DEFAULT_TITLE; + const instanceTagline = customProfile?.description || DEFAULT_TAGLINE; + const instanceLogo = customProfile?.logoUrl || DEFAULT_LOGO; + const isCustomBranded = instanceTitle !== DEFAULT_TITLE; + + // Dev builds report version "dev" and commit "unknown"; show the raw version and skip the commit chip. + const hasSemver = isSemver(profile.version); + const releaseUrl = hasSemver ? `${GITHUB_RELEASE_URL_PREFIX}${profile.version}` : ""; + const versionLabel = hasSemver ? `v${profile.version}` : profile.version; + const hasCommitSha = isCommitSha(profile.commit); + const commitUrl = hasCommitSha ? `${GITHUB_COMMIT_URL_PREFIX}${profile.commit}` : ""; + const shortCommit = hasCommitSha ? profile.commit.slice(0, 7) : ""; + + const projectLinks = [ + { label: t("about.official-website"), note: "the project homepage", href: "https://usememos.com/" }, + { label: t("about.documents"), note: "deploy, configure, use", href: "https://usememos.com/docs" }, + { label: "API Docs", note: "REST + gRPC reference", href: "https://usememos.com/docs/api" }, + { label: t("about.github-repository"), note: "source, issues, releases", href: "https://github.com/usememos/memos" }, + ]; + return (
- - -
-
- -
-

Memos

-

Capture first. Keep it yours.

-
-
-
- {PRODUCT_LINKS.map((link) => ( - - ))} -
+
+ +
+
+

{instanceTitle}

+ {profile.demo && Demo}
- - - -
- {PRODUCT_POINTS.map((item) => ( -
- {item} -
- ))} +

{instanceTagline}

+
+ {profile.version && {versionLabel}} + {shortCommit && {shortCommit}} + {isCustomBranded && Powered by Memos}
- +
+
- -
- {TILE_SPRITES.map((sprite) => ( - - ))} -
-
- + + +

Free and open source under the MIT license.

diff --git a/web/src/pages/Setting.tsx b/web/src/pages/Setting.tsx index 622f3ea3..4e8d3328 100644 --- a/web/src/pages/Setting.tsx +++ b/web/src/pages/Setting.tsx @@ -16,19 +16,14 @@ import useMediaQuery from "@/hooks/useMediaQuery"; import { User_Role } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; -const GITHUB_COMMIT_URL_PREFIX = "https://github.com/usememos/memos/commit/"; - -const isCommitSha = (commit: string) => /^[0-9a-f]{7,40}$/i.test(commit); - const Setting = () => { const t = useTranslate(); const sm = useMediaQuery("sm"); const location = useLocation(); const user = useCurrentUser(); - const { profile, fetchSettings } = useInstance(); + const { fetchSettings } = useInstance(); const [selectedSection, setSelectedSection] = useState(DEFAULT_SETTING_SECTION); const isHost = user?.role === User_Role.ADMIN; - const commitUrl = isCommitSha(profile.commit) ? `${GITHUB_COMMIT_URL_PREFIX}${profile.commit}` : ""; const sectionGroups = useMemo(() => { const visibleSections = SETTINGS_SECTIONS.filter((section) => section.scope === "basic" || isHost); @@ -93,24 +88,7 @@ const Setting = () => { {isHost && ( <> {t("common.admin")} -
- {renderSectionMenuItems(sectionGroups.admin)} -
- {t("setting.version")}: {profile.version} - {profile.commit && ( - - Commit:{" "} - {commitUrl ? ( - - {profile.commit} - - ) : ( - profile.commit - )} - - )} -
-
+
{renderSectionMenuItems(sectionGroups.admin)}
)} diff --git a/web/tests/about-page.test.tsx b/web/tests/about-page.test.tsx index fb79c600..51e3cbe9 100644 --- a/web/tests/about-page.test.tsx +++ b/web/tests/about-page.test.tsx @@ -1,26 +1,103 @@ -import { render, screen, within } from "@testing-library/react"; -import { afterEach, describe, expect, it } from "vitest"; -import { TILE_SPRITES } from "@/components/Placeholder/tileSprites"; +import { render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import About from "@/pages/About"; +const mockInstance = { + profile: { + version: "0.25.0", + commit: "0123456789abcdef0123456789abcdef01234567", + instanceUrl: "", + demo: false, + admin: undefined as { username: string; displayName: string } | undefined, + }, + generalSetting: {} as { customProfile?: { title: string; description: string; logoUrl: string } }, +}; + +vi.mock("@/contexts/InstanceContext", () => ({ + useInstance: () => mockInstance, +})); + +vi.mock("@/utils/i18n", () => ({ + useTranslate: () => (key: string) => (key === "common.version" ? "Version" : key), +})); + describe("", () => { + beforeEach(() => { + mockInstance.profile = { + version: "0.25.0", + commit: "0123456789abcdef0123456789abcdef01234567", + instanceUrl: "https://notes.example.com", + demo: false, + admin: { username: "steven", displayName: "Steven" }, + }; + mockInstance.generalSetting = {}; + }); + afterEach(() => { document.documentElement.removeAttribute("data-theme"); }); - it("renders the product story and current bird sprites", () => { + it("renders the identity hero with linked version and commit chips", () => { render(); expect(screen.getByRole("heading", { name: "Memos" })).toBeInTheDocument(); expect(screen.getByText(/Capture first/i)).toBeInTheDocument(); - expect(screen.getByText(/quick capture/i)).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "v0.25.0" })).toHaveAttribute("href", "https://github.com/usememos/memos/releases/tag/v0.25.0"); + expect(screen.getByRole("link", { name: "0123456" })).toHaveAttribute( + "href", + "https://github.com/usememos/memos/commit/0123456789abcdef0123456789abcdef01234567", + ); + expect(screen.getByText(/MIT license/i)).toBeInTheDocument(); + }); - const birds = screen.getByRole("region", { name: "Birds" }); - expect(within(birds).getAllByTestId("about-bird-sprite")).toHaveLength(TILE_SPRITES.length); + it("links to the usememos.com homepage, docs, API docs, and GitHub repo", () => { + render(); - for (const sprite of TILE_SPRITES) { - expect(within(birds).getByText(sprite.name)).toBeInTheDocument(); - } + expect(screen.getByRole("link", { name: /about\.official-website/ })).toHaveAttribute("href", "https://usememos.com/"); + expect(screen.getByRole("link", { name: /about\.documents/ })).toHaveAttribute("href", "https://usememos.com/docs"); + expect(screen.getByRole("link", { name: /API Docs/ })).toHaveAttribute("href", "https://usememos.com/docs/api"); + expect(screen.getByRole("link", { name: /about\.github-repository/ })).toHaveAttribute("href", "https://github.com/usememos/memos"); + }); + + it("does not surface the instance URL, administrator, or birds", () => { + render(); + + expect(screen.queryByText("https://notes.example.com")).not.toBeInTheDocument(); + expect(screen.queryByText("Administrator")).not.toBeInTheDocument(); + expect(screen.queryByText("Steven")).not.toBeInTheDocument(); + expect(screen.queryByText("Birds")).not.toBeInTheDocument(); + expect(screen.queryByTestId("about-bird-sprite")).not.toBeInTheDocument(); + }); + + it("shows a plain version chip and no commit chip on dev builds", () => { + mockInstance.profile.version = "dev"; + mockInstance.profile.commit = "unknown"; + + render(); + + expect(screen.getByText("dev")).toBeInTheDocument(); + expect(screen.queryByText("vdev")).not.toBeInTheDocument(); + expect(screen.queryByText(/unknown/)).not.toBeInTheDocument(); + }); + + it("shows the demo badge on demo instances", () => { + mockInstance.profile.demo = true; + + render(); + + expect(screen.getByText("Demo")).toBeInTheDocument(); + }); + + it("uses custom branding for the identity hero and credits Memos", () => { + mockInstance.generalSetting = { + customProfile: { title: "Team Notes", description: "Our shared scratchpad.", logoUrl: "/custom-logo.png" }, + }; + + render(); + + expect(screen.getByRole("heading", { name: "Team Notes" })).toBeInTheDocument(); + expect(screen.getByText("Our shared scratchpad.")).toBeInTheDocument(); + expect(screen.getByText("Powered by Memos")).toBeInTheDocument(); }); it("does not add nested horizontal page padding on mobile", () => {