Unverified Commit 2aaca27b authored by memoclaw's avatar memoclaw Committed by GitHub

refactor(web): improve MemoDetail and sidebar maintainability (#5769)

Co-authored-by: 's avatarmemoclaw <265580040+memoclaw@users.noreply.github.com>
parent e176b28c
...@@ -51,7 +51,6 @@ ...@@ -51,7 +51,6 @@
"mime": "^4.1.0", "mime": "^4.1.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-force-graph-2d": "^1.29.1",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-i18next": "^15.7.4", "react-i18next": "^15.7.4",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
...@@ -73,7 +72,6 @@ ...@@ -73,7 +72,6 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.7", "@biomejs/biome": "^2.4.7",
"baseline-browser-mapping": "^2.10.8",
"@bufbuild/protobuf": "^2.11.0", "@bufbuild/protobuf": "^2.11.0",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
...@@ -89,6 +87,7 @@ ...@@ -89,6 +87,7 @@
"@types/unist": "^3.0.3", "@types/unist": "^3.0.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.7.0", "@vitejs/plugin-react": "^4.7.0",
"baseline-browser-mapping": "^2.10.8",
"long": "^5.3.2", "long": "^5.3.2",
"terser": "^5.46.1", "terser": "^5.46.1",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
......
This diff is collapsed.
import { MessageCircleIcon } from "lucide-react";
import { useState } from "react";
import MemoEditor from "@/components/MemoEditor";
import MemoView from "@/components/MemoView";
import { Button } from "@/components/ui/button";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import useCurrentUser from "@/hooks/useCurrentUser";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
interface Props {
memo: Memo;
comments: Memo[];
parentPage?: string;
}
const MemoCommentSection = ({ memo, comments, parentPage }: Props) => {
const t = useTranslate();
const currentUser = useCurrentUser();
const [showEditor, setShowEditor] = useState(false);
const showCreateButton = currentUser && !showEditor;
const handleCommentCreated = async (_memoCommentName: string) => {
setShowEditor(false);
};
return (
<div className="pt-8 pb-16 w-full">
<h2 id="comments" className="sr-only">
{t("memo.comment.self")}
</h2>
<div className="relative mx-auto grow w-full min-h-full flex flex-col justify-start items-start gap-y-1">
{comments.length === 0 ? (
showCreateButton && (
<div className="w-full flex flex-row justify-center items-center py-6">
<Button variant="ghost" onClick={() => setShowEditor(true)}>
<span className="text-muted-foreground">{t("memo.comment.write-a-comment")}</span>
<MessageCircleIcon className="ml-2 w-5 h-auto text-muted-foreground" />
</Button>
</div>
)
) : (
<div className="w-full flex flex-row justify-between items-center h-8 pl-3 mb-2">
<div className="flex flex-row justify-start items-center">
<MessageCircleIcon className="w-5 h-auto text-muted-foreground mr-1" />
<span className="text-muted-foreground text-sm">{t("memo.comment.self")}</span>
<span className="text-muted-foreground text-sm ml-1">({comments.length})</span>
</div>
{showCreateButton && (
<Button variant="ghost" className="text-muted-foreground" onClick={() => setShowEditor(true)}>
{t("memo.comment.write-a-comment")}
</Button>
)}
</div>
)}
{showEditor && (
<div className="w-full mb-2">
<MemoEditor
cacheKey={`${memo.name}-${memo.updateTime}-comment`}
placeholder={t("editor.add-your-comment-here")}
parentMemoName={memo.name}
autoFocus
onConfirm={handleCommentCreated}
onCancel={() => setShowEditor(false)}
/>
</div>
)}
{comments.map((comment) => (
<div className="w-full" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>
<MemoView memo={comment} parentPage={parentPage} showCreator compact />
</div>
))}
</div>
</div>
);
};
export default MemoCommentSection;
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { timestampDate } from "@bufbuild/protobuf/wkt"; import { timestampDate } from "@bufbuild/protobuf/wkt";
import { isEqual } from "lodash-es"; import { isEqual } from "lodash-es";
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, Share2Icon } from "lucide-react"; import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, type LucideIcon, Share2Icon } from "lucide-react";
import { useState } from "react"; import { useMemo, useState } from "react";
import MemoSharePanel from "@/components/MemoSharePanel";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Memo, Memo_PropertySchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; import { Memo, Memo_PropertySchema } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n"; import { type Translations, useTranslate } from "@/utils/i18n";
import { isSuperUser } from "@/utils/user"; import { isSuperUser } from "@/utils/user";
import MemoRelationForceGraph from "../MemoRelationForceGraph"; import MemoSharePanel from "./MemoSharePanel";
interface Props { interface Props {
memo: Memo; memo: Memo;
className?: string; className?: string;
parentPage?: string;
} }
const SectionLabel = ({ children }: { children: React.ReactNode }) => ( interface PropertyBadge {
<p className="text-xs font-medium text-muted-foreground/50 uppercase tracking-wider">{children}</p> icon: LucideIcon;
labelKey: Translations;
}
const SidebarSection = ({ label, count, children }: { label: string; count?: number; children: React.ReactNode }) => (
<div className="w-full space-y-2">
<div className="flex items-center gap-1.5">
<p className="text-xs font-medium text-muted-foreground/50 uppercase tracking-wider">{label}</p>
{count != null && <span className="text-xs text-muted-foreground/30">({count})</span>}
</div>
{children}
</div>
); );
const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { const PROPERTY_BADGE_CLASSES =
"inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground";
const TAG_BADGE_CLASSES =
"inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer";
const MemoDetailSidebar = ({ memo, className }: Props) => {
const t = useTranslate(); const t = useTranslate();
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const [sharePanelOpen, setSharePanelOpen] = useState(false); const [sharePanelOpen, setSharePanelOpen] = useState(false);
const property = create(Memo_PropertySchema, memo.property || {}); const property = create(Memo_PropertySchema, memo.property || {});
const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode;
const hasReferenceRelations = memo.relations.some((r) => r.type === MemoRelation_Type.REFERENCE);
const canManageShares = !memo.parent && (memo.creator === currentUser?.name || isSuperUser(currentUser)); const canManageShares = !memo.parent && (memo.creator === currentUser?.name || isSuperUser(currentUser));
const hasUpdated = !isEqual(memo.createTime, memo.updateTime);
const propertyBadges = useMemo(() => {
const badges: PropertyBadge[] = [];
if (property.hasLink) badges.push({ icon: LinkIcon, labelKey: "memo.links" });
if (property.hasTaskList) badges.push({ icon: CheckCircleIcon, labelKey: "memo.to-do" });
if (property.hasCode) badges.push({ icon: Code2Icon, labelKey: "memo.code" });
return badges;
}, [property.hasLink, property.hasTaskList, property.hasCode]);
return ( return (
<aside className={cn("relative w-full h-auto max-h-screen overflow-auto flex flex-col gap-5", className)}> <aside className={cn("relative w-full h-auto max-h-screen overflow-auto flex flex-col gap-5", className)}>
{canManageShares && ( {canManageShares && (
<div className="w-full space-y-2"> <SidebarSection label={t("memo.share.section-label")}>
<SectionLabel>{t("memo.share.section-label")}</SectionLabel>
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}> <Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
<Share2Icon className="w-4 h-4" /> <Share2Icon className="w-4 h-4" />
{t("memo.share.open-panel")} {t("memo.share.open-panel")}
</Button> </Button>
</div> </SidebarSection>
)} )}
{hasReferenceRelations && ( <SidebarSection label={t("common.created-at")}>
<div className="w-full space-y-2"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-1.5"> <p className="text-sm text-foreground/70">{memo.createTime ? timestampDate(memo.createTime).toLocaleString() : "—"}</p>
<SectionLabel>{t("common.relations")}</SectionLabel> {hasUpdated && (
<span className="text-xs text-muted-foreground/30">(Beta)</span> <p className="text-xs text-muted-foreground">
</div> {t("common.last-updated-at")}: {memo.updateTime ? timestampDate(memo.updateTime).toLocaleString() : "—"}
<div className="relative w-full h-36 border border-border rounded-lg bg-muted overflow-hidden"> </p>
<MemoRelationForceGraph className="w-full h-full" memo={memo} parentPage={parentPage} /> )}
</div>
</div> </div>
)} </SidebarSection>
<div className="w-full space-y-1">
<SectionLabel>{t("common.created-at")}</SectionLabel>
<p className="text-sm text-foreground/70">{memo.createTime ? timestampDate(memo.createTime).toLocaleString() : "—"}</p>
</div>
{!isEqual(memo.createTime, memo.updateTime) && ( {propertyBadges.length > 0 && (
<div className="w-full space-y-1"> <SidebarSection label={t("common.properties")}>
<SectionLabel>{t("common.last-updated-at")}</SectionLabel>
<p className="text-sm text-foreground/70">{memo.updateTime ? timestampDate(memo.updateTime).toLocaleString() : "—"}</p>
</div>
)}
{hasSpecialProperty && (
<div className="w-full space-y-2">
<SectionLabel>{t("common.properties")}</SectionLabel>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{property.hasLink && ( {propertyBadges.map(({ icon: Icon, labelKey }) => (
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground"> <span key={labelKey} className={PROPERTY_BADGE_CLASSES}>
<LinkIcon className="w-3.5 h-3.5" /> <Icon className="w-3.5 h-3.5" />
{t("memo.links")} {t(labelKey)}
</span>
)}
{property.hasTaskList && (
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground">
<CheckCircleIcon className="w-3.5 h-3.5" />
{t("memo.to-do")}
</span>
)}
{property.hasCode && (
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground">
<Code2Icon className="w-3.5 h-3.5" />
{t("memo.code")}
</span> </span>
)} ))}
</div> </div>
</div> </SidebarSection>
)} )}
{memo.tags.length > 0 && ( {memo.tags.length > 0 && (
<div className="w-full space-y-2"> <SidebarSection label={t("common.tags")} count={memo.tags.length}>
<div className="flex items-center gap-1.5">
<SectionLabel>{t("common.tags")}</SectionLabel>
<span className="text-xs text-muted-foreground/30">({memo.tags.length})</span>
</div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{memo.tags.map((tag) => ( {memo.tags.map((tag) => (
<span <span key={tag} className={TAG_BADGE_CLASSES}>
key={tag}
className="inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer"
>
<HashIcon className="w-3 h-3 opacity-50" /> <HashIcon className="w-3 h-3 opacity-50" />
{tag} {tag}
</span> </span>
))} ))}
</div> </div>
</div> </SidebarSection>
)} )}
{sharePanelOpen && <MemoSharePanel memoName={memo.name} open={sharePanelOpen} onClose={() => setSharePanelOpen(false)} />} {sharePanelOpen && <MemoSharePanel memoName={memo.name} open={sharePanelOpen} onClose={() => setSharePanelOpen(false)} />}
......
...@@ -8,10 +8,9 @@ import MemoDetailSidebar from "./MemoDetailSidebar"; ...@@ -8,10 +8,9 @@ import MemoDetailSidebar from "./MemoDetailSidebar";
interface Props { interface Props {
memo: Memo; memo: Memo;
parentPage?: string;
} }
const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => { const MemoDetailSidebarDrawer = ({ memo }: Props) => {
const location = useLocation(); const location = useLocation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
...@@ -27,7 +26,7 @@ const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => { ...@@ -27,7 +26,7 @@ const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => {
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="right" className="w-full sm:w-80 px-4 bg-background"> <SheetContent side="right" className="w-full sm:w-80 px-4 bg-background">
<MemoDetailSidebar className="py-4" memo={memo} parentPage={parentPage} /> <MemoDetailSidebar className="py-4" memo={memo} />
</SheetContent> </SheetContent>
</Sheet> </Sheet>
); );
......
import { useEffect, useRef, useState } from "react";
import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import useNavigateTo from "@/hooks/useNavigateTo";
import { cn } from "@/lib/utils";
import { Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { LinkType, NodeType } from "./types";
import { convertMemoRelationsToGraphData } from "./utils";
interface Props {
memo: Memo;
className?: string;
parentPage?: string;
}
const MAIN_NODE_COLOR = "#14b8a6";
const DEFAULT_NODE_COLOR = "#a1a1aa";
const MemoRelationForceGraph = ({ className, memo, parentPage }: Props) => {
const navigateTo = useNavigateTo();
const [mode] = useState<"light">("light");
const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<ForceGraphMethods<NodeObject<NodeType>, LinkObject<NodeType, LinkType>> | undefined>(undefined);
const [graphSize, setGraphSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!containerRef.current) return;
setGraphSize(containerRef.current.getBoundingClientRect());
}, []);
const onNodeClick = (node: NodeObject<NodeType>) => {
if (node.memo.name === memo.name) return;
navigateTo(`/${memo.name}`, {
state: {
from: parentPage,
},
});
};
return (
<div ref={containerRef} className={cn("opacity-80", className)}>
<ForceGraph2D
ref={graphRef}
width={graphSize.width}
height={graphSize.height}
enableZoomInteraction
cooldownTicks={0}
nodeColor={(node) => (node.id === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)}
nodeRelSize={3}
nodeLabel={(node) => extractMemoIdFromName(node.memo.name).slice(0, 6).toLowerCase()}
linkColor={() => (mode === "light" ? "#e4e4e7" : "#3f3f46")}
graphData={convertMemoRelationsToGraphData(memo.relations.filter((r) => r.type === MemoRelation_Type.REFERENCE))}
onNodeClick={onNodeClick}
linkDirectionalArrowLength={3}
linkDirectionalArrowRelPos={1}
linkCurvature={0.25}
/>
</div>
);
};
export default MemoRelationForceGraph;
import MemoRelationForceGraph from "./MemoRelationForceGraph";
export * from "./utils";
export default MemoRelationForceGraph;
import { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
export interface NodeType {
memo: MemoRelation_Memo;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface LinkType {
// ...add more additional properties relevant to the link here.
}
import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
import { MemoRelation, MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
import { LinkType, NodeType } from "./types";
export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData<NodeType, LinkType> => {
const nodesMap = new Map<string, NodeObject<NodeType>>();
const links: LinkObject<NodeType, LinkType>[] = [];
// Iterate through memoRelations to populate nodes and links.
memoRelations.forEach((relation) => {
const memo = relation.memo as MemoRelation_Memo;
const relatedMemo = relation.relatedMemo as MemoRelation_Memo;
// Add memo node if not already present.
if (!nodesMap.has(memo.name)) {
nodesMap.set(memo.name, { id: memo.name, memo });
}
// Add related_memo node if not already present.
if (!nodesMap.has(relatedMemo.name)) {
nodesMap.set(relatedMemo.name, { id: relatedMemo.name, memo: relatedMemo });
}
// Create link between memo and relatedMemo.
links.push({
source: memo.name,
target: relatedMemo.name,
type: relation.type, // Include the type of relation as a property of the link.
});
});
return {
nodes: Array.from(nodesMap.values()),
links,
};
};
import { Code, ConnectError } from "@connectrpc/connect";
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import useNavigateTo from "@/hooks/useNavigateTo";
import { AUTH_REASON_PROTECTED_MEMO, redirectOnAuthFailure } from "@/utils/auth-redirect";
interface UseMemoDetailErrorOptions {
error: Error | null;
pathname: string;
search: string;
hash: string;
}
const useMemoDetailError = ({ error, pathname, search, hash }: UseMemoDetailErrorOptions) => {
const navigateTo = useNavigateTo();
useEffect(() => {
if (!error) {
return;
}
if (error instanceof ConnectError) {
if (error.code === Code.Unauthenticated) {
redirectOnAuthFailure(true, {
redirect: `${pathname}${search}${hash}`,
reason: AUTH_REASON_PROTECTED_MEMO,
});
return;
}
if (error.code === Code.PermissionDenied || error.code === Code.NotFound) {
navigateTo("/404", { replace: true });
return;
}
toast.error(error.message);
return;
}
toast.error(error.message);
}, [error, hash, pathname, search, navigateTo]);
};
export default useMemoDetailError;
import { Code, ConnectError } from "@connectrpc/connect"; import { ArrowUpLeftFromCircleIcon } from "lucide-react";
import { ArrowUpLeftFromCircleIcon, MessageCircleIcon } from "lucide-react"; import { useEffect } from "react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { Link, useLocation, useParams } from "react-router-dom"; import { Link, useLocation, useParams } from "react-router-dom";
import MemoCommentSection from "@/components/MemoCommentSection";
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar"; import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
import MemoEditor from "@/components/MemoEditor";
import MemoView from "@/components/MemoView"; import MemoView from "@/components/MemoView";
import MobileHeader from "@/components/MobileHeader"; import MobileHeader from "@/components/MobileHeader";
import { Button } from "@/components/ui/button"; import { memoNamePrefix } from "@/helpers/resource-names";
import { extractMemoIdFromName, memoNamePrefix } from "@/helpers/resource-names";
import useCurrentUser from "@/hooks/useCurrentUser";
import useMediaQuery from "@/hooks/useMediaQuery"; import useMediaQuery from "@/hooks/useMediaQuery";
import useMemoDetailError from "@/hooks/useMemoDetailError";
import { useMemo, useMemoComments } from "@/hooks/useMemoQueries"; import { useMemo, useMemoComments } from "@/hooks/useMemoQueries";
import useNavigateTo from "@/hooks/useNavigateTo";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AUTH_REASON_PROTECTED_MEMO, redirectOnAuthFailure } from "@/utils/auth-redirect";
import { useTranslate } from "@/utils/i18n";
const MemoDetail = () => { const MemoDetail = () => {
const t = useTranslate();
const md = useMediaQuery("md"); const md = useMediaQuery("md");
const params = useParams(); const params = useParams();
const navigateTo = useNavigateTo();
const location = useLocation(); const location = useLocation();
const { state: locationState, hash } = location; const { state: locationState, hash } = location;
const currentUser = useCurrentUser(); const memoName = `${memoNamePrefix}${params.uid}`;
const uid = params.uid;
const memoName = `${memoNamePrefix}${uid}`;
const [showCommentEditor, setShowCommentEditor] = useState(false);
// Fetch main memo with React Query
const { data: memo, error, isLoading } = useMemo(memoName, { enabled: !!memoName }); const { data: memo, error, isLoading } = useMemo(memoName, { enabled: !!memoName });
// Handle errors useMemoDetailError({
useEffect(() => { error: error as Error | null,
if (!error) { pathname: location.pathname,
return; search: location.search,
} hash: location.hash,
});
if (error instanceof ConnectError) {
if (error.code === Code.Unauthenticated) {
redirectOnAuthFailure(true, {
redirect: `${location.pathname}${location.search}${location.hash}`,
reason: AUTH_REASON_PROTECTED_MEMO,
});
return;
}
if (error.code === Code.PermissionDenied || error.code === Code.NotFound) {
navigateTo("/404", { replace: true });
return;
}
toast.error(error.message);
return;
}
toast.error((error as Error).message);
}, [error, location.hash, location.pathname, location.search, navigateTo]);
// Fetch parent memo if exists
const { data: parentMemo } = useMemo(memo?.parent || "", { const { data: parentMemo } = useMemo(memo?.parent || "", {
enabled: !!memo?.parent, enabled: !!memo?.parent,
}); });
// Fetch all comments for this memo in a single query
const { data: commentsResponse } = useMemoComments(memoName, { const { data: commentsResponse } = useMemoComments(memoName, {
enabled: !!memo, enabled: !!memo,
}); });
...@@ -76,26 +42,15 @@ const MemoDetail = () => { ...@@ -76,26 +42,15 @@ const MemoDetail = () => {
el?.scrollIntoView({ behavior: "smooth", block: "center" }); el?.scrollIntoView({ behavior: "smooth", block: "center" });
}, [hash, comments]); }, [hash, comments]);
const showCreateCommentButton = currentUser && !showCommentEditor;
if (isLoading || !memo) { if (isLoading || !memo) {
return null; return null;
} }
const handleShowCommentEditor = () => {
setShowCommentEditor(true);
};
const handleCommentCreated = async (_memoCommentName: string) => {
// React Query will auto-refetch due to invalidation in the mutation
setShowCommentEditor(false);
};
return ( return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"> <section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
{!md && ( {!md && (
<MobileHeader> <MobileHeader>
<MemoDetailSidebarDrawer memo={memo} parentPage={locationState?.from} /> <MemoDetailSidebarDrawer memo={memo} />
</MobileHeader> </MobileHeader>
)} )}
<div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}> <div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
...@@ -115,7 +70,6 @@ const MemoDetail = () => { ...@@ -115,7 +70,6 @@ const MemoDetail = () => {
)} )}
<MemoView <MemoView
key={`${memo.name}-${memo.displayTime}`} key={`${memo.name}-${memo.displayTime}`}
className="shadow hover:shadow-md transition-all"
memo={memo} memo={memo}
compact={false} compact={false}
parentPage={locationState?.from} parentPage={locationState?.from}
...@@ -123,57 +77,11 @@ const MemoDetail = () => { ...@@ -123,57 +77,11 @@ const MemoDetail = () => {
showVisibility showVisibility
showPinned showPinned
/> />
<div className="pt-8 pb-16 w-full"> <MemoCommentSection memo={memo} comments={comments} parentPage={locationState?.from} />
<h2 id="comments" className="sr-only">
{t("memo.comment.self")}
</h2>
<div className="relative mx-auto grow w-full min-h-full flex flex-col justify-start items-start gap-y-1">
{comments.length === 0 ? (
showCreateCommentButton && (
<div className="w-full flex flex-row justify-center items-center py-6">
<Button variant="ghost" onClick={handleShowCommentEditor}>
<span className="text-muted-foreground">{t("memo.comment.write-a-comment")}</span>
<MessageCircleIcon className="ml-2 w-5 h-auto text-muted-foreground" />
</Button>
</div>
)
) : (
<div className="w-full flex flex-row justify-between items-center h-8 pl-3 mb-2">
<div className="flex flex-row justify-start items-center">
<MessageCircleIcon className="w-5 h-auto text-muted-foreground mr-1" />
<span className="text-muted-foreground text-sm">{t("memo.comment.self")}</span>
<span className="text-muted-foreground text-sm ml-1">({comments.length})</span>
</div>
{showCreateCommentButton && (
<Button variant="ghost" className="text-muted-foreground" onClick={handleShowCommentEditor}>
{t("memo.comment.write-a-comment")}
</Button>
)}
</div>
)}
{showCommentEditor && (
<div className="w-full mb-2">
<MemoEditor
cacheKey={`${memo.name}-${memo.updateTime}-comment`}
placeholder={t("editor.add-your-comment-here")}
parentMemoName={memo.name}
autoFocus
onConfirm={handleCommentCreated}
onCancel={() => setShowCommentEditor(false)}
/>
</div>
)}
{comments.map((comment) => (
<div className="w-full" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>
<MemoView memo={comment} parentPage={locationState?.from} showCreator compact />
</div>
))}
</div>
</div>
</div> </div>
{md && ( {md && (
<div className="sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full"> <div className="sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full">
<MemoDetailSidebar className="py-6" memo={memo} parentPage={locationState?.from} /> <MemoDetailSidebar className="py-6" memo={memo} />
</div> </div>
)} )}
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment