feat: implement part of full-screen layout

This commit is contained in:
Steven 2023-12-18 22:10:36 +08:00
parent 15a091fe4c
commit d6656db20d
21 changed files with 116 additions and 255 deletions

View file

@ -12,8 +12,8 @@ import { useUserV1Store } from "./store/v1";
const App = () => {
const { i18n } = useTranslation();
const navigateTo = useNavigateTo();
const globalStore = useGlobalStore();
const { mode, setMode } = useColorScheme();
const globalStore = useGlobalStore();
const userV1Store = useUserV1Store();
const [loading, setLoading] = useState(true);
const { appearance, locale, systemStatus } = globalStore.state;
@ -30,7 +30,7 @@ const App = () => {
try {
await userV1Store.fetchCurrentUser();
} catch (error) {
// Skip.
// Do nothing.
}
setLoading(false);
};
@ -75,8 +75,8 @@ const App = () => {
}
}, [systemStatus.additionalScript]);
// Dynamic update metadata with customized profile.
useEffect(() => {
// dynamic update metadata with customized profile.
document.title = systemStatus.customizedProfile.name;
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
link.href = systemStatus.customizedProfile.logoUrl || "/logo.png";

View file

@ -1,62 +0,0 @@
import copy from "copy-to-clipboard";
import React from "react";
import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
interface Props extends DialogProps {
memoId: MemoId;
}
const EmbedMemoDialog: React.FC<Props> = (props: Props) => {
const t = useTranslate();
const { memoId, destroy } = props;
const memoEmbeddedCode = () => {
return `<iframe style="width:100%;height:auto;min-width:256px;" src="${window.location.origin}/m/${memoId}/embed" frameBorder="0"></iframe>`;
};
const handleCopyCode = () => {
copy(memoEmbeddedCode());
toast.success("Succeed to copy code to clipboard.");
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">{t("embed-memo.title")}</p>
<button className="btn close-btn" onClick={() => destroy()}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container !w-80">
<p className="text-base leading-6 mb-2">{t("embed-memo.text")}</p>
<pre className="w-full font-mono text-sm p-3 border rounded-lg">
<code className="w-full break-all whitespace-pre-wrap">{memoEmbeddedCode()}</code>
</pre>
<p className="w-full text-sm leading-6 flex flex-row justify-between items-center mt-2">
<span className="italic opacity-80">{t("embed-memo.only-public-supported")}</span>
<span className="btn-primary" onClick={handleCopyCode}>
{t("embed-memo.copy")}
</span>
</p>
</div>
</>
);
};
function showEmbedMemoDialog(memoId: MemoId) {
generateDialog(
{
className: "embed-memo-dialog",
dialogName: "embed-memo-dialog",
},
EmbedMemoDialog,
{
memoId,
}
);
}
export default showEmbedMemoDialog;

View file

@ -12,7 +12,7 @@ const MobileHeader = (props: Props) => {
const [titleText] = useState("MEMOS");
return (
<div className="sticky top-0 pt-4 sm:pt-1 pb-1 mb-1 backdrop-blur bg-zinc-100 dark:bg-zinc-800 bg-opacity-70 flex md:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-2">
<div className="sticky top-0 pt-4 sm:pt-1 pb-1 mb-1 backdrop-blur flex md:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-2">
<div className="flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden">
{!sm && <NavigationDrawer />}
<span

View file

@ -72,7 +72,7 @@ const Navigation = () => {
id: "header-explore",
path: "/explore",
title: t("common.explore"),
icon: <Icon.Hash className="mr-3 w-6 h-auto opacity-70" />,
icon: <Icon.Globe2 className="mr-3 w-6 h-auto opacity-70" />,
};
const archivedNavLink: NavLinkItem = {
id: "header-archived",
@ -108,7 +108,7 @@ const Navigation = () => {
id={navLink.id}
className={({ isActive }) =>
classNames(
"px-4 pr-5 py-2 rounded-2xl border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
"w-full px-4 pr-5 py-2 rounded-2xl border flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700",
isActive ? "bg-white drop-shadow-sm dark:bg-zinc-700 border-gray-200 dark:border-zinc-600" : "border-transparent"
)
}

View file

@ -8,7 +8,6 @@ import toImage from "@/labs/html2image";
import { useUserV1Store, extractUsernameFromName } from "@/store/v1";
import { useTranslate } from "@/utils/i18n";
import { generateDialog } from "./Dialog";
import showEmbedMemoDialog from "./EmbedMemoDialog";
import Icon from "./Icon";
import MemoContentV1 from "./MemoContentV1";
import MemoResourceListView from "./MemoResourceListView";
@ -65,10 +64,6 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
});
};
const handleShowEmbedMemoDialog = () => {
showEmbedMemoDialog(memo.id);
};
const handleCopyLinkBtnClick = () => {
copy(`${window.location.origin}/m/${memo.id}`);
toast.success(t("message.succeed-copy-link"));
@ -96,10 +91,6 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
)}
{t("common.image")}
</Button>
<Button color="neutral" variant="outlined" onClick={handleShowEmbedMemoDialog}>
<Icon.Code className="w-4 h-auto mr-1" />
{t("memo.embed")}
</Button>
<Button color="neutral" variant="outlined" onClick={handleCopyLinkBtnClick}>
<Icon.Link className="w-4 h-auto mr-1" />
{t("common.link")}

View file

@ -16,7 +16,7 @@ const UserBanner = () => {
const globalStore = useGlobalStore();
const { systemStatus } = globalStore.state;
const user = useCurrentUser();
const title = user ? user.nickname : systemStatus.customizedProfile.name || "memos";
const title = user ? user.nickname || extractUsernameFromName(user.name) : systemStatus.customizedProfile.name || "memos";
const handleMyAccountClick = () => {
navigateTo(`/u/${encodeURIComponent(extractUsernameFromName(user.name))}`);

View file

@ -1,8 +1,8 @@
html,
body {
@apply text-base w-full h-full overflow-hidden dark:bg-zinc-800;
@apply text-base w-full min-h-full bg-zinc-100 dark:bg-zinc-800;
}
#root {
@apply w-full h-full overflow-auto;
@apply w-full h-auto overflow-auto;
}

View file

@ -1,18 +1,23 @@
import { Outlet } from "react-router-dom";
import DemoBanner from "@/components/DemoBanner";
import Navigation from "@/components/Navigation";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
function Root() {
const { sm } = useResponsiveWidth();
return (
<div className="w-full min-h-full bg-zinc-100 dark:bg-zinc-800">
<div className="w-full min-h-full">
<div className="w-full h-auto flex flex-col justify-start items-center">
<DemoBanner />
</div>
<div className="w-full max-w-6xl mx-auto flex flex-row justify-center items-start sm:px-4">
<div className="hidden sm:block sticky top-0 left-0 w-56">
<Navigation />
</div>
<main className="w-full min-h-screen sm:max-w-[calc(100%-14rem)] flex-grow shrink flex flex-col justify-start items-start">
<div className="w-full sm:pl-56 mx-auto flex flex-row justify-center items-start">
{sm && (
<div className="hidden sm:block fixed top-0 left-0 w-56 border-r dark:border-zinc-800 h-full bg-zinc-50 dark:bg-zinc-700 dark:bg-opacity-40 transition-all hover:shadow-xl">
<Navigation />
</div>
)}
<main className="w-full sm:px-4 min-h-screen flex-grow shrink flex flex-col justify-start items-center">
<Outlet />
</main>
</div>

View file

@ -35,7 +35,7 @@ const Archived = () => {
}, [memos, textQuery]);
return (
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8">
<MobileHeader />
<MemoFilter />
{loadingState.isLoading ? (

View file

@ -64,7 +64,7 @@ const DailyReview = () => {
};
return (
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8">
<MobileHeader />
<div className="w-full shadow flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
<div className="relative w-full flex flex-row justify-start items-center">

View file

@ -1,62 +0,0 @@
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useParams } from "react-router-dom";
import MemoContentV1 from "@/components/MemoContentV1";
import MemoResourceListView from "@/components/MemoResourceListView";
import { UNKNOWN_ID } from "@/helpers/consts";
import { getDateTimeString } from "@/helpers/datetime";
import useLoading from "@/hooks/useLoading";
import { useMemoStore } from "@/store/module";
interface State {
memo: Memo;
}
const EmbedMemo = () => {
const params = useParams();
const memoStore = useMemoStore();
const [state, setState] = useState<State>({
memo: {
id: UNKNOWN_ID,
} as Memo,
});
const loadingState = useLoading();
useEffect(() => {
const memoId = Number(params.memoId);
if (memoId && !isNaN(memoId)) {
memoStore
.fetchMemoById(memoId)
.then((memo) => {
setState({
memo,
});
loadingState.setFinish();
})
.catch((error) => {
toast.error(error.response.data.message);
});
}
}, []);
return (
<section className="w-full h-full flex flex-row justify-start items-start p-2">
{!loadingState.isLoading && (
<div className="w-full max-w-lg mx-auto my-auto shadow px-4 py-4 rounded-lg">
<div className="w-full flex flex-col justify-start items-start">
<div className="w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300">
<span>{getDateTimeString(state.memo.displayTs)}</span>
<a className="ml-2 hover:underline hover:text-green-600" href={`/u/${state.memo.creatorUsername}`}>
@{state.memo.creatorName}
</a>
</div>
<MemoContentV1 className="memo-content" content={state.memo.content} onMemoContentClick={() => undefined} />
<MemoResourceListView resourceList={state.memo.resourceList} />
</div>
</div>
)}
</section>
);
};
export default EmbedMemo;

View file

@ -54,7 +54,7 @@ const Explore = () => {
};
return (
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8">
<MobileHeader />
<div className="relative w-full h-auto flex flex-col justify-start items-start">
<MemoFilter />

View file

@ -8,8 +8,8 @@ import useResponsiveWidth from "@/hooks/useResponsiveWidth";
const Home = () => {
const { md } = useResponsiveWidth();
return (
<div className="w-full flex flex-row justify-start items-start">
<div className="w-full px-4 md:max-w-[calc(100%-14rem)] sm:px-2 sm:pt-4">
<div className="w-full flex flex-row justify-center items-start">
<div className="w-full px-4 max-w-3xl sm:px-2 sm:pt-4">
<MobileHeader>{!md && <HomeSidebarDrawer />}</MobileHeader>
<MemoEditor className="mb-2" cacheKey="home-memo-editor" />
<MemoList />

View file

@ -20,7 +20,7 @@ const Inboxes = () => {
}, []);
return (
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8">
<MobileHeader />
<div className="w-full shadow flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
<div className="relative w-full flex flex-row justify-between items-center">

View file

@ -2,7 +2,7 @@ import Icon from "@/components/Icon";
function Loading() {
return (
<div className="flex flex-row justify-center items-center w-full h-full bg-zinc-100 dark:bg-zinc-800">
<div className="flex flex-row justify-center items-center w-full h-full">
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-center items-center">
<Icon.Loader className="animate-spin dark:text-gray-200" />
</div>

View file

@ -111,92 +111,86 @@ const MemoDetail = () => {
return (
<>
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-900">
<div className="relative w-full h-auto mx-auto flex flex-col justify-start items-center bg-white dark:bg-zinc-700">
<div className="w-full flex flex-col justify-start items-center pt-16 pb-8">
<UserAvatar className="!w-20 !h-20 mb-2 drop-shadow" avatarUrl={systemStatus.customizedProfile.logoUrl} />
<p className="text-3xl text-black opacity-80 dark:text-gray-200">{systemStatus.customizedProfile.name}</p>
</div>
<div className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 pb-6">
{memo.parent && (
<div className="w-auto mb-4">
<Link
className="px-3 py-1 border rounded-full max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-400 dark:border-gray-500 hover:shadow hover:opacity-80"
to={`/m/${memo.parent.id}`}
>
<Icon.ArrowUpLeftFromCircle className="w-4 h-auto shrink-0 opacity-60" />
<span className="mx-1 opacity-60">#{memo.parent.id}</span>
<span className="truncate">{memo.parent.content}</span>
</Link>
</div>
)}
<div className="w-full mb-4 flex flex-row justify-start items-center mr-1">
<span className="text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8">
<div className="relative flex-grow w-full min-h-full flex flex-col justify-start items-start border dark:border-zinc-700 bg-white dark:bg-zinc-700 shadow hover:shadow-xl transition-all p-4 pb-3 rounded-lg">
{memo.parent && (
<div className="w-auto mb-2">
<Link
className="px-3 py-1 border rounded-full max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-gray-600 dark:text-gray-400 dark:border-gray-500 hover:shadow hover:opacity-80"
to={`/m/${memo.parent.id}`}
>
<Icon.ArrowUpLeftFromCircle className="w-4 h-auto shrink-0 opacity-60" />
<span className="mx-1 opacity-60">#{memo.parent.id}</span>
<span className="truncate">{memo.parent.content}</span>
</Link>
</div>
<MemoContentV1 content={memo.content} />
<MemoResourceListView resourceList={memo.resourceList} />
<MemoRelationListView memo={memo} relationList={referenceRelations} />
<div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
<div className="flex flex-row justify-start items-center">
<Tooltip title={"Identifier"} placement="top">
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
)}
<div className="w-full mb-2 flex flex-row justify-start items-center">
<span className="text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
</div>
<MemoContentV1 content={memo.content} />
<MemoResourceListView resourceList={memo.resourceList} />
<MemoRelationListView memo={memo} relationList={referenceRelations} />
<div className="w-full mt-4 flex flex-col sm:flex-row justify-start sm:justify-between sm:items-center gap-2">
<div className="flex flex-row justify-start items-center">
<Tooltip title={"Identifier"} placement="top">
<span className="text-sm text-gray-500 dark:text-gray-400">#{memo.id}</span>
</Tooltip>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}>
<Tooltip title={"Creator"} placement="top">
<span className="flex flex-row justify-start items-center">
<UserAvatar className="!w-5 !h-5 mr-1" avatarUrl={creator?.avatarUrl} />
<span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-gray-400">{creator?.nickname}</span>
</span>
</Tooltip>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Link to={`/u/${encodeURIComponent(memo.creatorUsername)}`}>
<Tooltip title={"Creator"} placement="top">
<span className="flex flex-row justify-start items-center">
<UserAvatar className="!w-5 !h-5 mr-1" avatarUrl={creator?.avatarUrl} />
<span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-gray-400">{creator?.nickname}</span>
</span>
</Tooltip>
</Link>
{allowEdit && (
<>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Select
className="w-auto text-sm"
variant="plain"
value={memo.visibility}
startDecorator={<VisibilityIcon visibility={memo.visibility} />}
onChange={(_, visibility) => {
if (visibility) {
handleMemoVisibilityOptionChanged(visibility);
}
}}
>
{VISIBILITY_SELECTOR_ITEMS.map((item) => (
<Option key={item} value={item} className="whitespace-nowrap" disabled={disableOption(item)}>
{t(`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`)}
</Option>
))}
</Select>
</>
)}
</div>
<div className="flex flex-row sm:justify-end items-center">
{allowEdit && (
<Tooltip title={"Edit"} placement="top">
<IconButton size="sm" onClick={handleEditMemoClick}>
<Icon.Edit3 className="w-4 h-auto text-gray-600 dark:text-gray-400" />
</IconButton>
</Tooltip>
)}
<Tooltip title={"Copy link"} placement="top">
<IconButton size="sm" onClick={handleCopyLinkBtnClick}>
<Icon.Link className="w-4 h-auto text-gray-600 dark:text-gray-400" />
</Link>
{allowEdit && (
<>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
<Select
className="w-auto text-sm"
variant="plain"
value={memo.visibility}
startDecorator={<VisibilityIcon visibility={memo.visibility} />}
onChange={(_, visibility) => {
if (visibility) {
handleMemoVisibilityOptionChanged(visibility);
}
}}
>
{VISIBILITY_SELECTOR_ITEMS.map((item) => (
<Option key={item} value={item} className="whitespace-nowrap" disabled={disableOption(item)}>
{t(`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`)}
</Option>
))}
</Select>
</>
)}
</div>
<div className="flex flex-row sm:justify-end items-center">
{allowEdit && (
<Tooltip title={"Edit"} placement="top">
<IconButton size="sm" onClick={handleEditMemoClick}>
<Icon.Edit3 className="w-4 h-auto text-gray-600 dark:text-gray-400" />
</IconButton>
</Tooltip>
<Tooltip title={"Share"} placement="top">
<IconButton size="sm" onClick={() => showShareMemoDialog(memo)}>
<Icon.Share className="w-4 h-auto text-gray-600 dark:text-gray-400" />
</IconButton>
</Tooltip>
</div>
)}
<Tooltip title={"Copy link"} placement="top">
<IconButton size="sm" onClick={handleCopyLinkBtnClick}>
<Icon.Link className="w-4 h-auto text-gray-600 dark:text-gray-400" />
</IconButton>
</Tooltip>
<Tooltip title={"Share"} placement="top">
<IconButton size="sm" onClick={() => showShareMemoDialog(memo)}>
<Icon.Share className="w-4 h-auto text-gray-600 dark:text-gray-400" />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="pt-8 pb-16 w-full border-t dark:border-t-zinc-700">
<div className="relative mx-auto flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 gap-y-1">
<div className="pt-8 pb-16 w-full">
<div className="relative mx-auto flex-grow w-full min-h-full flex flex-col justify-start items-start gap-y-1">
{comments.length === 0 ? (
<div className="w-full flex flex-col justify-center items-center py-6 mb-2">
<Icon.MessageCircle strokeWidth={1} className="w-8 h-auto text-gray-400" />

View file

@ -7,7 +7,7 @@ const NotFound = () => {
const t = useTranslate();
return (
<div className="w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<div className="w-full h-full overflow-y-auto overflow-x-hidden">
<div className="w-full h-full flex flex-col justify-center items-center">
<Icon.Meh strokeWidth={1} className="w-20 h-auto opacity-80 dark:text-gray-300" />
<p className="mt-4 text-5xl font-mono dark:text-gray-300">404</p>

View file

@ -66,7 +66,7 @@ const Resources = () => {
};
return (
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8">
<MobileHeader />
<div className="w-full shadow flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
<div className="relative w-full flex flex-row justify-between items-center">

View file

@ -42,7 +42,7 @@ const Setting = () => {
};
return (
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
<section className="@container w-full max-w-3xl min-h-full flex flex-col justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8">
<MobileHeader />
<div className="setting-page-wrapper">
<div className="section-selector-container">

View file

@ -36,7 +36,7 @@ const UserProfile = () => {
return (
<>
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<section className="relative top-0 w-full min-h-full overflow-x-hidden">
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center">
{!loadingState.isLoading &&
(user ? (

View file

@ -12,7 +12,6 @@ const Explore = lazy(() => import("@/pages/Explore"));
const Home = lazy(() => import("@/pages/Home"));
const UserProfile = lazy(() => import("@/pages/UserProfile"));
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
const EmbedMemo = lazy(() => import("@/pages/EmbedMemo"));
const Archived = lazy(() => import("@/pages/Archived"));
const DailyReview = lazy(() => import("@/pages/DailyReview"));
const Resources = lazy(() => import("@/pages/Resources"));
@ -103,24 +102,20 @@ const router = createBrowserRouter([
path: "explore",
element: <Explore />,
},
{
path: "m/:memoId",
element: <MemoDetail />,
},
{
path: "u/:username",
element: <UserProfile />,
},
{
path: "*",
element: <NotFound />,
},
],
},
{
path: "/m/:memoId",
element: <MemoDetail />,
},
{
path: "/m/:memoId/embed",
element: <EmbedMemo />,
},
{
path: "/u/:username",
element: <UserProfile />,
},
{
path: "*",
element: <NotFound />,
},
],
},
]);