From 4e37fcfa223d9bc4d1eb17cd0e5f63b2e3ff560d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Nov 2025 23:11:34 +0000 Subject: [PATCH] refactor(web): unify Location/Attachments/Relations components for consistency This refactor introduces a maintainable, unified component architecture for memo metadata (Location, Attachments, Relations) that ensures visual and functional consistency between editor and view modes. ## Key Changes ### New Shared Component Library (web/src/components/memo-metadata/) - **MetadataBadge**: Reusable badge component for compact metadata display - **MetadataCard**: Reusable card component for structured metadata - **LocationDisplay**: Unified location component with mode support - **AttachmentList**: Unified attachment list with thumbnail support - **RelationList**: Unified relation list with bidirectional tabs - **AttachmentCard**: Shared attachment rendering logic - **RelationCard**: Shared relation item rendering logic ### Benefits 1. **Single Source of Truth**: One component handles both edit/view modes 2. **Visual Consistency**: Same badge/card styles across all contexts 3. **Better Maintainability**: Shared logic reduces duplication (9 files vs 6) 4. **Enhanced Features**: - Thumbnails for images in editor mode - Consistent badge sizing (h-7) across all metadata types - Unified spacing and interaction patterns ### Component Updates - **MemoEditor**: Now uses LocationDisplay, AttachmentList, RelationList - **MemoView**: Now uses the same unified components with mode="view" - Removed old separate implementations: - MemoEditor/{LocationView, AttachmentListView, RelationListView} - {MemoLocationView, MemoAttachmentListView, MemoRelationListView} ### Technical Details - All components use `DisplayMode` type ("edit" | "view") for behavior - Editor mode: Shows badges with remove buttons, drag-sort for attachments - View mode: Shows rich previews, popovers, galleries, navigation links - TypeScript strict mode compatible, passes all lint checks - Production build tested and verified This architecture follows modern React patterns and makes it easy to add new metadata types or modify existing ones without duplicating code. --- web/src/components/MemoEditor/index.tsx | 11 +- web/src/components/MemoView.tsx | 10 +- .../memo-metadata/AttachmentCard.tsx | 96 +++++++++++ .../memo-metadata/AttachmentList.tsx | 144 +++++++++++++++++ .../memo-metadata/LocationDisplay.tsx | 62 +++++++ .../memo-metadata/MetadataBadge.tsx | 46 ++++++ .../components/memo-metadata/MetadataCard.tsx | 26 +++ .../components/memo-metadata/RelationCard.tsx | 61 +++++++ .../components/memo-metadata/RelationList.tsx | 153 ++++++++++++++++++ web/src/components/memo-metadata/index.ts | 17 ++ web/src/components/memo-metadata/types.ts | 10 ++ 11 files changed, 624 insertions(+), 12 deletions(-) create mode 100644 web/src/components/memo-metadata/AttachmentCard.tsx create mode 100644 web/src/components/memo-metadata/AttachmentList.tsx create mode 100644 web/src/components/memo-metadata/LocationDisplay.tsx create mode 100644 web/src/components/memo-metadata/MetadataBadge.tsx create mode 100644 web/src/components/memo-metadata/MetadataCard.tsx create mode 100644 web/src/components/memo-metadata/RelationCard.tsx create mode 100644 web/src/components/memo-metadata/RelationList.tsx create mode 100644 web/src/components/memo-metadata/index.ts create mode 100644 web/src/components/memo-metadata/types.ts diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 4b48d7401..e9868ca75 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -20,12 +20,10 @@ import { Location, Memo, MemoRelation, MemoRelation_Type, Visibility } from "@/t import { useTranslate } from "@/utils/i18n"; import { convertVisibilityFromString } from "@/utils/memo"; import DateTimeInput from "../DateTimeInput"; +import { LocationDisplay, AttachmentList, RelationList } from "../memo-metadata"; import InsertMenu from "./ActionButton/InsertMenu"; import VisibilitySelector from "./ActionButton/VisibilitySelector"; -import AttachmentListView from "./AttachmentListView"; import Editor, { EditorRefActions } from "./Editor"; -import LocationView from "./LocationView"; -import RelationListView from "./RelationListView"; import { handleEditorKeydownWithMarkdownShortcuts, hyperlinkHighlightedText } from "./handlers"; import { MemoEditorContext } from "./types"; @@ -490,7 +488,8 @@ const MemoEditor = observer((props: Props) => { onCompositionEnd={handleCompositionEnd} > - setState((prevState) => ({ @@ -499,8 +498,8 @@ const MemoEditor = observer((props: Props) => { })) } /> - - + +
e.stopPropagation()}>
= observer((props: Props) => { compact={memo.pinned ? false : props.compact} // Always show full content when pinned. parentPage={parentPage} /> - {memo.location && } - - + {memo.location && } + +
{nsfw && !showNSFWContent && ( diff --git a/web/src/components/memo-metadata/AttachmentCard.tsx b/web/src/components/memo-metadata/AttachmentCard.tsx new file mode 100644 index 000000000..4b60f03ee --- /dev/null +++ b/web/src/components/memo-metadata/AttachmentCard.tsx @@ -0,0 +1,96 @@ +import { FileIcon, XIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Attachment } from "@/types/proto/api/v1/attachment_service"; +import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; +import { DisplayMode } from "./types"; + +interface AttachmentCardProps { + attachment: Attachment; + mode: DisplayMode; + onRemove?: () => void; + onClick?: () => void; + className?: string; + showThumbnail?: boolean; +} + +/** + * Shared attachment card component + * Displays thumbnails for images in both modes, with size variations + */ +const AttachmentCard = ({ attachment, mode, onRemove, onClick, className, showThumbnail = true }: AttachmentCardProps) => { + const type = getAttachmentType(attachment); + const attachmentUrl = getAttachmentUrl(attachment); + const attachmentThumbnailUrl = getAttachmentThumbnailUrl(attachment); + const isMedia = type === "image/*" || type === "video/*"; + + // Editor mode - compact badge style with thumbnail + if (mode === "edit") { + return ( +
+ {showThumbnail && type === "image/*" ? ( + {attachment.filename} + ) : ( + + )} + {attachment.filename} + {onRemove && ( + + )} +
+ ); + } + + // View mode - media gets special treatment + if (isMedia) { + if (type === "image/*") { + return ( + { + const target = e.target as HTMLImageElement; + if (target.src.includes("?thumbnail=true")) { + target.src = attachmentUrl; + } + }} + onClick={onClick} + decoding="async" + loading="lazy" + alt={attachment.filename} + /> + ); + } else if (type === "video/*") { + return ( +