Skip to content

Commit f403f8c

Browse files
committed
refactor: simplify memo metadata components
1 parent 0e4d2d2 commit f403f8c

15 files changed

+266
-180
lines changed

web/src/components/MemoEditor/components/VoiceRecorderPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AudioLinesIcon, LoaderCircleIcon, MicIcon, RotateCcwIcon, SquareIcon, Trash2Icon } from "lucide-react";
22
import type { FC } from "react";
33
import { AudioAttachmentItem } from "@/components/MemoMetadata/Attachment";
4-
import { formatAudioTime } from "@/components/MemoMetadata/Attachment/attachmentViewHelpers";
4+
import { formatAudioTime } from "@/components/MemoMetadata/Attachment/attachmentHelpers";
55
import { Button } from "@/components/ui/button";
66
import { cn } from "@/lib/utils";
77
import { useTranslate } from "@/utils/i18n";

web/src/components/MemoMetadata/Attachment/AttachmentListEditor.tsx

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from "
22
import type { FC } from "react";
33
import type { AttachmentItem, LocalFile } from "@/components/MemoEditor/types/attachment";
44
import { toAttachmentItems } from "@/components/MemoEditor/types/attachment";
5+
import MetadataSection from "@/components/MemoMetadata/MetadataSection";
56
import { cn } from "@/lib/utils";
67
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
78
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
8-
import SectionHeader from "../SectionHeader";
99

1010
interface AttachmentListEditorProps {
1111
attachments: Attachment[];
@@ -142,28 +142,24 @@ const AttachmentListEditor: FC<AttachmentListEditorProps> = ({ attachments, loca
142142
};
143143

144144
return (
145-
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
146-
<SectionHeader icon={PaperclipIcon} title="Attachments" count={items.length} />
147-
148-
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
149-
{items.map((item) => {
150-
const isLocalFile = item.isLocal;
151-
const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id);
152-
153-
return (
154-
<AttachmentItemCard
155-
key={item.id}
156-
item={item}
157-
onRemove={() => handleRemoveItem(item)}
158-
onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined}
159-
onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined}
160-
canMoveUp={!isLocalFile && attachmentIndex > 0}
161-
canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1}
162-
/>
163-
);
164-
})}
165-
</div>
166-
</div>
145+
<MetadataSection icon={PaperclipIcon} title="Attachments" count={items.length} contentClassName="flex flex-col gap-0.5 p-1 sm:p-1.5">
146+
{items.map((item) => {
147+
const isLocalFile = item.isLocal;
148+
const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id);
149+
150+
return (
151+
<AttachmentItemCard
152+
key={item.id}
153+
item={item}
154+
onRemove={() => handleRemoveItem(item)}
155+
onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined}
156+
onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined}
157+
canMoveUp={!isLocalFile && attachmentIndex > 0}
158+
canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1}
159+
/>
160+
);
161+
})}
162+
</MetadataSection>
167163
);
168164
};
169165

web/src/components/MemoMetadata/Attachment/AttachmentListView.tsx

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { DownloadIcon, FileIcon, Maximize2Icon, PaperclipIcon, PlayIcon } from "lucide-react";
22
import { useMemo } from "react";
3+
import MetadataSection from "@/components/MemoMetadata/MetadataSection";
34
import { cn } from "@/lib/utils";
45
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
56
import { getAttachmentUrl } from "@/utils/attachment";
6-
import SectionHeader from "../SectionHeader";
77
import AttachmentCard from "./AttachmentCard";
88
import AudioAttachmentItem from "./AudioAttachmentItem";
9-
import { getAttachmentMetadata, isImageAttachment, isVideoAttachment, separateAttachments } from "./attachmentViewHelpers";
9+
import { getAttachmentMetadata, isImageAttachment, isVideoAttachment, separateAttachments } from "./attachmentHelpers";
1010

1111
interface AttachmentListViewProps {
1212
attachments: Attachment[];
@@ -172,9 +172,12 @@ const Divider = () => <div className="border-t border-border/70 opacity-80" />;
172172

173173
const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewProps) => {
174174
const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]);
175-
176175
const imageAttachments = useMemo(() => visual.filter(isImageAttachment), [visual]);
177176
const imageUrls = useMemo(() => imageAttachments.map(getAttachmentUrl), [imageAttachments]);
177+
const hasVisual = visual.length > 0;
178+
const hasAudio = audio.length > 0;
179+
const hasDocs = docs.length > 0;
180+
const sectionCount = [hasVisual, hasAudio, hasDocs].filter(Boolean).length;
178181

179182
if (attachments.length === 0) {
180183
return null;
@@ -185,25 +188,14 @@ const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewP
185188
onImagePreview?.(imageUrls, index >= 0 ? index : 0);
186189
};
187190

188-
const sections = [visual.length > 0, audio.length > 0, docs.length > 0];
189-
const sectionCount = sections.filter(Boolean).length;
190-
191191
return (
192-
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
193-
<SectionHeader icon={PaperclipIcon} title="Attachments" count={attachments.length} />
194-
195-
<div className="flex flex-col gap-2 p-2">
196-
{visual.length > 0 && <VisualSection attachments={visual} onImageClick={handleImageClick} />}
197-
198-
{visual.length > 0 && sectionCount > 1 && <Divider />}
199-
200-
{audio.length > 0 && <AudioList attachments={audio} />}
201-
202-
{audio.length > 0 && docs.length > 0 && <Divider />}
203-
204-
{docs.length > 0 && <DocsList attachments={docs} />}
205-
</div>
206-
</div>
192+
<MetadataSection icon={PaperclipIcon} title="Attachments" count={attachments.length} contentClassName="flex flex-col gap-2 p-2">
193+
{hasVisual && <VisualSection attachments={visual} onImageClick={handleImageClick} />}
194+
{hasVisual && sectionCount > 1 && <Divider />}
195+
{hasAudio && <AudioList attachments={audio} />}
196+
{hasAudio && hasDocs && <Divider />}
197+
{hasDocs && <DocsList attachments={docs} />}
198+
</MetadataSection>
207199
);
208200
};
209201

web/src/components/MemoMetadata/Attachment/AudioAttachmentItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FileAudioIcon, PauseIcon, PlayIcon } from "lucide-react";
22
import { useEffect, useRef, useState } from "react";
33
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
4-
import { formatAudioTime } from "./attachmentViewHelpers";
4+
import { formatAudioTime } from "./attachmentHelpers";
55

66
const AUDIO_PLAYBACK_RATES = [1, 1.5, 2] as const;
77

web/src/components/MemoMetadata/Attachment/attachmentViewHelpers.ts renamed to web/src/components/MemoMetadata/Attachment/attachmentHelpers.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,24 @@ export const isVideoAttachment = (attachment: Attachment): boolean => getAttachm
1818
export const isAudioAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "audio/*";
1919

2020
export const separateAttachments = (attachments: Attachment[]): AttachmentGroups => {
21-
const groups: AttachmentGroups = {
22-
visual: [],
23-
audio: [],
24-
docs: [],
25-
};
26-
27-
for (const attachment of attachments) {
28-
if (isImageAttachment(attachment) || isVideoAttachment(attachment)) {
29-
groups.visual.push(attachment);
30-
continue;
31-
}
32-
33-
if (isAudioAttachment(attachment)) {
34-
groups.audio.push(attachment);
35-
continue;
36-
}
37-
38-
groups.docs.push(attachment);
39-
}
40-
41-
return groups;
21+
return attachments.reduce<AttachmentGroups>(
22+
(groups, attachment) => {
23+
if (isImageAttachment(attachment) || isVideoAttachment(attachment)) {
24+
groups.visual.push(attachment);
25+
} else if (isAudioAttachment(attachment)) {
26+
groups.audio.push(attachment);
27+
} else {
28+
groups.docs.push(attachment);
29+
}
30+
31+
return groups;
32+
},
33+
{
34+
visual: [],
35+
audio: [],
36+
docs: [],
37+
},
38+
);
4239
};
4340

4441
export const getAttachmentMetadata = (attachment: Attachment): AttachmentMetadata => ({

web/src/components/MemoMetadata/Location/LocationDisplayEditor.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { MapPinIcon, XIcon } from "lucide-react";
22
import type { FC } from "react";
33
import { cn } from "@/lib/utils";
44
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
5+
import { getLocationCoordinatesText, getLocationDisplayText } from "./locationHelpers";
56

67
interface LocationDisplayEditorProps {
78
location: Location;
@@ -10,7 +11,7 @@ interface LocationDisplayEditorProps {
1011
}
1112

1213
const LocationDisplayEditor: FC<LocationDisplayEditorProps> = ({ location, onRemove, className }) => {
13-
const displayText = location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
14+
const displayText = getLocationDisplayText(location);
1415

1516
return (
1617
<div
@@ -25,9 +26,7 @@ const LocationDisplayEditor: FC<LocationDisplayEditorProps> = ({ location, onRem
2526
<span className="text-xs truncate" title={displayText}>
2627
{displayText}
2728
</span>
28-
<span className="text-[11px] text-muted-foreground shrink-0 hidden sm:inline">
29-
{location.latitude.toFixed(4)}°, {location.longitude.toFixed(4)}°
30-
</span>
29+
<span className="text-[11px] text-muted-foreground shrink-0 hidden sm:inline">{getLocationCoordinatesText(location)}</span>
3130
</div>
3231

3332
{onRemove && (

web/src/components/MemoMetadata/Location/LocationDisplayView.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LocationPicker } from "@/components/map";
55
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
66
import { cn } from "@/lib/utils";
77
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
8+
import { getLocationCoordinatesText, getLocationDisplayText } from "./locationHelpers";
89

910
interface LocationDisplayViewProps {
1011
location?: Location;
@@ -18,27 +19,25 @@ const LocationDisplayView = ({ location, className }: LocationDisplayViewProps)
1819
return null;
1920
}
2021

21-
const displayText = location.placeholder || `Position: [${location.latitude}, ${location.longitude}]`;
22+
const displayText = getLocationDisplayText(location);
2223

2324
return (
2425
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
2526
<PopoverTrigger asChild>
26-
<div
27+
<button
28+
type="button"
2729
className={cn(
2830
"w-full flex flex-row gap-2 cursor-pointer",
2931
"relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-muted/20 hover:bg-accent/20 text-muted-foreground hover:text-foreground text-xs transition-colors",
3032
className,
3133
)}
32-
onClick={() => setPopoverOpen(true)}
3334
>
3435
<span className="shrink-0 text-muted-foreground">
3536
<MapPinIcon className="w-3.5 h-3.5" />
3637
</span>
37-
<span className="text-nowrap opacity-80">
38-
[{location.latitude.toFixed(2)}°, {location.longitude.toFixed(2)}°]
39-
</span>
38+
<span className="text-nowrap opacity-80">[{getLocationCoordinatesText(location, 2)}]</span>
4039
<span className="text-nowrap truncate">{displayText}</span>
41-
</div>
40+
</button>
4241
</PopoverTrigger>
4342
<PopoverContent align="start">
4443
<div className="min-w-80 sm:w-lg flex flex-col justify-start items-start">
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
2+
3+
export const getLocationDisplayText = (location: Location): string => {
4+
return location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
5+
};
6+
7+
export const getLocationCoordinatesText = (location: Location, digits = 4): string => {
8+
return `${location.latitude.toFixed(digits)}°, ${location.longitude.toFixed(digits)}°`;
9+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { LucideIcon } from "lucide-react";
2+
import type { PropsWithChildren } from "react";
3+
import { cn } from "@/lib/utils";
4+
import SectionHeader, { type SectionHeaderTab } from "./SectionHeader";
5+
6+
interface MetadataSectionProps extends PropsWithChildren {
7+
icon: LucideIcon;
8+
title: string;
9+
count: number;
10+
tabs?: SectionHeaderTab[];
11+
className?: string;
12+
contentClassName?: string;
13+
}
14+
15+
const MetadataSection = ({ icon, title, count, tabs, className, contentClassName, children }: MetadataSectionProps) => {
16+
return (
17+
<div className={cn("w-full overflow-hidden rounded-lg border border-border bg-muted/20", className)}>
18+
<SectionHeader icon={icon} title={title} count={count} tabs={tabs} />
19+
<div className={contentClassName}>{children}</div>
20+
</div>
21+
);
22+
};
23+
24+
export default MetadataSection;

web/src/components/MemoMetadata/Relation/RelationListEditor.tsx

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { create } from "@bufbuild/protobuf";
21
import { LinkIcon, XIcon } from "lucide-react";
32
import type { FC } from "react";
4-
import { useEffect, useMemo, useState } from "react";
5-
import { memoServiceClient } from "@/connect";
3+
import { useMemo } from "react";
4+
import MetadataSection from "@/components/MemoMetadata/MetadataSection";
65
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
7-
import { MemoRelation_Memo, MemoRelation_MemoSchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
8-
import SectionHeader from "../SectionHeader";
96
import RelationCard from "./RelationCard";
7+
import { getEditorReferenceRelations } from "./relationHelpers";
8+
import { useResolvedRelationMemos } from "./useResolvedRelationMemos";
109

1110
interface RelationListEditorProps {
1211
relations: MemoRelation[];
@@ -40,31 +39,8 @@ const RelationItemCard: FC<{
4039
};
4140

4241
const RelationListEditor: FC<RelationListEditorProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {
43-
const referenceRelations = useMemo(
44-
() => relations.filter((r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName)),
45-
[relations, memoName],
46-
);
47-
const [fetchedMemos, setFetchedMemos] = useState<Record<string, MemoRelation_Memo>>({});
48-
49-
useEffect(() => {
50-
(async () => {
51-
const missingSnippetRelations = referenceRelations.filter((relation) => !relation.relatedMemo?.snippet && relation.relatedMemo?.name);
52-
if (missingSnippetRelations.length > 0) {
53-
const requests = missingSnippetRelations.map(async (relation) => {
54-
const memo = await memoServiceClient.getMemo({ name: relation.relatedMemo!.name });
55-
return create(MemoRelation_MemoSchema, { name: memo.name, snippet: memo.snippet });
56-
});
57-
const list = await Promise.all(requests);
58-
setFetchedMemos((prev) => {
59-
const next = { ...prev };
60-
for (const memo of list) {
61-
next[memo.name] = memo;
62-
}
63-
return next;
64-
});
65-
}
66-
})();
67-
}, [referenceRelations]);
42+
const referenceRelations = useMemo(() => getEditorReferenceRelations(relations, memoName), [relations, memoName]);
43+
const resolvedMemos = useResolvedRelationMemos(referenceRelations);
6844

6945
const handleDeleteRelation = (memoName: string) => {
7046
if (onRelationsChange) {
@@ -77,17 +53,18 @@ const RelationListEditor: FC<RelationListEditorProps> = ({ relations, onRelation
7753
}
7854

7955
return (
80-
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
81-
<SectionHeader icon={LinkIcon} title="Relations" count={referenceRelations.length} />
82-
83-
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
84-
{referenceRelations.map((relation) => {
85-
const relatedMemo = relation.relatedMemo!;
86-
const memo = relatedMemo.snippet ? relatedMemo : fetchedMemos[relatedMemo.name] || relatedMemo;
87-
return <RelationItemCard key={memo.name} memo={memo} onRemove={() => handleDeleteRelation(memo.name)} parentPage={parentPage} />;
88-
})}
89-
</div>
90-
</div>
56+
<MetadataSection
57+
icon={LinkIcon}
58+
title="Relations"
59+
count={referenceRelations.length}
60+
contentClassName="flex flex-col gap-0.5 p-1 sm:p-1.5"
61+
>
62+
{referenceRelations.map((relation) => {
63+
const relatedMemo = relation.relatedMemo!;
64+
const memo = relatedMemo.snippet ? relatedMemo : resolvedMemos[relatedMemo.name] || relatedMemo;
65+
return <RelationItemCard key={memo.name} memo={memo} onRemove={() => handleDeleteRelation(memo.name)} parentPage={parentPage} />;
66+
})}
67+
</MetadataSection>
9168
);
9269
};
9370

0 commit comments

Comments
 (0)