Skip to content

Commit be1b758

Browse files
committed
refactor: simplify memo-metadata components
1 parent d7284fe commit be1b758

File tree

18 files changed

+506
-331
lines changed

18 files changed

+506
-331
lines changed

web/src/components/LeafletMap.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@ const LocationMarker = (props: MarkerProps) => {
3434
// Call the parent onChange function.
3535
props.onChange(e.latlng);
3636
},
37-
locationfound() {},
37+
locationfound() { },
3838
});
3939

4040
useEffect(() => {
4141
if (!initializedRef.current) {
42-
map.attributionControl.setPrefix("");
4342
map.locate();
4443
initializedRef.current = true;
4544
}
@@ -247,7 +246,7 @@ const LeafletMap = (props: MapProps) => {
247246
isDark ? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" : "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
248247
}
249248
/>
250-
<LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => {}} />
249+
<LocationMarker position={position} readonly={props.readonly} onChange={props.onChange ? props.onChange : () => { }} />
251250
<MapControls position={props.latlng} />
252251
<MapCleanup />
253252
</MapContainer>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, XIcon } from "lucide-react";
2+
import type { FC } from "react";
3+
import type { AttachmentItem } from "@/components/memo-metadata/types";
4+
import { cn } from "@/lib/utils";
5+
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
6+
7+
interface AttachmentItemCardProps {
8+
item: AttachmentItem;
9+
onRemove?: () => void;
10+
onMoveUp?: () => void;
11+
onMoveDown?: () => void;
12+
canMoveUp?: boolean;
13+
canMoveDown?: boolean;
14+
className?: string;
15+
}
16+
17+
const AttachmentItemCard: FC<AttachmentItemCardProps> = ({
18+
item,
19+
onRemove,
20+
onMoveUp,
21+
onMoveDown,
22+
canMoveUp = true,
23+
canMoveDown = true,
24+
className,
25+
}) => {
26+
const { category, filename, thumbnailUrl, mimeType, size, isLocal } = item;
27+
const fileTypeLabel = getFileTypeLabel(mimeType);
28+
const fileSizeLabel = size ? formatFileSize(size) : undefined;
29+
30+
return (
31+
<div
32+
className={cn(
33+
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
34+
className,
35+
)}
36+
>
37+
<div className="flex-shrink-0 w-6 h-6 rounded overflow-hidden bg-muted/40 flex items-center justify-center">
38+
{category === "image" && thumbnailUrl ? (
39+
<img src={thumbnailUrl} alt="" className="w-full h-full object-cover" />
40+
) : (
41+
<FileIcon className="w-3.5 h-3.5 text-muted-foreground" />
42+
)}
43+
</div>
44+
45+
<div className="flex-1 min-w-0 flex flex-col sm:flex-row sm:items-baseline gap-0.5 sm:gap-1.5">
46+
<span className="text-xs font-medium truncate" title={filename}>
47+
{filename}
48+
</span>
49+
50+
<div className="flex items-center gap-1 text-[11px] text-muted-foreground shrink-0">
51+
{isLocal && (
52+
<>
53+
<Loader2Icon className="w-2.5 h-2.5 animate-spin" />
54+
<span className="text-muted-foreground/50"></span>
55+
</>
56+
)}
57+
<span>{fileTypeLabel}</span>
58+
{fileSizeLabel && (
59+
<>
60+
<span className="text-muted-foreground/50 hidden sm:inline"></span>
61+
<span className="hidden sm:inline">{fileSizeLabel}</span>
62+
</>
63+
)}
64+
</div>
65+
</div>
66+
67+
<div className="flex-shrink-0 flex items-center gap-0.5">
68+
{onMoveUp && (
69+
<button
70+
type="button"
71+
onClick={onMoveUp}
72+
disabled={!canMoveUp}
73+
className={cn(
74+
"p-0.5 rounded hover:bg-accent active:bg-accent transition-colors touch-manipulation",
75+
!canMoveUp && "opacity-20 cursor-not-allowed hover:bg-transparent",
76+
)}
77+
title="Move up"
78+
aria-label="Move attachment up"
79+
>
80+
<ChevronUpIcon className="w-3 h-3 text-muted-foreground" />
81+
</button>
82+
)}
83+
84+
{onMoveDown && (
85+
<button
86+
type="button"
87+
onClick={onMoveDown}
88+
disabled={!canMoveDown}
89+
className={cn(
90+
"p-0.5 rounded hover:bg-accent active:bg-accent transition-colors touch-manipulation",
91+
!canMoveDown && "opacity-20 cursor-not-allowed hover:bg-transparent",
92+
)}
93+
title="Move down"
94+
aria-label="Move attachment down"
95+
>
96+
<ChevronDownIcon className="w-3 h-3 text-muted-foreground" />
97+
</button>
98+
)}
99+
100+
{onRemove && (
101+
<button
102+
type="button"
103+
onClick={onRemove}
104+
className="p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors ml-0.5 touch-manipulation"
105+
title="Remove"
106+
aria-label="Remove attachment"
107+
>
108+
<XIcon className="w-3 h-3 text-muted-foreground hover:text-destructive" />
109+
</button>
110+
)}
111+
</div>
112+
</div>
113+
);
114+
};
115+
116+
export default AttachmentItemCard;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { PaperclipIcon } from "lucide-react";
2+
import type { FC } from "react";
3+
import type { LocalFile } from "@/components/memo-metadata/types";
4+
import { toAttachmentItems } from "@/components/memo-metadata/types";
5+
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
6+
import AttachmentItemCard from "./AttachmentItemCard";
7+
8+
interface AttachmentListV2Props {
9+
attachments: Attachment[];
10+
localFiles?: LocalFile[];
11+
onAttachmentsChange?: (attachments: Attachment[]) => void;
12+
onRemoveLocalFile?: (previewUrl: string) => void;
13+
}
14+
15+
const AttachmentListV2: FC<AttachmentListV2Props> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {
16+
if (attachments.length === 0 && localFiles.length === 0) {
17+
return null;
18+
}
19+
20+
const items = toAttachmentItems(attachments, localFiles);
21+
22+
const handleMoveUp = (index: number) => {
23+
if (index === 0 || !onAttachmentsChange) return;
24+
25+
const newAttachments = [...attachments];
26+
[newAttachments[index - 1], newAttachments[index]] = [newAttachments[index], newAttachments[index - 1]];
27+
onAttachmentsChange(newAttachments);
28+
};
29+
30+
const handleMoveDown = (index: number) => {
31+
if (index === attachments.length - 1 || !onAttachmentsChange) return;
32+
33+
const newAttachments = [...attachments];
34+
[newAttachments[index], newAttachments[index + 1]] = [newAttachments[index + 1], newAttachments[index]];
35+
onAttachmentsChange(newAttachments);
36+
};
37+
38+
const handleRemoveAttachment = (name: string) => {
39+
if (onAttachmentsChange) {
40+
onAttachmentsChange(attachments.filter((attachment) => attachment.name !== name));
41+
}
42+
};
43+
44+
const handleRemoveItem = (item: (typeof items)[0]) => {
45+
if (item.isLocal) {
46+
onRemoveLocalFile?.(item.id);
47+
} else {
48+
handleRemoveAttachment(item.id);
49+
}
50+
};
51+
52+
return (
53+
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
54+
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-muted/30">
55+
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
56+
<span className="text-xs font-medium text-muted-foreground">Attachments ({items.length})</span>
57+
</div>
58+
59+
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
60+
{items.map((item) => {
61+
const isLocalFile = item.isLocal;
62+
const attachmentIndex = isLocalFile ? -1 : attachments.findIndex((a) => a.name === item.id);
63+
64+
return (
65+
<AttachmentItemCard
66+
key={item.id}
67+
item={item}
68+
onRemove={() => handleRemoveItem(item)}
69+
onMoveUp={!isLocalFile ? () => handleMoveUp(attachmentIndex) : undefined}
70+
onMoveDown={!isLocalFile ? () => handleMoveDown(attachmentIndex) : undefined}
71+
canMoveUp={!isLocalFile && attachmentIndex > 0}
72+
canMoveDown={!isLocalFile && attachmentIndex < attachments.length - 1}
73+
/>
74+
);
75+
})}
76+
</div>
77+
</div>
78+
);
79+
};
80+
81+
export default AttachmentListV2;
Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,30 @@
11
import type { FC } from "react";
2-
import { AttachmentList, LocationDisplay, RelationList } from "@/components/memo-metadata";
32
import { useEditorContext } from "../state";
43
import type { EditorMetadataProps } from "../types";
4+
import AttachmentListV2 from "./AttachmentListV2";
5+
import LocationDisplayV2 from "./LocationDisplayV2";
6+
import RelationListV2 from "./RelationListV2";
57

68
export const EditorMetadata: FC<EditorMetadataProps> = () => {
79
const { state, actions, dispatch } = useEditorContext();
810

911
return (
1012
<div className="w-full flex flex-col gap-2">
11-
{state.metadata.location && (
12-
<LocationDisplay
13-
mode="edit"
14-
location={state.metadata.location}
15-
onRemove={() => dispatch(actions.setMetadata({ location: undefined }))}
16-
/>
17-
)}
18-
19-
<AttachmentList
20-
mode="edit"
13+
<AttachmentListV2
2114
attachments={state.metadata.attachments}
2215
localFiles={state.localFiles}
2316
onAttachmentsChange={(attachments) => dispatch(actions.setMetadata({ attachments }))}
2417
onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))}
2518
/>
2619

27-
<RelationList
28-
mode="edit"
20+
<RelationListV2
2921
relations={state.metadata.relations}
30-
currentMemoName=""
3122
onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))}
3223
/>
24+
25+
{state.metadata.location && (
26+
<LocationDisplayV2 location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />
27+
)}
3328
</div>
3429
);
3530
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { MapPinIcon, XIcon } from "lucide-react";
2+
import type { FC } from "react";
3+
import { cn } from "@/lib/utils";
4+
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
5+
6+
interface LocationDisplayV2Props {
7+
location: Location;
8+
onRemove?: () => void;
9+
className?: string;
10+
}
11+
12+
const LocationDisplayV2: FC<LocationDisplayV2Props> = ({ location, onRemove, className }) => {
13+
const displayText = location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
14+
15+
return (
16+
<div
17+
className={cn(
18+
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-border bg-background hover:bg-accent/20 transition-all w-full",
19+
className,
20+
)}
21+
>
22+
<MapPinIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
23+
24+
<div className="flex items-center gap-1.5 min-w-0 flex-1">
25+
<span className="text-xs font-medium truncate" title={displayText}>
26+
{displayText}
27+
</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>
31+
</div>
32+
33+
{onRemove && (
34+
<button
35+
type="button"
36+
onClick={onRemove}
37+
className="p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors touch-manipulation shrink-0 ml-auto"
38+
title="Remove"
39+
aria-label="Remove location"
40+
>
41+
<XIcon className="w-3 h-3 text-muted-foreground hover:text-destructive" />
42+
</button>
43+
)}
44+
</div>
45+
);
46+
};
47+
48+
export default LocationDisplayV2;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { LinkIcon, XIcon } from "lucide-react";
2+
import type { FC } from "react";
3+
import { Link } from "react-router-dom";
4+
import { extractMemoIdFromName } from "@/helpers/resource-names";
5+
import { cn } from "@/lib/utils";
6+
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
7+
8+
interface RelationItemCardProps {
9+
memo: MemoRelation_Memo;
10+
onRemove?: () => void;
11+
parentPage?: string;
12+
className?: string;
13+
}
14+
15+
const RelationItemCard: FC<RelationItemCardProps> = ({ memo, onRemove, parentPage, className }) => {
16+
const memoId = extractMemoIdFromName(memo.name);
17+
18+
if (onRemove) {
19+
return (
20+
<div
21+
className={cn(
22+
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
23+
className,
24+
)}
25+
>
26+
<LinkIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
27+
<span className="text-xs font-medium truncate flex-1" title={memo.snippet}>
28+
{memo.snippet}
29+
</span>
30+
31+
<div className="flex-shrink-0 flex items-center gap-0.5">
32+
<button
33+
type="button"
34+
onClick={onRemove}
35+
className="p-0.5 rounded hover:bg-destructive/10 active:bg-destructive/10 transition-colors touch-manipulation"
36+
title="Remove"
37+
aria-label="Remove relation"
38+
>
39+
<XIcon className="w-3 h-3 text-muted-foreground hover:text-destructive" />
40+
</button>
41+
</div>
42+
</div>
43+
);
44+
}
45+
46+
return (
47+
<Link
48+
className={cn(
49+
"relative flex items-center gap-1.5 px-1.5 py-1 rounded border border-transparent hover:border-border hover:bg-accent/20 transition-all",
50+
className,
51+
)}
52+
to={`/${memo.name}`}
53+
viewTransition
54+
state={{ from: parentPage }}
55+
>
56+
<span className="text-[10px] font-mono px-1 py-0.5 rounded bg-muted/50 text-muted-foreground shrink-0">{memoId.slice(0, 6)}</span>
57+
<span className="text-xs truncate flex-1" title={memo.snippet}>
58+
{memo.snippet}
59+
</span>
60+
</Link>
61+
);
62+
};
63+
64+
export default RelationItemCard;

0 commit comments

Comments
 (0)