diff --git a/web/src/components/MemoEditor/Editor/SlashCommands.tsx b/web/src/components/MemoEditor/Editor/SlashCommands.tsx index 3fabb27d2..883225be0 100644 --- a/web/src/components/MemoEditor/Editor/SlashCommands.tsx +++ b/web/src/components/MemoEditor/Editor/SlashCommands.tsx @@ -1,14 +1,7 @@ -import type { EditorRefActions } from "."; -import type { Command } from "./commands"; +import type { SlashCommandsProps } from "../types"; import { SuggestionsPopup } from "./SuggestionsPopup"; import { useSuggestions } from "./useSuggestions"; -interface SlashCommandsProps { - editorRef: React.RefObject; - editorActions: React.ForwardedRef; - commands: Command[]; -} - const SlashCommands = ({ editorRef, editorActions, commands }: SlashCommandsProps) => { const { position, suggestions, selectedIndex, isVisible, handleItemSelect } = useSuggestions({ editorRef, diff --git a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx index 6ee6cd866..6ef6cd8af 100644 --- a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx +++ b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx @@ -3,15 +3,10 @@ import { matchPath } from "react-router-dom"; import OverflowTip from "@/components/kit/OverflowTip"; import { useTagCounts } from "@/hooks/useUserQueries"; import { Routes } from "@/router"; -import type { EditorRefActions } from "."; +import type { TagSuggestionsProps } from "../types"; import { SuggestionsPopup } from "./SuggestionsPopup"; import { useSuggestions } from "./useSuggestions"; -interface TagSuggestionsProps { - editorRef: React.RefObject; - editorActions: React.ForwardedRef; -} - export default function TagSuggestions({ editorRef, editorActions }: TagSuggestionsProps) { // On explore page, show all users' tags; otherwise show current user's tags const isExplorePage = Boolean(matchPath(Routes.EXPLORE, window.location.pathname)); diff --git a/web/src/components/MemoEditor/Editor/index.tsx b/web/src/components/MemoEditor/Editor/index.tsx index 3e859f96d..2f684c3ed 100644 --- a/web/src/components/MemoEditor/Editor/index.tsx +++ b/web/src/components/MemoEditor/Editor/index.tsx @@ -1,6 +1,7 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react"; import { cn } from "@/lib/utils"; import { EDITOR_HEIGHT } from "../constants"; +import type { EditorProps } from "../types"; import { editorCommands } from "./commands"; import SlashCommands from "./SlashCommands"; import TagSuggestions from "./TagSuggestions"; @@ -22,19 +23,7 @@ export interface EditorRefActions { setLine: (lineNumber: number, text: string) => void; } -interface Props { - className: string; - initialContent: string; - placeholder: string; - onContentChange: (content: string) => void; - onPaste: (event: React.ClipboardEvent) => void; - isFocusMode?: boolean; - isInIME?: boolean; - onCompositionStart?: () => void; - onCompositionEnd?: () => void; -} - -const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef) { +const Editor = forwardRef(function Editor(props: EditorProps, ref: React.ForwardedRef) { const { className, initialContent, diff --git a/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx b/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx index e10411cf1..4f9f8ecd1 100644 --- a/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx +++ b/web/src/components/MemoEditor/Toolbar/InsertMenu.tsx @@ -1,7 +1,7 @@ import { LatLng } from "leaflet"; import { uniqBy } from "lodash-es"; import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react"; -import { useContext, useState } from "react"; +import { useState } from "react"; import type { LocalFile } from "@/components/memo-metadata"; import { Button } from "@/components/ui/button"; import { @@ -14,24 +14,17 @@ import { DropdownMenuTrigger, useDropdownMenuSubHoverDelay, } from "@/components/ui/dropdown-menu"; -import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; +import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; import { LinkMemoDialog, LocationDialog } from "../components"; import { GEOCODING } from "../constants"; -import { useFileUpload, useLinkMemo, useLocation } from "../hooks"; -import { useAbortController } from "../hooks/useAbortController"; -import { MemoEditorContext } from "../types"; +import { useAbortController, useFileUpload, useLinkMemo, useLocation } from "../hooks"; +import { useEditorContext } from "../state"; +import type { InsertMenuProps } from "../types"; -interface Props { - isUploading?: boolean; - location?: Location; - onLocationChange: (location?: Location) => void; - onToggleFocusMode?: () => void; -} - -const InsertMenu = (props: Props) => { +const InsertMenu = (props: InsertMenuProps) => { const t = useTranslate(); - const context = useContext(MemoEditorContext); + const { state, actions, dispatch } = useEditorContext(); const [linkDialogOpen, setLinkDialogOpen] = useState(false); const [locationDialogOpen, setLocationDialogOpen] = useState(false); @@ -46,17 +39,15 @@ const InsertMenu = (props: Props) => { ); const { fileInputRef, selectingFlag, handleFileInputChange, handleUploadClick } = useFileUpload((newFiles: LocalFile[]) => { - if (context.addLocalFiles) { - context.addLocalFiles(newFiles); - } + newFiles.forEach((file) => dispatch(actions.addLocalFile(file))); }); const linkMemo = useLinkMemo({ isOpen: linkDialogOpen, - currentMemoName: context.memoName, - existingRelations: context.relationList, + currentMemoName: props.memoName, + existingRelations: state.metadata.relations, onAddRelation: (relation: MemoRelation) => { - context.setRelationList(uniqBy([...context.relationList, relation], (r) => r.relatedMemo?.name)); + dispatch(actions.setMetadata({ relations: uniqBy([...state.metadata.relations, relation], (r) => r.relatedMemo?.name) })); setLinkDialogOpen(false); }, }); diff --git a/web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx b/web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx index 6172ac366..e74f6d240 100644 --- a/web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx +++ b/web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx @@ -3,14 +3,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import VisibilityIcon from "@/components/VisibilityIcon"; import { Visibility } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; +import type { VisibilitySelectorProps } from "../types"; -interface Props { - value: Visibility; - onChange: (visibility: Visibility) => void; - onOpenChange?: (open: boolean) => void; -} - -const VisibilitySelector = (props: Props) => { +const VisibilitySelector = (props: VisibilitySelectorProps) => { const { value, onChange } = props; const t = useTranslate(); diff --git a/web/src/components/MemoEditor/components/EditorContent.tsx b/web/src/components/MemoEditor/components/EditorContent.tsx index 2c7ae13d4..e371dd600 100644 --- a/web/src/components/MemoEditor/components/EditorContent.tsx +++ b/web/src/components/MemoEditor/components/EditorContent.tsx @@ -3,11 +3,7 @@ import type { LocalFile } from "@/components/memo-metadata"; import Editor, { type EditorRefActions } from "../Editor"; import { useBlobUrls, useDragAndDrop } from "../hooks"; import { useEditorContext } from "../state"; - -interface EditorContentProps { - placeholder?: string; - autoFocus?: boolean; -} +import type { EditorContentProps } from "../types"; export const EditorContent = forwardRef(({ placeholder }, ref) => { const { state, actions, dispatch } = useEditorContext(); diff --git a/web/src/components/MemoEditor/components/EditorMetadata.tsx b/web/src/components/MemoEditor/components/EditorMetadata.tsx index 4f0b2d4dc..555e4358d 100644 --- a/web/src/components/MemoEditor/components/EditorMetadata.tsx +++ b/web/src/components/MemoEditor/components/EditorMetadata.tsx @@ -1,8 +1,9 @@ import type { FC } from "react"; import { AttachmentList, LocationDisplay, RelationList } from "@/components/memo-metadata"; import { useEditorContext } from "../state"; +import type { EditorMetadataProps } from "../types"; -export const EditorMetadata: FC = () => { +export const EditorMetadata: FC = () => { const { state, actions, dispatch } = useEditorContext(); return ( diff --git a/web/src/components/MemoEditor/components/EditorToolbar.tsx b/web/src/components/MemoEditor/components/EditorToolbar.tsx index 5ef214314..15e8db194 100644 --- a/web/src/components/MemoEditor/components/EditorToolbar.tsx +++ b/web/src/components/MemoEditor/components/EditorToolbar.tsx @@ -4,13 +4,9 @@ import { validationService } from "../services"; import { useEditorContext } from "../state"; import InsertMenu from "../Toolbar/InsertMenu"; import VisibilitySelector from "../Toolbar/VisibilitySelector"; +import type { EditorToolbarProps } from "../types"; -interface EditorToolbarProps { - onSave: () => void; - onCancel?: () => void; -} - -export const EditorToolbar: FC = ({ onSave, onCancel }) => { +export const EditorToolbar: FC = ({ onSave, onCancel, memoName }) => { const { state, actions, dispatch } = useEditorContext(); const { valid } = validationService.canSave(state); @@ -36,6 +32,7 @@ export const EditorToolbar: FC = ({ onSave, onCancel }) => { location={state.metadata.location} onLocationChange={handleLocationChange} onToggleFocusMode={handleToggleFocusMode} + memoName={memoName} /> diff --git a/web/src/components/MemoEditor/components/FocusModeOverlay.tsx b/web/src/components/MemoEditor/components/FocusModeOverlay.tsx index 6b8ac289d..871c89750 100644 --- a/web/src/components/MemoEditor/components/FocusModeOverlay.tsx +++ b/web/src/components/MemoEditor/components/FocusModeOverlay.tsx @@ -1,11 +1,7 @@ import { Minimize2Icon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { FOCUS_MODE_STYLES } from "../constants"; - -interface FocusModeOverlayProps { - isActive: boolean; - onToggle: () => void; -} +import type { FocusModeExitButtonProps, FocusModeOverlayProps } from "../types"; export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) { if (!isActive) return null; @@ -21,12 +17,6 @@ export function FocusModeOverlay({ isActive, onToggle }: FocusModeOverlayProps) ); } -interface FocusModeExitButtonProps { - isActive: boolean; - onToggle: () => void; - title: string; -} - export function FocusModeExitButton({ isActive, onToggle, title }: FocusModeExitButtonProps) { if (!isActive) return null; diff --git a/web/src/components/MemoEditor/components/LinkMemoDialog.tsx b/web/src/components/MemoEditor/components/LinkMemoDialog.tsx index 0b6875dec..29761889c 100644 --- a/web/src/components/MemoEditor/components/LinkMemoDialog.tsx +++ b/web/src/components/MemoEditor/components/LinkMemoDialog.tsx @@ -1,8 +1,8 @@ import { timestampDate } from "@bufbuild/protobuf/wkt"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Memo } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; +import type { LinkMemoDialogProps } from "../types"; function highlightSearchText(content: string, searchText: string): React.ReactNode { if (!searchText) return content; @@ -29,16 +29,6 @@ function highlightSearchText(content: string, searchText: string): React.ReactNo ); } -interface LinkMemoDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - searchText: string; - onSearchChange: (text: string) => void; - filteredMemos: Memo[]; - isFetching: boolean; - onSelectMemo: (memo: Memo) => void; -} - export const LinkMemoDialog = ({ open, onOpenChange, diff --git a/web/src/components/MemoEditor/components/LocationDialog.tsx b/web/src/components/MemoEditor/components/LocationDialog.tsx index da5a088bd..99f39d05c 100644 --- a/web/src/components/MemoEditor/components/LocationDialog.tsx +++ b/web/src/components/MemoEditor/components/LocationDialog.tsx @@ -1,4 +1,3 @@ -import { LatLng } from "leaflet"; import LeafletMap from "@/components/LeafletMap"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; @@ -7,19 +6,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { VisuallyHidden } from "@/components/ui/visually-hidden"; import { useTranslate } from "@/utils/i18n"; -import { LocationState } from "../types/insert-menu"; - -interface LocationDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - state: LocationState; - locationInitialized: boolean; - onPositionChange: (position: LatLng) => void; - onUpdateCoordinate: (type: "lat" | "lng", value: string) => void; - onPlaceholderChange: (value: string) => void; - onCancel: () => void; - onConfirm: () => void; -} +import type { LocationDialogProps } from "../types"; export const LocationDialog = ({ open, diff --git a/web/src/components/MemoEditor/hooks/index.ts b/web/src/components/MemoEditor/hooks/index.ts index a9f799aa8..269c39d14 100644 --- a/web/src/components/MemoEditor/hooks/index.ts +++ b/web/src/components/MemoEditor/hooks/index.ts @@ -7,6 +7,5 @@ export { useFileUpload } from "./useFileUpload"; export { useFocusMode } from "./useFocusMode"; export { useKeyboard } from "./useKeyboard"; export { useLinkMemo } from "./useLinkMemo"; -export { useLocalFileManager } from "./useLocalFileManager"; export { useLocation } from "./useLocation"; export { useMemoInit } from "./useMemoInit"; diff --git a/web/src/components/MemoEditor/hooks/useDragAndDrop.ts b/web/src/components/MemoEditor/hooks/useDragAndDrop.ts index 85668c0f1..eb8edb121 100644 --- a/web/src/components/MemoEditor/hooks/useDragAndDrop.ts +++ b/web/src/components/MemoEditor/hooks/useDragAndDrop.ts @@ -1,26 +1,18 @@ -import { useState } from "react"; - export function useDragAndDrop(onDrop: (files: FileList) => void) { - const [isDragging, setIsDragging] = useState(false); - return { - isDragging, dragHandlers: { onDragOver: (e: React.DragEvent) => { if (e.dataTransfer?.types.includes("Files")) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; - setIsDragging(true); } }, onDragLeave: (e: React.DragEvent) => { e.preventDefault(); - setIsDragging(false); }, onDrop: (e: React.DragEvent) => { if (e.dataTransfer?.files.length) { e.preventDefault(); - setIsDragging(false); onDrop(e.dataTransfer.files); } }, diff --git a/web/src/components/MemoEditor/hooks/useLocalFileManager.ts b/web/src/components/MemoEditor/hooks/useLocalFileManager.ts deleted file mode 100644 index 91297f842..000000000 --- a/web/src/components/MemoEditor/hooks/useLocalFileManager.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useState } from "react"; -import type { LocalFile } from "@/components/memo-metadata"; -import { useBlobUrls } from "./useBlobUrls"; - -export function useLocalFileManager() { - const [localFiles, setLocalFiles] = useState([]); - const { createBlobUrl, revokeBlobUrl } = useBlobUrls(); - - const addFiles = (files: FileList | File[]): void => { - const fileArray = Array.from(files); - const newLocalFiles: LocalFile[] = fileArray.map((file) => ({ - file, - previewUrl: createBlobUrl(file), - })); - setLocalFiles((prev) => [...prev, ...newLocalFiles]); - }; - - const removeFile = (previewUrl: string): void => { - setLocalFiles((prev) => { - const toRemove = prev.find((f) => f.previewUrl === previewUrl); - if (toRemove) { - revokeBlobUrl(toRemove.previewUrl); - } - return prev.filter((f) => f.previewUrl !== previewUrl); - }); - }; - - const clearFiles = (): void => { - localFiles.forEach(({ previewUrl }) => revokeBlobUrl(previewUrl)); - setLocalFiles([]); - }; - - return { - localFiles, - addFiles, - removeFile, - clearFiles, - }; -} diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 99e8ac88c..145c0843f 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from "@tanstack/react-query"; -import { useMemo, useRef } from "react"; +import { useRef } from "react"; import { toast } from "react-hot-toast"; import useCurrentUser from "@/hooks/useCurrentUser"; import { memoKeys } from "@/hooks/useMemoQueries"; @@ -13,18 +13,7 @@ import type { EditorRefActions } from "./Editor"; import { useAutoSave, useFocusMode, useKeyboard, useMemoInit } from "./hooks"; import { cacheService, errorService, memoService, validationService } from "./services"; import { EditorProvider, useEditorContext } from "./state"; -import { MemoEditorContext } from "./types"; - -export interface MemoEditorProps { - className?: string; - cacheKey?: string; - placeholder?: string; - memoName?: string; - parentMemoName?: string; - autoFocus?: boolean; - onConfirm?: (memoName: string) => void; - onCancel?: () => void; -} +import type { MemoEditorProps } from "./types"; const MemoEditor = (props: MemoEditorProps) => { const { className, cacheKey, memoName, parentMemoName, autoFocus, placeholder, onConfirm, onCancel } = props; @@ -61,26 +50,6 @@ const MemoEditorImpl: React.FC = ({ const editorRef = useRef(null); const { state, actions, dispatch } = useEditorContext(); - // Bridge for old MemoEditorContext (used by InsertMenu and other components) - const legacyContextValue = useMemo( - () => ({ - attachmentList: state.metadata.attachments, - relationList: state.metadata.relations, - setAttachmentList: (attachments: typeof state.metadata.attachments) => dispatch(actions.setMetadata({ attachments })), - setRelationList: (relations: typeof state.metadata.relations) => dispatch(actions.setMetadata({ relations })), - memoName, - addLocalFiles: (files: typeof state.localFiles) => { - files.forEach((file) => { - dispatch(actions.addLocalFile(file)); - }); - }, - removeLocalFile: (previewUrl: string) => dispatch(actions.removeLocalFile(previewUrl)), - localFiles: state.localFiles, - }), - [state.metadata.attachments, state.metadata.relations, state.localFiles, memoName, actions, dispatch], - ); - - // Initialize editor (load memo or cache) useMemoInit(editorRef, memoName, cacheKey, currentUser?.name ?? "", autoFocus); // Auto-save content to localStorage @@ -149,7 +118,7 @@ const MemoEditorImpl: React.FC = ({ } return ( - + <> {/* @@ -175,10 +144,10 @@ const MemoEditorImpl: React.FC = ({ {/* Metadata and toolbar grouped together at bottom */}
- +
-
+ ); }; diff --git a/web/src/components/MemoEditor/types/components.ts b/web/src/components/MemoEditor/types/components.ts new file mode 100644 index 000000000..b86d0a008 --- /dev/null +++ b/web/src/components/MemoEditor/types/components.ts @@ -0,0 +1,99 @@ +import type { LatLng } from "leaflet"; +import type { Location, Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb"; +import type { EditorRefActions } from "../Editor"; +import type { Command } from "../Editor/commands"; +import type { LocationState } from "./insert-menu"; + +export interface MemoEditorProps { + className?: string; + cacheKey?: string; + placeholder?: string; + memoName?: string; + parentMemoName?: string; + autoFocus?: boolean; + onConfirm?: (memoName: string) => void; + onCancel?: () => void; +} + +export interface EditorContentProps { + placeholder?: string; + autoFocus?: boolean; +} + +export interface EditorToolbarProps { + onSave: () => void; + onCancel?: () => void; + memoName?: string; +} + +export interface EditorMetadataProps {} + +export interface FocusModeOverlayProps { + isActive: boolean; + onToggle: () => void; +} + +export interface FocusModeExitButtonProps { + isActive: boolean; + onToggle: () => void; + title: string; +} + +export interface LinkMemoDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + searchText: string; + onSearchChange: (text: string) => void; + filteredMemos: Memo[]; + isFetching: boolean; + onSelectMemo: (memo: Memo) => void; +} + +export interface LocationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + state: LocationState; + locationInitialized: boolean; + onPositionChange: (position: LatLng) => void; + onUpdateCoordinate: (type: "lat" | "lng", value: string) => void; + onPlaceholderChange: (placeholder: string) => void; + onCancel: () => void; + onConfirm: () => void; +} + +export interface InsertMenuProps { + isUploading?: boolean; + location?: Location; + onLocationChange: (location?: Location) => void; + onToggleFocusMode?: () => void; + memoName?: string; +} + +export interface TagSuggestionsProps { + editorRef: React.RefObject; + editorActions: React.ForwardedRef; +} + +export interface SlashCommandsProps { + editorRef: React.RefObject; + editorActions: React.ForwardedRef; + commands: Command[]; +} + +export interface EditorProps { + className: string; + initialContent: string; + placeholder: string; + onContentChange: (content: string) => void; + onPaste: (event: React.ClipboardEvent) => void; + isFocusMode?: boolean; + isInIME?: boolean; + onCompositionStart?: () => void; + onCompositionEnd?: () => void; +} + +export interface VisibilitySelectorProps { + value: Visibility; + onChange: (visibility: Visibility) => void; + onOpenChange?: (open: boolean) => void; +} diff --git a/web/src/components/MemoEditor/types/index.ts b/web/src/components/MemoEditor/types/index.ts index 8474d38d7..77485691a 100644 --- a/web/src/components/MemoEditor/types/index.ts +++ b/web/src/components/MemoEditor/types/index.ts @@ -1,3 +1,19 @@ // MemoEditor type exports + +export type { + EditorContentProps, + EditorMetadataProps, + EditorProps, + EditorToolbarProps, + FocusModeExitButtonProps, + FocusModeOverlayProps, + InsertMenuProps, + LinkMemoDialogProps, + LocationDialogProps, + MemoEditorProps, + SlashCommandsProps, + TagSuggestionsProps, + VisibilitySelectorProps, +} from "./components"; export { MemoEditorContext, type MemoEditorContextValue } from "./context"; export type { LocationState } from "./insert-menu";