refactor: replace useResponsiveWidth with useMediaQuery across components

This commit is contained in:
Johnny 2025-12-26 23:44:52 +08:00
parent d21610cce1
commit 0ad75b8f08
12 changed files with 111 additions and 112 deletions

View File

@ -55,7 +55,6 @@
"react-leaflet": "^4.2.1",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.9.6",
"react-simple-pull-to-refresh": "^1.3.3",
"react-use": "^17.6.0",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",

View File

@ -1,5 +1,5 @@
import useWindowScroll from "react-use/lib/useWindowScroll";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import useMediaQuery from "@/hooks/useMediaQuery";
import { cn } from "@/lib/utils";
import NavigationDrawer from "./NavigationDrawer";
@ -10,19 +10,22 @@ interface Props {
const MobileHeader = (props: Props) => {
const { className, children } = props;
const { sm } = useResponsiveWidth();
const { y: offsetTop } = useWindowScroll();
const md = useMediaQuery("md");
const sm = useMediaQuery("sm");
if (md) return null;
return (
<div
className={cn(
"sticky top-0 pt-3 pb-2 sm:pt-2 px-4 sm:px-6 sm:mb-1 bg-background bg-opacity-80 backdrop-blur-lg flex md:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-1",
"sticky top-0 pt-3 pb-2 sm:pt-2 px-4 sm:px-6 sm:mb-1 bg-background bg-opacity-80 backdrop-blur-lg flex flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-1",
offsetTop > 0 && "shadow-md",
className,
)}
>
<div className="flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden">{!sm && <NavigationDrawer />}</div>
<div className="flex flex-row justify-end items-center">{children}</div>
{!sm && <NavigationDrawer />}
<div className="w-full flex flex-row justify-end items-center">{children}</div>
</div>
);
};

View File

@ -1,13 +1,11 @@
import { ArrowUpIcon, LoaderIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { matchPath } from "react-router-dom";
import PullToRefresh from "react-simple-pull-to-refresh";
import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import { useView } from "@/contexts/ViewContext";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { Routes } from "@/router";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
@ -82,14 +80,13 @@ function useAutoFetchWhenNotScrollable({
const PagedMemoList = (props: Props) => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const { layout } = useView();
// Show memo editor only on the root route
const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname));
// Use React Query's infinite query for pagination
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteMemos({
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteMemos({
state: props.state || State.NORMAL,
orderBy: props.orderBy || "display_time desc",
filter: props.filter,
@ -192,29 +189,7 @@ const PagedMemoList = (props: Props) => {
</div>
);
if (md) {
return children;
}
return (
<PullToRefresh
onRefresh={async () => {
await refetch();
}}
pullingContent={
<div className="w-full flex flex-row justify-center items-center my-4">
<LoaderIcon className="opacity-60" />
</div>
}
refreshingContent={
<div className="w-full flex flex-row justify-center items-center my-4">
<LoaderIcon className="animate-spin" />
</div>
}
>
{children}
</PullToRefresh>
);
return children;
};
const BackToTop = () => {

View File

@ -3,9 +3,9 @@ export * from "./useCurrentUser";
export * from "./useDateFilterNavigation";
export * from "./useFilteredMemoStats";
export * from "./useLoading";
export * from "./useMediaQuery";
export * from "./useMemoFilters";
export * from "./useMemoSorting";
export * from "./useNavigateTo";
export * from "./useResponsiveWidth";
export * from "./useUserLocale";
export * from "./useUserTheme";

View File

@ -0,0 +1,35 @@
import { useEffect, useState } from "react";
type Breakpoint = "sm" | "md" | "lg";
const BREAKPOINTS: Record<Breakpoint, number> = {
sm: 640,
md: 768,
lg: 1024,
};
const useMediaQuery = (breakpoint: Breakpoint): boolean => {
const [matches, setMatches] = useState(() => {
if (typeof window === "undefined") return false;
return window.matchMedia(`(min-width: ${BREAKPOINTS[breakpoint]}px)`).matches;
});
useEffect(() => {
const mediaQuery = window.matchMedia(`(min-width: ${BREAKPOINTS[breakpoint]}px)`);
const handleChange = (e: MediaQueryListEvent) => {
setMatches(e.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, [breakpoint]);
return matches;
};
export default useMediaQuery;

View File

@ -1,18 +0,0 @@
import useWindowSize from "react-use/lib/useWindowSize";
enum TailwindResponsiveWidth {
sm = 640,
md = 768,
lg = 1024,
}
const useResponsiveWidth = () => {
const { width } = useWindowSize();
return {
sm: width >= TailwindResponsiveWidth.sm,
md: width >= TailwindResponsiveWidth.md,
lg: width >= TailwindResponsiveWidth.lg,
};
};
export default useResponsiveWidth;

View File

@ -6,12 +6,13 @@ import MobileHeader from "@/components/MobileHeader";
import { userServiceClient } from "@/connect";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useFilteredMemoStats } from "@/hooks/useFilteredMemoStats";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import useMediaQuery from "@/hooks/useMediaQuery";
import { cn } from "@/lib/utils";
import { Routes } from "@/router";
const MainLayout = () => {
const { md, lg } = useResponsiveWidth();
const md = useMediaQuery("md");
const lg = useMediaQuery("lg");
const location = useLocation();
const currentUser = useCurrentUser();
const [profileUserName, setProfileUserName] = useState<string | undefined>();

View File

@ -5,7 +5,7 @@ import Navigation from "@/components/Navigation";
import { useInstance } from "@/contexts/InstanceContext";
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import useMediaQuery from "@/hooks/useMediaQuery";
import { cn } from "@/lib/utils";
import Loading from "@/pages/Loading";
import { Routes } from "@/router";
@ -13,7 +13,7 @@ import { Routes } from "@/router";
const RootLayout = () => {
const location = useLocation();
const [searchParams] = useSearchParams();
const { sm } = useResponsiveWidth();
const sm = useMediaQuery("sm");
const currentUser = useCurrentUser();
const { memoRelatedSetting } = useInstance();
const { removeFilter } = useMemoFilterContext();

View File

@ -15,7 +15,7 @@ import { attachmentServiceClient } from "@/connect";
import { useDeleteAttachment } from "@/hooks/useAttachmentQueries";
import useDialog from "@/hooks/useDialog";
import useLoading from "@/hooks/useLoading";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import useMediaQuery from "@/hooks/useMediaQuery";
import i18n from "@/i18n";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { useTranslate } from "@/utils/i18n";
@ -69,7 +69,7 @@ const AttachmentItem = ({ attachment }: AttachmentItemProps) => (
const Attachments = () => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const md = useMediaQuery("md");
const loadingState = useLoading();
const deleteUnusedAttachmentsDialog = useDialog();
const { mutateAsync: deleteAttachment } = useDeleteAttachment();

View File

@ -5,7 +5,7 @@ import { useState } from "react";
import Empty from "@/components/Empty";
import MemoCommentMessage from "@/components/Inbox/MemoCommentMessage";
import MobileHeader from "@/components/MobileHeader";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import useMediaQuery from "@/hooks/useMediaQuery";
import { useNotifications } from "@/hooks/useUserQueries";
import { cn } from "@/lib/utils";
import { UserNotification, UserNotification_Status, UserNotification_Type } from "@/types/proto/api/v1/user_service_pb";
@ -13,7 +13,7 @@ import { useTranslate } from "@/utils/i18n";
const Inboxes = () => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const md = useMediaQuery("md");
const [filter, setFilter] = useState<"all" | "unread" | "archived">("all");
// Fetch notifications with React Query

View File

@ -10,15 +10,15 @@ import MobileHeader from "@/components/MobileHeader";
import { Button } from "@/components/ui/button";
import { memoNamePrefix } from "@/helpers/resource-names";
import useCurrentUser from "@/hooks/useCurrentUser";
import useMediaQuery from "@/hooks/useMediaQuery";
import { useMemo, useMemoComments } from "@/hooks/useMemoQueries";
import useNavigateTo from "@/hooks/useNavigateTo";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
const MemoDetail = () => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const md = useMediaQuery("md");
const params = useParams();
const navigateTo = useNavigateTo();
const { state: locationState } = useLocation();
@ -70,7 +70,7 @@ const MemoDetail = () => {
</MobileHeader>
)}
<div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
<div className={cn(md ? "w-[calc(100%-15rem)]" : "w-full")}>
<div className={cn("w-full md:w-[calc(100%-15rem)]")}>
{parentMemo && (
<div className="w-auto inline-block mb-2">
<Link

View File

@ -13,7 +13,7 @@ import StorageSection from "@/components/Settings/StorageSection";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useInstance } from "@/contexts/InstanceContext";
import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import useMediaQuery from "@/hooks/useMediaQuery";
import { InstanceSetting_Key } from "@/types/proto/api/v1/instance_service_pb";
import { User_Role } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
@ -38,7 +38,7 @@ const SECTION_ICON_MAP: Record<SettingSection, LucideIcon> = {
const Setting = () => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const sm = useMediaQuery("sm");
const location = useLocation();
const user = useCurrentUser();
const { profile, fetchSetting } = useInstance();
@ -85,57 +85,61 @@ const Setting = () => {
return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8">
{!md && <MobileHeader />}
{!sm && <MobileHeader />}
<div className="w-full px-4 sm:px-6">
<div className="w-full border border-border flex flex-row justify-start items-start px-4 py-3 rounded-xl bg-background text-muted-foreground">
<div className="hidden sm:flex flex-col justify-start items-start w-40 h-auto shrink-0 py-2">
<span className="text-sm mt-0.5 pl-3 font-mono select-none text-muted-foreground">{t("common.basic")}</span>
<div className="w-full flex flex-col justify-start items-start mt-1">
{BASIC_SECTIONS.map((item) => (
<SectionMenuItem
key={item}
text={t(`setting.${item}`)}
icon={SECTION_ICON_MAP[item]}
isSelected={state.selectedSection === item}
onClick={() => handleSectionSelectorItemClick(item)}
/>
))}
{sm && (
<div className="flex flex-col justify-start items-start w-40 h-auto shrink-0 py-2">
<span className="text-sm mt-0.5 pl-3 font-mono select-none text-muted-foreground">{t("common.basic")}</span>
<div className="w-full flex flex-col justify-start items-start mt-1">
{BASIC_SECTIONS.map((item) => (
<SectionMenuItem
key={item}
text={t(`setting.${item}`)}
icon={SECTION_ICON_MAP[item]}
isSelected={state.selectedSection === item}
onClick={() => handleSectionSelectorItemClick(item)}
/>
))}
</div>
{isHost ? (
<>
<span className="text-sm mt-4 pl-3 font-mono select-none text-muted-foreground">{t("common.admin")}</span>
<div className="w-full flex flex-col justify-start items-start mt-1">
{ADMIN_SECTIONS.map((item) => (
<SectionMenuItem
key={item}
text={t(`setting.${item}`)}
icon={SECTION_ICON_MAP[item]}
isSelected={state.selectedSection === item}
onClick={() => handleSectionSelectorItemClick(item)}
/>
))}
<span className="px-3 mt-2 opacity-70 text-sm">
{t("setting.version")}: v{profile.version}
</span>
</div>
</>
) : null}
</div>
{isHost ? (
<>
<span className="text-sm mt-4 pl-3 font-mono select-none text-muted-foreground">{t("common.admin")}</span>
<div className="w-full flex flex-col justify-start items-start mt-1">
{ADMIN_SECTIONS.map((item) => (
<SectionMenuItem
key={item}
text={t(`setting.${item}`)}
icon={SECTION_ICON_MAP[item]}
isSelected={state.selectedSection === item}
onClick={() => handleSectionSelectorItemClick(item)}
/>
))}
<span className="px-3 mt-2 opacity-70 text-sm">
{t("setting.version")}: v{profile.version}
</span>
</div>
</>
) : null}
</div>
)}
<div className="w-full grow sm:pl-4 overflow-x-auto">
<div className="w-auto inline-block my-2 sm:hidden">
<Select value={state.selectedSection} onValueChange={(value) => handleSectionSelectorItemClick(value as SettingSection)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select section" />
</SelectTrigger>
<SelectContent>
{settingsSectionList.map((settingSection) => (
<SelectItem key={settingSection} value={settingSection}>
{t(`setting.${settingSection}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{!sm && (
<div className="w-auto inline-block my-2">
<Select value={state.selectedSection} onValueChange={(value) => handleSectionSelectorItemClick(value as SettingSection)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select section" />
</SelectTrigger>
<SelectContent>
{settingsSectionList.map((settingSection) => (
<SelectItem key={settingSection} value={settingSection}>
{t(`setting.${settingSection}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{state.selectedSection === "my-account" ? (
<MyAccountSection />
) : state.selectedSection === "preference" ? (