diff options
Diffstat (limited to 'apps')
96 files changed, 1845 insertions, 1405 deletions
diff --git a/apps/browser-extension/package.json b/apps/browser-extension/package.json index d2a699c3..575bb8b7 100644 --- a/apps/browser-extension/package.json +++ b/apps/browser-extension/package.json @@ -24,9 +24,9 @@ "@tanstack/query-async-storage-persister": "5.90.2", "@tanstack/react-query": "5.90.2", "@tanstack/react-query-persist-client": "5.90.2", - "@trpc/client": "^11.4.3", - "@trpc/react-query": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", + "@trpc/tanstack-react-query": "^11.9.0", "@uidotdev/usehooks": "^2.4.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/apps/browser-extension/src/OptionsPage.tsx b/apps/browser-extension/src/OptionsPage.tsx index cac32eff..1b1dc8b6 100644 --- a/apps/browser-extension/src/OptionsPage.tsx +++ b/apps/browser-extension/src/OptionsPage.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Button } from "./components/ui/button"; @@ -17,27 +18,32 @@ import usePluginSettings, { DEFAULT_BADGE_CACHE_EXPIRE_MS, } from "./utils/settings"; import { useTheme } from "./utils/ThemeProvider"; -import { api } from "./utils/trpc"; +import { useTRPC } from "./utils/trpc"; export default function OptionsPage() { + const api = useTRPC(); + const queryClient = useQueryClient(); const navigate = useNavigate(); const { settings, setSettings } = usePluginSettings(); const { setTheme, theme } = useTheme(); - const { data: whoami, error: whoAmIError } = api.users.whoami.useQuery( - undefined, - { + const { data: whoami, error: whoAmIError } = useQuery( + api.users.whoami.queryOptions(undefined, { enabled: settings.address != "", - }, + }), ); - const { mutate: deleteKey } = api.apiKeys.revoke.useMutation(); + const { mutate: deleteKey } = useMutation( + api.apiKeys.revoke.mutationOptions(), + ); - const invalidateWhoami = api.useUtils().users.whoami.refetch; + const invalidateWhoami = () => { + queryClient.refetchQueries(api.users.whoami.queryFilter()); + }; useEffect(() => { invalidateWhoami(); - }, [settings, invalidateWhoami]); + }, [settings]); let loggedInMessage: React.ReactNode; if (whoAmIError) { diff --git a/apps/browser-extension/src/SavePage.tsx b/apps/browser-extension/src/SavePage.tsx index b4b9ce95..5f55e164 100644 --- a/apps/browser-extension/src/SavePage.tsx +++ b/apps/browser-extension/src/SavePage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; import { Navigate } from "react-router-dom"; import { @@ -9,33 +10,36 @@ import { import { NEW_BOOKMARK_REQUEST_KEY_NAME } from "./background/protocol"; import Spinner from "./Spinner"; -import { api } from "./utils/trpc"; +import { useTRPC } from "./utils/trpc"; import { MessageType } from "./utils/type"; import { isHttpUrl } from "./utils/url"; export default function SavePage() { + const api = useTRPC(); const [error, setError] = useState<string | undefined>(undefined); const { data, mutate: createBookmark, status, - } = api.bookmarks.createBookmark.useMutation({ - onError: (e) => { - setError("Something went wrong: " + e.message); - }, - onSuccess: async () => { - // After successful creation, update badge cache and notify background - const [currentTab] = await chrome.tabs.query({ - active: true, - lastFocusedWindow: true, - }); - await chrome.runtime.sendMessage({ - type: MessageType.BOOKMARK_REFRESH_BADGE, - currentTab: currentTab, - }); - }, - }); + } = useMutation( + api.bookmarks.createBookmark.mutationOptions({ + onError: (e) => { + setError("Something went wrong: " + e.message); + }, + onSuccess: async () => { + // After successful creation, update badge cache and notify background + const [currentTab] = await chrome.tabs.query({ + active: true, + lastFocusedWindow: true, + }); + await chrome.runtime.sendMessage({ + type: MessageType.BOOKMARK_REFRESH_BADGE, + currentTab: currentTab, + }); + }, + }), + ); useEffect(() => { async function getNewBookmarkRequestFromBackgroundScriptIfAny(): Promise<ZNewBookmarkRequest | null> { const { [NEW_BOOKMARK_REQUEST_KEY_NAME]: req } = diff --git a/apps/browser-extension/src/SignInPage.tsx b/apps/browser-extension/src/SignInPage.tsx index 6cf8b35d..8a7229b6 100644 --- a/apps/browser-extension/src/SignInPage.tsx +++ b/apps/browser-extension/src/SignInPage.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; import { useNavigate } from "react-router-dom"; import { Button } from "./components/ui/button"; import { Input } from "./components/ui/input"; import Logo from "./Logo"; import usePluginSettings from "./utils/settings"; -import { api } from "./utils/trpc"; +import { useTRPC } from "./utils/trpc"; const enum LoginState { NONE = "NONE", @@ -14,6 +15,7 @@ const enum LoginState { } export default function SignInPage() { + const api = useTRPC(); const navigate = useNavigate(); const { settings, setSettings } = usePluginSettings(); @@ -21,23 +23,27 @@ export default function SignInPage() { mutate: login, error: usernamePasswordError, isPending: userNamePasswordRequestIsPending, - } = api.apiKeys.exchange.useMutation({ - onSuccess: (resp) => { - setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id })); - navigate("/options"); - }, - }); + } = useMutation( + api.apiKeys.exchange.mutationOptions({ + onSuccess: (resp) => { + setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id })); + navigate("/options"); + }, + }), + ); const { mutate: validateApiKey, error: apiKeyValidationError, isPending: apiKeyValueRequestIsPending, - } = api.apiKeys.validate.useMutation({ - onSuccess: () => { - setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey })); - navigate("/options"); - }, - }); + } = useMutation( + api.apiKeys.validate.mutationOptions({ + onSuccess: () => { + setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey })); + navigate("/options"); + }, + }), + ); const [lastLoginAttemptSource, setLastLoginAttemptSource] = useState<LoginState>(LoginState.NONE); diff --git a/apps/browser-extension/src/components/BookmarkLists.tsx b/apps/browser-extension/src/components/BookmarkLists.tsx index 1d70d257..8debef5c 100644 --- a/apps/browser-extension/src/components/BookmarkLists.tsx +++ b/apps/browser-extension/src/components/BookmarkLists.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@tanstack/react-query"; import { X } from "lucide-react"; import { @@ -5,15 +6,18 @@ import { useRemoveBookmarkFromList, } from "@karakeep/shared-react/hooks/lists"; -import { api } from "../utils/trpc"; +import { useTRPC } from "../utils/trpc"; import { Button } from "./ui/button"; export default function BookmarkLists({ bookmarkId }: { bookmarkId: string }) { + const api = useTRPC(); const { data: allLists } = useBookmarkLists(); const { mutate: deleteFromList } = useRemoveBookmarkFromList(); - const { data: lists } = api.lists.getListsOfBookmark.useQuery({ bookmarkId }); + const { data: lists } = useQuery( + api.lists.getListsOfBookmark.queryOptions({ bookmarkId }), + ); if (!lists || !allLists) { return null; } diff --git a/apps/browser-extension/src/components/ListsSelector.tsx b/apps/browser-extension/src/components/ListsSelector.tsx index 86c151d1..b27e866a 100644 --- a/apps/browser-extension/src/components/ListsSelector.tsx +++ b/apps/browser-extension/src/components/ListsSelector.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; import { useSet } from "@uidotdev/usehooks"; import { Check, ChevronsUpDown } from "lucide-react"; @@ -9,7 +10,7 @@ import { } from "@karakeep/shared-react/hooks/lists"; import { cn } from "../utils/css"; -import { api } from "../utils/trpc"; +import { useTRPC } from "../utils/trpc"; import { Button } from "./ui/button"; import { Command, @@ -23,14 +24,17 @@ import { DynamicPopoverContent } from "./ui/dynamic-popover"; import { Popover, PopoverTrigger } from "./ui/popover"; export function ListsSelector({ bookmarkId }: { bookmarkId: string }) { + const api = useTRPC(); const currentlyUpdating = useSet<string>(); const [open, setOpen] = React.useState(false); const { mutate: addToList } = useAddBookmarkToList(); const { mutate: removeFromList } = useRemoveBookmarkFromList(); - const { data: existingLists } = api.lists.getListsOfBookmark.useQuery({ - bookmarkId, - }); + const { data: existingLists } = useQuery( + api.lists.getListsOfBookmark.queryOptions({ + bookmarkId, + }), + ); const { data: allLists } = useBookmarkLists(); diff --git a/apps/browser-extension/src/components/TagsSelector.tsx b/apps/browser-extension/src/components/TagsSelector.tsx index ce404ac8..30cdcafc 100644 --- a/apps/browser-extension/src/components/TagsSelector.tsx +++ b/apps/browser-extension/src/components/TagsSelector.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; import { useSet } from "@uidotdev/usehooks"; import { Check, ChevronsUpDown, Plus } from "lucide-react"; @@ -8,7 +9,7 @@ import { } from "@karakeep/shared-react/hooks/bookmarks"; import { cn } from "../utils/css"; -import { api } from "../utils/trpc"; +import { useTRPC } from "../utils/trpc"; import { Button } from "./ui/button"; import { Command, @@ -22,7 +23,8 @@ import { DynamicPopoverContent } from "./ui/dynamic-popover"; import { Popover, PopoverTrigger } from "./ui/popover"; export function TagsSelector({ bookmarkId }: { bookmarkId: string }) { - const { data: allTags } = api.tags.list.useQuery({}); + const api = useTRPC(); + const { data: allTags } = useQuery(api.tags.list.queryOptions({})); const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId }); const existingTagIds = new Set(bookmark?.tags.map((t) => t.id) ?? []); diff --git a/apps/browser-extension/src/utils/providers.tsx b/apps/browser-extension/src/utils/providers.tsx index 86489d6d..4c09084d 100644 --- a/apps/browser-extension/src/utils/providers.tsx +++ b/apps/browser-extension/src/utils/providers.tsx @@ -1,4 +1,4 @@ -import { TRPCProvider } from "@karakeep/shared-react/providers/trpc-provider"; +import { TRPCSettingsProvider } from "@karakeep/shared-react/providers/trpc-provider"; import usePluginSettings from "./settings"; import { ThemeProvider } from "./ThemeProvider"; @@ -7,8 +7,8 @@ export function Providers({ children }: { children: React.ReactNode }) { const { settings } = usePluginSettings(); return ( - <TRPCProvider settings={settings}> + <TRPCSettingsProvider settings={settings}> <ThemeProvider>{children}</ThemeProvider> - </TRPCProvider> + </TRPCSettingsProvider> ); } diff --git a/apps/browser-extension/src/utils/trpc.ts b/apps/browser-extension/src/utils/trpc.ts index b3215d9d..73fe68c5 100644 --- a/apps/browser-extension/src/utils/trpc.ts +++ b/apps/browser-extension/src/utils/trpc.ts @@ -1,7 +1,7 @@ import { QueryClient } from "@tanstack/react-query"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import { createTRPCReact } from "@trpc/react-query"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; import superjson from "superjson"; import type { AppRouter } from "@karakeep/trpc/routers/_app"; @@ -9,7 +9,7 @@ import type { AppRouter } from "@karakeep/trpc/routers/_app"; import { getPluginSettings } from "./settings"; import { createChromeStorage } from "./storagePersister"; -export const api = createTRPCReact<AppRouter>(); +export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>(); let apiClient: ReturnType<typeof createTRPCClient<AppRouter>> | null = null; let queryClient: QueryClient | null = null; diff --git a/apps/cli/package.json b/apps/cli/package.json index ca085997..04c72f81 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -20,8 +20,8 @@ "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", "@tsconfig/node22": "^22.0.0", "chalk": "^5.3.0", "commander": "^12.0.0", diff --git a/apps/mobile/app/dashboard/(tabs)/highlights.tsx b/apps/mobile/app/dashboard/(tabs)/highlights.tsx index 7879081b..8a0a8ae3 100644 --- a/apps/mobile/app/dashboard/(tabs)/highlights.tsx +++ b/apps/mobile/app/dashboard/(tabs)/highlights.tsx @@ -4,11 +4,13 @@ import HighlightList from "@/components/highlights/HighlightList"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; export default function Highlights() { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const { data, isPending, @@ -17,12 +19,14 @@ export default function Highlights() { fetchNextPage, isFetchingNextPage, refetch, - } = api.highlights.getAll.useInfiniteQuery( - {}, - { - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.highlights.getAll.infiniteQueryOptions( + {}, + { + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); if (error) { @@ -34,7 +38,7 @@ export default function Highlights() { } const onRefresh = () => { - apiUtils.highlights.getAll.invalidate(); + queryClient.invalidateQueries(api.highlights.getAll.pathFilter()); }; return ( diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/lists.tsx index 45c23a28..5719c67c 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/lists.tsx @@ -8,9 +8,10 @@ import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useColorScheme } from "@/lib/useColorScheme"; import { condProps } from "@/lib/utils"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus } from "lucide-react-native"; import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; @@ -84,8 +85,9 @@ export default function Lists() { const [showChildrenOf, setShowChildrenOf] = useState<Record<string, boolean>>( {}, ); - const apiUtils = api.useUtils(); - const { data: listStats } = api.lists.stats.useQuery(); + const api = useTRPC(); + const queryClient = useQueryClient(); + const { data: listStats } = useQuery(api.lists.stats.queryOptions()); // Check if there are any shared lists const hasSharedLists = useMemo(() => { @@ -116,8 +118,8 @@ export default function Lists() { } const onRefresh = () => { - apiUtils.lists.list.invalidate(); - apiUtils.lists.stats.invalidate(); + queryClient.invalidateQueries(api.lists.list.pathFilter()); + queryClient.invalidateQueries(api.lists.stats.pathFilter()); }; const links: ListLink[] = [ diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index 106baec5..2610aa37 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -13,7 +13,8 @@ import { Text } from "@/components/ui/Text"; import { useServerVersion } from "@/lib/hooks"; import { useSession } from "@/lib/session"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; export default function Dashboard() { const { logout } = useSession(); @@ -22,6 +23,7 @@ export default function Dashboard() { setSettings, isLoading: isSettingsLoading, } = useAppSettings(); + const api = useTRPC(); const imageQuality = useSharedValue(0); const imageQualityMin = useSharedValue(0); @@ -31,7 +33,7 @@ export default function Dashboard() { imageQuality.value = settings.imageQuality * 100; }, [settings]); - const { data, error } = api.users.whoami.useQuery(); + const { data, error } = useQuery(api.users.whoami.queryOptions()); const { data: serverVersion, isLoading: isServerVersionLoading, diff --git a/apps/mobile/app/dashboard/(tabs)/tags.tsx b/apps/mobile/app/dashboard/(tabs)/tags.tsx index c0ac2d49..470ff3f3 100644 --- a/apps/mobile/app/dashboard/(tabs)/tags.tsx +++ b/apps/mobile/app/dashboard/(tabs)/tags.tsx @@ -8,7 +8,8 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; import { SearchInput } from "@/components/ui/SearchInput"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQueryClient } from "@tanstack/react-query"; import { usePaginatedSearchTags } from "@karakeep/shared-react/hooks/tags"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; @@ -23,7 +24,8 @@ interface TagItem { export default function Tags() { const [refreshing, setRefreshing] = useState(false); const [searchQuery, setSearchQuery] = useState(""); - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); // Debounce search query to avoid too many API calls const debouncedSearch = useDebounce(searchQuery, 300); @@ -56,7 +58,7 @@ export default function Tags() { } const onRefresh = () => { - apiUtils.tags.list.invalidate(); + queryClient.invalidateQueries(api.tags.list.pathFilter()); }; const tags: TagItem[] = data.tags.map((tag) => ({ diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index 8fd04115..567ac605 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -12,7 +12,8 @@ import BottomActions from "@/components/bookmarks/BottomActions"; import FullPageError from "@/components/FullPageError"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Settings } from "lucide-react-native"; import { useColorScheme } from "nativewind"; @@ -25,6 +26,7 @@ export default function BookmarkView() { const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const { settings } = useAppSettings(); + const api = useTRPC(); const [bookmarkLinkType, setBookmarkLinkType] = useState<BookmarkLinkType>( settings.defaultBookmarkView, @@ -38,10 +40,12 @@ export default function BookmarkView() { data: bookmark, error, refetch, - } = api.bookmarks.getBookmark.useQuery({ - bookmarkId: slug, - includeContent: false, - }); + } = useQuery( + api.bookmarks.getBookmark.queryOptions({ + bookmarkId: slug, + includeContent: false, + }), + ); if (error) { return <FullPageError error={error.message} onRetry={refetch} />; diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx index 8402bb0b..1070207b 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx @@ -5,15 +5,18 @@ import { useLocalSearchParams } from "expo-router"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; +import { useQuery } from "@tanstack/react-query"; +import type { ZBookmarkList } from "@karakeep/shared/types/lists"; import { useAddBookmarkToList, useBookmarkLists, useRemoveBookmarkFromList, } from "@karakeep/shared-react/hooks/lists"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; const ListPickerPage = () => { + const api = useTRPC(); const { slug: bookmarkId } = useLocalSearchParams(); if (typeof bookmarkId !== "string") { throw new Error("Unexpected param type"); @@ -26,13 +29,16 @@ const ListPickerPage = () => { showProgress: false, }); }; - const { data: existingLists } = api.lists.getListsOfBookmark.useQuery( - { - bookmarkId, - }, - { - select: (data) => new Set(data.lists.map((l) => l.id)), - }, + const { data: existingLists } = useQuery( + api.lists.getListsOfBookmark.queryOptions( + { + bookmarkId, + }, + { + select: (data: { lists: ZBookmarkList[] }) => + new Set(data.lists.map((l) => l.id)), + }, + ), ); const { data } = useBookmarkLists(); diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx index 984bc224..64d057f2 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx @@ -6,17 +6,19 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { useColorScheme } from "@/lib/useColorScheme"; +import { useQuery } from "@tanstack/react-query"; import { Check, Plus } from "lucide-react-native"; import { useAutoRefreshingBookmarkQuery, useUpdateBookmarkTags, } from "@karakeep/shared-react/hooks/bookmarks"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; const NEW_TAG_ID = "new-tag"; const ListPickerPage = () => { + const api = useTRPC(); const { colors } = useColorScheme(); const { slug: bookmarkId } = useLocalSearchParams(); @@ -34,22 +36,24 @@ const ListPickerPage = () => { }); }; - const { data: allTags, isPending: isAllTagsPending } = api.tags.list.useQuery( - {}, - { - select: React.useCallback( - (data: { tags: { id: string; name: string }[] }) => { - return data.tags - .map((t) => ({ - id: t.id, - name: t.name, - lowered: t.name.toLowerCase(), - })) - .sort((a, b) => a.lowered.localeCompare(b.lowered)); - }, - [], - ), - }, + const { data: allTags, isPending: isAllTagsPending } = useQuery( + api.tags.list.queryOptions( + {}, + { + select: React.useCallback( + (data: { tags: { id: string; name: string }[] }) => { + return data.tags + .map((t) => ({ + id: t.id, + name: t.name, + lowered: t.name.toLowerCase(), + })) + .sort((a, b) => a.lowered.localeCompare(b.lowered)); + }, + [], + ), + }, + ), ); const { data: existingTags } = useAutoRefreshingBookmarkQuery({ bookmarkId, diff --git a/apps/mobile/app/dashboard/lists/[slug]/edit.tsx b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx index 6ccc2f26..e0654722 100644 --- a/apps/mobile/app/dashboard/lists/[slug]/edit.tsx +++ b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx @@ -7,7 +7,8 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { useEditBookmarkList } from "@karakeep/shared-react/hooks/lists"; @@ -16,6 +17,7 @@ const EditListPage = () => { const [text, setText] = useState(""); const [query, setQuery] = useState(""); const { toast } = useToast(); + const api = useTRPC(); const { mutate, isPending: editIsPending } = useEditBookmarkList({ onSuccess: () => { dismiss(); @@ -41,9 +43,11 @@ const EditListPage = () => { throw new Error("Unexpected param type"); } - const { data: list, isLoading: fetchIsPending } = api.lists.get.useQuery({ - listId, - }); + const { data: list, isLoading: fetchIsPending } = useQuery( + api.lists.get.queryOptions({ + listId, + }), + ); const dismiss = () => { router.back(); diff --git a/apps/mobile/app/dashboard/lists/[slug]/index.tsx b/apps/mobile/app/dashboard/lists/[slug]/index.tsx index 11379588..97f797c6 100644 --- a/apps/mobile/app/dashboard/lists/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/lists/[slug]/index.tsx @@ -5,14 +5,16 @@ import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import FullPageError from "@/components/FullPageError"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { MenuView } from "@react-native-menu/menu"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { Ellipsis } from "lucide-react-native"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; export default function ListView() { const { slug } = useLocalSearchParams(); + const api = useTRPC(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } @@ -20,7 +22,7 @@ export default function ListView() { data: list, error, refetch, - } = api.lists.get.useQuery({ listId: slug }); + } = useQuery(api.lists.get.queryOptions({ listId: slug })); return ( <CustomSafeAreaView> @@ -58,17 +60,22 @@ function ListActionsMenu({ listId: string; role: ZBookmarkList["userRole"]; }) { - const { mutate: deleteList } = api.lists.delete.useMutation({ - onSuccess: () => { - router.replace("/dashboard/lists"); - }, - }); + const api = useTRPC(); + const { mutate: deleteList } = useMutation( + api.lists.delete.mutationOptions({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }), + ); - const { mutate: leaveList } = api.lists.leaveList.useMutation({ - onSuccess: () => { - router.replace("/dashboard/lists"); - }, - }); + const { mutate: leaveList } = useMutation( + api.lists.leaveList.mutationOptions({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }), + ); const handleDelete = () => { Alert.alert("Delete List", "Are you sure you want to delete this list?", [ diff --git a/apps/mobile/app/dashboard/search.tsx b/apps/mobile/app/dashboard/search.tsx index ab89ce8d..0e59c607 100644 --- a/apps/mobile/app/dashboard/search.tsx +++ b/apps/mobile/app/dashboard/search.tsx @@ -7,9 +7,13 @@ import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { SearchInput } from "@/components/ui/SearchInput"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import AsyncStorage from "@react-native-async-storage/async-storage"; -import { keepPreviousData } from "@tanstack/react-query"; +import { + keepPreviousData, + useInfiniteQuery, + useQueryClient, +} from "@tanstack/react-query"; import { useSearchHistory } from "@karakeep/shared-react/hooks/search-history"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; @@ -29,7 +33,12 @@ export default function Search() { removeItem: (k: string) => AsyncStorage.removeItem(k), }); - const onRefresh = api.useUtils().bookmarks.searchBookmarks.invalidate; + const api = useTRPC(); + const queryClient = useQueryClient(); + + const onRefresh = () => { + queryClient.invalidateQueries(api.bookmarks.searchBookmarks.pathFilter()); + }; const { data, @@ -39,14 +48,16 @@ export default function Search() { isFetching, fetchNextPage, isFetchingNextPage, - } = api.bookmarks.searchBookmarks.useInfiniteQuery( - { text: query }, - { - placeholderData: keepPreviousData, - gcTime: 0, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.bookmarks.searchBookmarks.infiniteQueryOptions( + { text: query }, + { + placeholderData: keepPreviousData, + gcTime: 0, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); const filteredHistory = useMemo(() => { diff --git a/apps/mobile/app/dashboard/tags/[slug].tsx b/apps/mobile/app/dashboard/tags/[slug].tsx index 3f294328..170cb04d 100644 --- a/apps/mobile/app/dashboard/tags/[slug].tsx +++ b/apps/mobile/app/dashboard/tags/[slug].tsx @@ -4,15 +4,21 @@ import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import FullPageError from "@/components/FullPageError"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; export default function TagView() { const { slug } = useLocalSearchParams(); + const api = useTRPC(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } - const { data: tag, error, refetch } = api.tags.get.useQuery({ tagId: slug }); + const { + data: tag, + error, + refetch, + } = useQuery(api.tags.get.queryOptions({ tagId: slug })); return ( <CustomSafeAreaView> diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 3e2b6bfb..6d9167db 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -5,8 +5,9 @@ import { useShareIntentContext } from "expo-share-intent"; import { Button } from "@/components/ui/Button"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useUploadAsset } from "@/lib/upload"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { z } from "zod"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -18,8 +19,11 @@ type Mode = | { type: "error" }; function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { + const api = useTRPC(); + const queryClient = useQueryClient(); + const onSaved = (d: ZBookmark & { alreadyExists: boolean }) => { - invalidateAllBookmarks(); + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); setMode({ type: d.alreadyExists ? "alreadyExists" : "success", bookmarkId: d.id, @@ -36,9 +40,6 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { }, }); - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - useEffect(() => { if (isLoading) { return; @@ -77,12 +78,14 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { } }, [isLoading]); - const { mutate, isPending } = api.bookmarks.createBookmark.useMutation({ - onSuccess: onSaved, - onError: () => { - setMode({ type: "error" }); - }, - }); + const { mutate, isPending } = useMutation( + api.bookmarks.createBookmark.mutationOptions({ + onSuccess: onSaved, + onError: () => { + setMode({ type: "error" }); + }, + }), + ); return ( <View className="flex flex-row gap-3"> diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 03cbba5a..5c6370ca 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -13,7 +13,8 @@ import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { Bug, Edit3 } from "lucide-react-native"; enum LoginType { @@ -24,6 +25,7 @@ enum LoginType { export default function Signin() { const { settings, setSettings } = useAppSettings(); const router = useRouter(); + const api = useTRPC(); const [error, setError] = useState<string | undefined>(); const [loginType, setLoginType] = useState<LoginType>(LoginType.Password); @@ -43,33 +45,37 @@ export default function Signin() { }; const { mutate: login, isPending: userNamePasswordRequestIsPending } = - api.apiKeys.exchange.useMutation({ - onSuccess: (resp) => { - setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id }); - }, - onError: (e) => { - if (e.data?.code === "UNAUTHORIZED") { - setError("Wrong username or password"); - } else { - setError(`${e.message}`); - } - }, - }); + useMutation( + api.apiKeys.exchange.mutationOptions({ + onSuccess: (resp) => { + setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id }); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Wrong username or password"); + } else { + setError(`${e.message}`); + } + }, + }), + ); const { mutate: validateApiKey, isPending: apiKeyValueRequestIsPending } = - api.apiKeys.validate.useMutation({ - onSuccess: () => { - const apiKey = apiKeyRef.current; - setSettings({ ...settings, apiKey: apiKey }); - }, - onError: (e) => { - if (e.data?.code === "UNAUTHORIZED") { - setError("Invalid API key"); - } else { - setError(`${e.message}`); - } - }, - }); + useMutation( + api.apiKeys.validate.mutationOptions({ + onSuccess: () => { + const apiKey = apiKeyRef.current; + setSettings({ ...settings, apiKey: apiKey }); + }, + onError: (e) => { + if (e.data?.code === "UNAUTHORIZED") { + setError("Invalid API key"); + } else { + setError(`${e.message}`); + } + }, + }), + ); if (settings.apiKey) { return <Redirect href="dashboard" />; diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 6c3ef070..38fed737 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -15,9 +15,10 @@ import { router, useRouter } from "expo-router"; import * as Sharing from "expo-sharing"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { buildApiHeaders } from "@/lib/utils"; import { MenuView } from "@react-native-menu/menu"; +import { useQuery } from "@tanstack/react-query"; import { Ellipsis, ShareIcon, Star } from "lucide-react-native"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -477,20 +478,23 @@ export default function BookmarkCard({ }: { bookmark: ZBookmark; }) { - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: initialData.id, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const api = useTRPC(); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId: initialData.id, + }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, }, - }, + ), ); const router = useRouter(); diff --git a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx index 4478bdda..aa073c69 100644 --- a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx +++ b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx @@ -6,8 +6,9 @@ import { WebViewSourceUri } from "react-native-webview/lib/WebViewTypes"; import { Text } from "@/components/ui/Text"; import { useAssetUrl } from "@/lib/hooks"; import { useReaderSettings, WEBVIEW_FONT_FAMILIES } from "@/lib/readerSettings"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useColorScheme } from "@/lib/useColorScheme"; +import { useQuery } from "@tanstack/react-query"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -65,16 +66,19 @@ export function BookmarkLinkReaderPreview({ }) { const { isDarkColorScheme: isDark } = useColorScheme(); const { settings: readerSettings } = useReaderSettings(); + const api = useTRPC(); const { data: bookmarkWithContent, error, isLoading, refetch, - } = api.bookmarks.getBookmark.useQuery({ - bookmarkId: bookmark.id, - includeContent: true, - }); + } = useQuery( + api.bookmarks.getBookmark.queryOptions({ + bookmarkId: bookmark.id, + includeContent: true, + }), + ); if (isLoading) { return <FullPageSpinner />; diff --git a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx index e627ee16..b5aeaa40 100644 --- a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx +++ b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx @@ -1,4 +1,5 @@ -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import type { ZGetBookmarksRequest } from "@karakeep/shared/types/bookmarks"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; @@ -14,7 +15,8 @@ export default function UpdatingBookmarkList({ query: Omit<ZGetBookmarksRequest, "sortOrder" | "includeContent">; // Sort order is not supported in mobile yet header?: React.ReactElement; }) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const { data, isPending, @@ -23,12 +25,14 @@ export default function UpdatingBookmarkList({ fetchNextPage, isFetchingNextPage, refetch, - } = api.bookmarks.getBookmarks.useInfiniteQuery( - { ...query, useCursorV2: true, includeContent: false }, - { - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.bookmarks.getBookmarks.infiniteQueryOptions( + { ...query, useCursorV2: true, includeContent: false }, + { + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); if (error) { @@ -40,8 +44,8 @@ export default function UpdatingBookmarkList({ } const onRefresh = () => { - apiUtils.bookmarks.getBookmarks.invalidate(); - apiUtils.bookmarks.getBookmark.invalidate(); + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); + queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter()); }; return ( diff --git a/apps/mobile/components/highlights/HighlightCard.tsx b/apps/mobile/components/highlights/HighlightCard.tsx index 7e0b4a2b..c8f97e32 100644 --- a/apps/mobile/components/highlights/HighlightCard.tsx +++ b/apps/mobile/components/highlights/HighlightCard.tsx @@ -2,7 +2,8 @@ import { ActivityIndicator, Alert, Pressable, View } from "react-native"; import * as Haptics from "expo-haptics"; import { useRouter } from "expo-router"; import { Text } from "@/components/ui/Text"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { ExternalLink, Trash2 } from "lucide-react-native"; @@ -29,6 +30,7 @@ export default function HighlightCard({ }) { const { toast } = useToast(); const router = useRouter(); + const api = useTRPC(); const onError = () => { toast({ @@ -64,13 +66,15 @@ export default function HighlightCard({ ], ); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: highlight.bookmarkId, - }, - { - retry: false, - }, + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId: highlight.bookmarkId, + }, + { + retry: false, + }, + ), ); const handleBookmarkPress = () => { diff --git a/apps/mobile/lib/providers.tsx b/apps/mobile/lib/providers.tsx index 01d2d5b5..4a7def1d 100644 --- a/apps/mobile/lib/providers.tsx +++ b/apps/mobile/lib/providers.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Toaster } from "sonner-native"; -import { TRPCProvider } from "@karakeep/shared-react/providers/trpc-provider"; +import { TRPCSettingsProvider } from "@karakeep/shared-react/providers/trpc-provider"; import { ReaderSettingsProvider } from "./readerSettings"; import useAppSettings from "./settings"; @@ -20,11 +20,11 @@ export function Providers({ children }: { children: React.ReactNode }) { } return ( - <TRPCProvider settings={settings}> + <TRPCSettingsProvider settings={settings}> <ReaderSettingsProvider> {children} <Toaster /> </ReaderSettingsProvider> - </TRPCProvider> + </TRPCSettingsProvider> ); } diff --git a/apps/mobile/lib/session.ts b/apps/mobile/lib/session.ts index 8eb646cb..9f539693 100644 --- a/apps/mobile/lib/session.ts +++ b/apps/mobile/lib/session.ts @@ -1,12 +1,16 @@ import { useCallback } from "react"; +import { useMutation } from "@tanstack/react-query"; import useAppSettings from "./settings"; -import { api } from "./trpc"; +import { useTRPC } from "./trpc"; export function useSession() { const { settings, setSettings } = useAppSettings(); + const api = useTRPC(); - const { mutate: deleteKey } = api.apiKeys.revoke.useMutation(); + const { mutate: deleteKey } = useMutation( + api.apiKeys.revoke.mutationOptions(), + ); const logout = useCallback(() => { if (settings.apiKeyId) { diff --git a/apps/mobile/lib/trpc.ts b/apps/mobile/lib/trpc.ts index e56968b8..915c265d 100644 --- a/apps/mobile/lib/trpc.ts +++ b/apps/mobile/lib/trpc.ts @@ -1,5 +1,3 @@ -import { createTRPCReact } from "@trpc/react-query"; - -import type { AppRouter } from "@karakeep/trpc/routers/_app"; - -export const api = createTRPCReact<AppRouter>(); +// Re-export from shared-react to ensure there's only one TRPCProvider context +// This is necessary because the hooks in shared-react use useTRPC from shared-react +export { TRPCProvider, useTRPC } from "@karakeep/shared-react/trpc"; diff --git a/apps/mobile/lib/upload.ts b/apps/mobile/lib/upload.ts index 06f007f7..13abae16 100644 --- a/apps/mobile/lib/upload.ts +++ b/apps/mobile/lib/upload.ts @@ -1,5 +1,5 @@ import ReactNativeBlobUtil from "react-native-blob-util"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { @@ -8,7 +8,7 @@ import { } from "@karakeep/shared/types/uploads"; import type { Settings } from "./settings"; -import { api } from "./trpc"; +import { useTRPC } from "./trpc"; import { buildApiHeaders } from "./utils"; export function useUploadAsset( @@ -18,13 +18,13 @@ export function useUploadAsset( onError?: (e: string) => void; }, ) { - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; + const api = useTRPC(); + const queryClient = useQueryClient(); - const { mutate: createBookmark, isPending: isCreatingBookmark } = - api.bookmarks.createBookmark.useMutation({ + const { mutate: createBookmark, isPending: isCreatingBookmark } = useMutation( + api.bookmarks.createBookmark.mutationOptions({ onSuccess: (d) => { - invalidateAllBookmarks(); + queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter()); if (options.onSuccess) { options.onSuccess(d); } @@ -34,7 +34,8 @@ export function useUploadAsset( options.onError(e.message); } }, - }); + }), + ); const { mutate: uploadAsset, isPending: isUploading } = useMutation({ mutationFn: async (file: { type: string; name: string; uri: string }) => { diff --git a/apps/web/app/check-email/page.tsx b/apps/web/app/check-email/page.tsx index 227e116c..9e6a37b8 100644 --- a/apps/web/app/check-email/page.tsx +++ b/apps/web/app/check-email/page.tsx @@ -11,26 +11,30 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { Loader2, Mail } from "lucide-react"; export default function CheckEmailPage() { + const api = useTRPC(); const searchParams = useSearchParams(); const router = useRouter(); const [message, setMessage] = useState(""); const email = searchParams.get("email"); - const resendEmailMutation = api.users.resendVerificationEmail.useMutation({ - onSuccess: () => { - setMessage( - "A new verification email has been sent to your email address.", - ); - }, - onError: (error) => { - setMessage(error.message || "Failed to resend verification email."); - }, - }); + const resendEmailMutation = useMutation( + api.users.resendVerificationEmail.mutationOptions({ + onSuccess: () => { + setMessage( + "A new verification email has been sent to your email address.", + ); + }, + onError: (error) => { + setMessage(error.message || "Failed to resend verification email."); + }, + }), + ); const handleResendEmail = () => { if (email) { diff --git a/apps/web/app/reader/[bookmarkId]/page.tsx b/apps/web/app/reader/[bookmarkId]/page.tsx index 3eba7c7a..0ba72016 100644 --- a/apps/web/app/reader/[bookmarkId]/page.tsx +++ b/apps/web/app/reader/[bookmarkId]/page.tsx @@ -10,22 +10,28 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { Separator } from "@/components/ui/separator"; import { useSession } from "@/lib/auth/client"; import { useReaderSettings } from "@/lib/readerSettings"; +import { useQuery } from "@tanstack/react-query"; import { HighlighterIcon as Highlight, Printer, X } from "lucide-react"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { READER_FONT_FAMILIES } from "@karakeep/shared/types/readers"; import { getBookmarkTitle } from "@karakeep/shared/utils/bookmarkUtils"; export default function ReaderViewPage() { + const api = useTRPC(); const params = useParams<{ bookmarkId: string }>(); const bookmarkId = params.bookmarkId; - const { data: highlights } = api.highlights.getForBookmark.useQuery({ - bookmarkId, - }); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery({ - bookmarkId, - }); + const { data: highlights } = useQuery( + api.highlights.getForBookmark.queryOptions({ + bookmarkId, + }), + ); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions({ + bookmarkId, + }), + ); const { data: session } = useSession(); const router = useRouter(); diff --git a/apps/web/app/settings/assets/page.tsx b/apps/web/app/settings/assets/page.tsx index a2d2c9ab..0991816c 100644 --- a/apps/web/app/settings/assets/page.tsx +++ b/apps/web/app/settings/assets/page.tsx @@ -16,8 +16,9 @@ import { } from "@/components/ui/table"; import { ASSET_TYPE_TO_ICON } from "@/lib/attachments"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { formatBytes } from "@/lib/utils"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { ExternalLink, Trash2 } from "lucide-react"; import { useDetachBookmarkAsset } from "@karakeep/shared-react/hooks/assets"; @@ -28,6 +29,7 @@ import { } from "@karakeep/trpc/lib/attachments"; export default function AssetsSettingsPage() { + const api = useTRPC(); const { t } = useTranslation(); const { mutate: detachAsset, isPending: isDetaching } = useDetachBookmarkAsset({ @@ -49,13 +51,15 @@ export default function AssetsSettingsPage() { fetchNextPage, hasNextPage, isFetchingNextPage, - } = api.assets.list.useInfiniteQuery( - { - limit: 20, - }, - { - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.assets.list.infiniteQueryOptions( + { + limit: 20, + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); const assets = data?.pages.flatMap((page) => page.assets) ?? []; diff --git a/apps/web/app/settings/broken-links/page.tsx b/apps/web/app/settings/broken-links/page.tsx index 139e8f91..4197d62e 100644 --- a/apps/web/app/settings/broken-links/page.tsx +++ b/apps/web/app/settings/broken-links/page.tsx @@ -11,6 +11,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { RefreshCw, Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; @@ -18,20 +19,23 @@ import { useDeleteBookmark, useRecrawlBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; export default function BrokenLinksPage() { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { data, isPending } = api.bookmarks.getBrokenLinks.useQuery(); + const queryClient = useQueryClient(); + const { data, isPending } = useQuery( + api.bookmarks.getBrokenLinks.queryOptions(), + ); const { mutate: deleteBookmark, isPending: isDeleting } = useDeleteBookmark({ onSuccess: () => { toast({ description: t("toasts.bookmarks.deleted"), }); - apiUtils.bookmarks.getBrokenLinks.invalidate(); + queryClient.invalidateQueries(api.bookmarks.getBrokenLinks.pathFilter()); }, onError: () => { toast({ @@ -47,7 +51,9 @@ export default function BrokenLinksPage() { toast({ description: t("toasts.bookmarks.refetch"), }); - apiUtils.bookmarks.getBrokenLinks.invalidate(); + queryClient.invalidateQueries( + api.bookmarks.getBrokenLinks.pathFilter(), + ); }, onError: () => { toast({ diff --git a/apps/web/app/settings/rules/page.tsx b/apps/web/app/settings/rules/page.tsx index 17f5b388..6d0b6522 100644 --- a/apps/web/app/settings/rules/page.tsx +++ b/apps/web/app/settings/rules/page.tsx @@ -6,21 +6,25 @@ import RuleList from "@/components/dashboard/rules/RuleEngineRuleList"; import { Button } from "@/components/ui/button"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { PlusCircle } from "lucide-react"; import { RuleEngineRule } from "@karakeep/shared/types/rules"; export default function RulesSettingsPage() { + const api = useTRPC(); const { t } = useTranslation(); const [editingRule, setEditingRule] = useState< (Omit<RuleEngineRule, "id"> & { id: string | null }) | null >(null); - const { data: rules, isLoading } = api.rules.list.useQuery(undefined, { - refetchOnWindowFocus: true, - refetchOnMount: true, - }); + const { data: rules, isLoading } = useQuery( + api.rules.list.queryOptions(undefined, { + refetchOnWindowFocus: true, + refetchOnMount: true, + }), + ); const handleCreateRule = () => { const newRule = { diff --git a/apps/web/app/settings/stats/page.tsx b/apps/web/app/settings/stats/page.tsx index 28c017f5..06076376 100644 --- a/apps/web/app/settings/stats/page.tsx +++ b/apps/web/app/settings/stats/page.tsx @@ -6,7 +6,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Skeleton } from "@/components/ui/skeleton"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Archive, BarChart3, @@ -159,9 +160,10 @@ function StatCard({ } export default function StatsPage() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: stats, isLoading } = api.users.stats.useQuery(); - const { data: userSettings } = api.users.settings.useQuery(); + const { data: stats, isLoading } = useQuery(api.users.stats.queryOptions()); + const { data: userSettings } = useQuery(api.users.settings.queryOptions()); const maxHourlyActivity = useMemo(() => { if (!stats) return 0; @@ -237,7 +239,6 @@ export default function StatsPage() { </p> </div> </div> - {/* Overview Stats */} <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <StatCard @@ -289,7 +290,6 @@ export default function StatsPage() { description={t("settings.stats.overview.bookmarks_added")} /> </div> - <div className="grid gap-6 md:grid-cols-2"> {/* Bookmark Types */} <Card> @@ -532,7 +532,6 @@ export default function StatsPage() { </CardContent> </Card> </div> - {/* Activity Patterns */} <div className="grid gap-6 md:grid-cols-2"> {/* Hourly Activity */} @@ -583,7 +582,6 @@ export default function StatsPage() { </CardContent> </Card> </div> - {/* Asset Storage */} {stats.assetsByType.length > 0 && ( <Card> diff --git a/apps/web/app/verify-email/page.tsx b/apps/web/app/verify-email/page.tsx index da9b8b6b..7da96761 100644 --- a/apps/web/app/verify-email/page.tsx +++ b/apps/web/app/verify-email/page.tsx @@ -11,10 +11,12 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { CheckCircle, Loader2, XCircle } from "lucide-react"; export default function VerifyEmailPage() { + const api = useTRPC(); const searchParams = useSearchParams(); const router = useRouter(); const [status, setStatus] = useState<"loading" | "success" | "error">( @@ -25,32 +27,36 @@ export default function VerifyEmailPage() { const token = searchParams.get("token"); const email = searchParams.get("email"); - const verifyEmailMutation = api.users.verifyEmail.useMutation({ - onSuccess: () => { - setStatus("success"); - setMessage( - "Your email has been successfully verified! You can now sign in.", - ); - }, - onError: (error) => { - setStatus("error"); - setMessage( - error.message || - "Failed to verify email. The link may be invalid or expired.", - ); - }, - }); + const verifyEmailMutation = useMutation( + api.users.verifyEmail.mutationOptions({ + onSuccess: () => { + setStatus("success"); + setMessage( + "Your email has been successfully verified! You can now sign in.", + ); + }, + onError: (error) => { + setStatus("error"); + setMessage( + error.message || + "Failed to verify email. The link may be invalid or expired.", + ); + }, + }), + ); - const resendEmailMutation = api.users.resendVerificationEmail.useMutation({ - onSuccess: () => { - setMessage( - "A new verification email has been sent to your email address.", - ); - }, - onError: (error) => { - setMessage(error.message || "Failed to resend verification email."); - }, - }); + const resendEmailMutation = useMutation( + api.users.resendVerificationEmail.mutationOptions({ + onSuccess: () => { + setMessage( + "A new verification email has been sent to your email address.", + ); + }, + onError: (error) => { + setMessage(error.message || "Failed to resend verification email."); + }, + }), + ); useEffect(() => { if (token && email) { diff --git a/apps/web/components/admin/AddUserDialog.tsx b/apps/web/components/admin/AddUserDialog.tsx index 3c578eca..2e29c6da 100644 --- a/apps/web/components/admin/AddUserDialog.tsx +++ b/apps/web/components/admin/AddUserDialog.tsx @@ -1,213 +1,217 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { toast } from "@/components/ui/sonner";
-import { api } from "@/lib/trpc";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { zAdminCreateUserSchema } from "@karakeep/shared/types/admin";
-
-type AdminCreateUserSchema = z.infer<typeof zAdminCreateUserSchema>;
-
-export default function AddUserDialog({
- children,
-}: {
- children?: React.ReactNode;
-}) {
- const apiUtils = api.useUtils();
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<AdminCreateUserSchema>({
- resolver: zodResolver(zAdminCreateUserSchema),
- defaultValues: {
- name: "",
- email: "",
- password: "",
- confirmPassword: "",
- role: "user",
- },
- });
- const { mutate, isPending } = api.admin.createUser.useMutation({
- onSuccess: () => {
- toast({
- description: "User created successfully",
- });
- onOpenChange(false);
- apiUtils.users.list.invalidate();
- apiUtils.admin.userStats.invalidate();
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to create user",
- });
- }
- },
- });
-
- useEffect(() => {
- if (!isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Add User</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Name</FormLabel>
- <FormControl>
- <Input
- type="text"
- placeholder="Name"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="email"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Email</FormLabel>
- <FormControl>
- <Input
- type="email"
- placeholder="Email"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="password"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="confirmPassword"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Confirm Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="role"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Role</FormLabel>
- <FormControl>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <SelectTrigger className="w-full">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="user">User</SelectItem>
- <SelectItem value="admin">Admin</SelectItem>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Create
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
+import { useEffect, useState } from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "@/components/ui/sonner"; +import { useTRPC } from "@/lib/trpc"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { TRPCClientError } from "@trpc/client"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { zAdminCreateUserSchema } from "@karakeep/shared/types/admin"; + +type AdminCreateUserSchema = z.infer<typeof zAdminCreateUserSchema>; + +export default function AddUserDialog({ + children, +}: { + children?: React.ReactNode; +}) { + const api = useTRPC(); + const queryClient = useQueryClient(); + const [isOpen, onOpenChange] = useState(false); + const form = useForm<AdminCreateUserSchema>({ + resolver: zodResolver(zAdminCreateUserSchema), + defaultValues: { + name: "", + email: "", + password: "", + confirmPassword: "", + role: "user", + }, + }); + const { mutate, isPending } = useMutation( + api.admin.createUser.mutationOptions({ + onSuccess: () => { + toast({ + description: "User created successfully", + }); + onOpenChange(false); + queryClient.invalidateQueries(api.users.list.pathFilter()); + queryClient.invalidateQueries(api.admin.userStats.pathFilter()); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to create user", + }); + } + }, + }), + ); + + useEffect(() => { + if (!isOpen) { + form.reset(); + } + }, [isOpen, form]); + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogTrigger asChild>{children}</DialogTrigger> + <DialogContent> + <DialogHeader> + <DialogTitle>Add User</DialogTitle> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit((val) => mutate(val))}> + <div className="flex w-full flex-col space-y-2"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Name</FormLabel> + <FormControl> + <Input + type="text" + placeholder="Name" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormLabel>Email</FormLabel> + <FormControl> + <Input + type="email" + placeholder="Email" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="confirmPassword" + render={({ field }) => ( + <FormItem> + <FormLabel>Confirm Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Confirm Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="role" + render={({ field }) => ( + <FormItem> + <FormLabel>Role</FormLabel> + <FormControl> + <Select + value={field.value} + onValueChange={field.onChange} + > + <SelectTrigger className="w-full"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="user">User</SelectItem> + <SelectItem value="admin">Admin</SelectItem> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <DialogFooter className="sm:justify-end"> + <DialogClose asChild> + <Button type="button" variant="secondary"> + Close + </Button> + </DialogClose> + <ActionButton + type="submit" + loading={isPending} + disabled={isPending} + > + Create + </ActionButton> + </DialogFooter> + </div> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/components/admin/AdminNotices.tsx b/apps/web/components/admin/AdminNotices.tsx index 77b1b481..e9d9a692 100644 --- a/apps/web/components/admin/AdminNotices.tsx +++ b/apps/web/components/admin/AdminNotices.tsx @@ -2,7 +2,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { AlertCircle } from "lucide-react"; import { AdminCard } from "./AdminCard"; @@ -14,7 +15,8 @@ interface AdminNotice { } function useAdminNotices() { - const { data } = api.admin.getAdminNoticies.useQuery(); + const api = useTRPC(); + const { data } = useQuery(api.admin.getAdminNoticies.queryOptions()); if (!data) { return []; } diff --git a/apps/web/components/admin/BackgroundJobs.tsx b/apps/web/components/admin/BackgroundJobs.tsx index 382069c8..3dab3c54 100644 --- a/apps/web/components/admin/BackgroundJobs.tsx +++ b/apps/web/components/admin/BackgroundJobs.tsx @@ -13,8 +13,8 @@ import { import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; -import { keepPreviousData } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; import { Activity, AlertTriangle, @@ -254,13 +254,51 @@ function JobCard({ } function useJobActions() { + const api = useTRPC(); const { t } = useTranslation(); const { mutateAsync: recrawlLinks, isPending: isRecrawlPending } = - api.admin.recrawlLinks.useMutation({ + useMutation( + api.admin.recrawlLinks.mutationOptions({ + onSuccess: () => { + toast({ + description: "Recrawl enqueued", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }), + ); + + const { mutateAsync: reindexBookmarks, isPending: isReindexPending } = + useMutation( + api.admin.reindexAllBookmarks.mutationOptions({ + onSuccess: () => { + toast({ + description: "Reindex enqueued", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }), + ); + + const { + mutateAsync: reprocessAssetsFixMode, + isPending: isReprocessingPending, + } = useMutation( + api.admin.reprocessAssetsFixMode.mutationOptions({ onSuccess: () => { toast({ - description: "Recrawl enqueued", + description: "Reprocessing enqueued", }); }, onError: (e) => { @@ -269,13 +307,17 @@ function useJobActions() { description: e.message, }); }, - }); + }), + ); - const { mutateAsync: reindexBookmarks, isPending: isReindexPending } = - api.admin.reindexAllBookmarks.useMutation({ + const { + mutateAsync: reRunInferenceOnAllBookmarks, + isPending: isInferencePending, + } = useMutation( + api.admin.reRunInferenceOnAllBookmarks.mutationOptions({ onSuccess: () => { toast({ - description: "Reindex enqueued", + description: "Inference jobs enqueued", }); }, onError: (e) => { @@ -284,58 +326,27 @@ function useJobActions() { description: e.message, }); }, - }); - - const { - mutateAsync: reprocessAssetsFixMode, - isPending: isReprocessingPending, - } = api.admin.reprocessAssetsFixMode.useMutation({ - onSuccess: () => { - toast({ - description: "Reprocessing enqueued", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); - - const { - mutateAsync: reRunInferenceOnAllBookmarks, - isPending: isInferencePending, - } = api.admin.reRunInferenceOnAllBookmarks.useMutation({ - onSuccess: () => { - toast({ - description: "Inference jobs enqueued", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); + }), + ); const { mutateAsync: runAdminMaintenanceTask, isPending: isAdminMaintenancePending, - } = api.admin.runAdminMaintenanceTask.useMutation({ - onSuccess: () => { - toast({ - description: "Admin maintenance request has been enqueued!", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); + } = useMutation( + api.admin.runAdminMaintenanceTask.mutationOptions({ + onSuccess: () => { + toast({ + description: "Admin maintenance request has been enqueued!", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }), + ); return { crawlActions: [ @@ -466,13 +477,13 @@ function useJobActions() { } export default function BackgroundJobs() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: serverStats } = api.admin.backgroundJobsStats.useQuery( - undefined, - { + const { data: serverStats } = useQuery( + api.admin.backgroundJobsStats.queryOptions(undefined, { refetchInterval: 1000, placeholderData: keepPreviousData, - }, + }), ); const actions = useJobActions(); diff --git a/apps/web/components/admin/BasicStats.tsx b/apps/web/components/admin/BasicStats.tsx index 67352f66..9c88ba83 100644 --- a/apps/web/components/admin/BasicStats.tsx +++ b/apps/web/components/admin/BasicStats.tsx @@ -3,7 +3,7 @@ import { AdminCard } from "@/components/admin/AdminCard"; import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useQuery } from "@tanstack/react-query"; const REPO_LATEST_RELEASE_API = @@ -42,7 +42,7 @@ function ReleaseInfo() { rel="noreferrer" title="Update available" > - ({latestRelease} ⬆️) + ({latestRelease}⬆️) </a> ); } @@ -71,10 +71,13 @@ function StatsSkeleton() { } export default function BasicStats() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: serverStats } = api.admin.stats.useQuery(undefined, { - refetchInterval: 5000, - }); + const { data: serverStats } = useQuery( + api.admin.stats.queryOptions(undefined, { + refetchInterval: 5000, + }), + ); if (!serverStats) { return <StatsSkeleton />; diff --git a/apps/web/components/admin/BookmarkDebugger.tsx b/apps/web/components/admin/BookmarkDebugger.tsx index 1628fdcc..78eb2c85 100644 --- a/apps/web/components/admin/BookmarkDebugger.tsx +++ b/apps/web/components/admin/BookmarkDebugger.tsx @@ -8,8 +8,9 @@ import { Button } from "@/components/ui/button"; import InfoTooltip from "@/components/ui/info-tooltip"; import { Input } from "@/components/ui/input"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { formatBytes } from "@/lib/utils"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; import { AlertCircle, @@ -37,6 +38,7 @@ import { toast } from "sonner"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; export default function BookmarkDebugger() { + const api = useTRPC(); const { t } = useTranslation(); const [inputValue, setInputValue] = useState(""); const [bookmarkId, setBookmarkId] = useQueryState( @@ -56,9 +58,11 @@ export default function BookmarkDebugger() { data: debugInfo, isLoading, error, - } = api.admin.getBookmarkDebugInfo.useQuery( - { bookmarkId: bookmarkId }, - { enabled: !!bookmarkId && bookmarkId.length > 0 }, + } = useQuery( + api.admin.getBookmarkDebugInfo.queryOptions( + { bookmarkId: bookmarkId }, + { enabled: !!bookmarkId && bookmarkId.length > 0 }, + ), ); const handleLookup = () => { @@ -67,57 +71,65 @@ export default function BookmarkDebugger() { } }; - const recrawlMutation = api.admin.adminRecrawlBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.recrawl_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const recrawlMutation = useMutation( + api.admin.adminRecrawlBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.recrawl_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); - const reindexMutation = api.admin.adminReindexBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.reindex_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const reindexMutation = useMutation( + api.admin.adminReindexBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.reindex_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); - const retagMutation = api.admin.adminRetagBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.retag_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const retagMutation = useMutation( + api.admin.adminRetagBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.retag_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); - const resummarizeMutation = api.admin.adminResummarizeBookmark.useMutation({ - onSuccess: () => { - toast.success(t("admin.admin_tools.action_success"), { - description: t("admin.admin_tools.resummarize_queued"), - }); - }, - onError: (error) => { - toast.error(t("admin.admin_tools.action_failed"), { - description: error.message, - }); - }, - }); + const resummarizeMutation = useMutation( + api.admin.adminResummarizeBookmark.mutationOptions({ + onSuccess: () => { + toast.success(t("admin.admin_tools.action_success"), { + description: t("admin.admin_tools.resummarize_queued"), + }); + }, + onError: (error) => { + toast.error(t("admin.admin_tools.action_failed"), { + description: error.message, + }); + }, + }), + ); const handleRecrawl = () => { if (bookmarkId) { diff --git a/apps/web/components/admin/CreateInviteDialog.tsx b/apps/web/components/admin/CreateInviteDialog.tsx index 6738adc9..c8b6be8c 100644 --- a/apps/web/components/admin/CreateInviteDialog.tsx +++ b/apps/web/components/admin/CreateInviteDialog.tsx @@ -20,8 +20,9 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -37,6 +38,8 @@ interface CreateInviteDialogProps { export default function CreateInviteDialog({ children, }: CreateInviteDialogProps) { + const api = useTRPC(); + const queryClient = useQueryClient(); const [open, setOpen] = useState(false); const [errorMessage, setErrorMessage] = useState(""); @@ -47,25 +50,26 @@ export default function CreateInviteDialog({ }, }); - const invalidateInvitesList = api.useUtils().invites.list.invalidate; - const createInviteMutation = api.invites.create.useMutation({ - onSuccess: () => { - toast({ - description: "Invite sent successfully", - }); - invalidateInvitesList(); - setOpen(false); - form.reset(); - setErrorMessage(""); - }, - onError: (e) => { - if (e instanceof TRPCClientError) { - setErrorMessage(e.message); - } else { - setErrorMessage("Failed to send invite"); - } - }, - }); + const createInviteMutation = useMutation( + api.invites.create.mutationOptions({ + onSuccess: () => { + toast({ + description: "Invite sent successfully", + }); + queryClient.invalidateQueries(api.invites.list.pathFilter()); + setOpen(false); + form.reset(); + setErrorMessage(""); + }, + onError: (e) => { + if (e instanceof TRPCClientError) { + setErrorMessage(e.message); + } else { + setErrorMessage("Failed to send invite"); + } + }, + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> diff --git a/apps/web/components/admin/InvitesList.tsx b/apps/web/components/admin/InvitesList.tsx index 75d29748..9d94f0a0 100644 --- a/apps/web/components/admin/InvitesList.tsx +++ b/apps/web/components/admin/InvitesList.tsx @@ -11,7 +11,12 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; import { Mail, MailX, UserPlus } from "lucide-react"; @@ -20,16 +25,17 @@ import { AdminCard } from "./AdminCard"; import CreateInviteDialog from "./CreateInviteDialog"; export default function InvitesList() { - const invalidateInvitesList = api.useUtils().invites.list.invalidate; - const [invites] = api.invites.list.useSuspenseQuery(); + const api = useTRPC(); + const queryClient = useQueryClient(); + const { data: invites } = useSuspenseQuery(api.invites.list.queryOptions()); - const { mutateAsync: revokeInvite, isPending: isRevokePending } = - api.invites.revoke.useMutation({ + const { mutateAsync: revokeInvite, isPending: isRevokePending } = useMutation( + api.invites.revoke.mutationOptions({ onSuccess: () => { toast({ description: "Invite revoked successfully", }); - invalidateInvitesList(); + queryClient.invalidateQueries(api.invites.list.pathFilter()); }, onError: (e) => { toast({ @@ -37,15 +43,16 @@ export default function InvitesList() { description: `Failed to revoke invite: ${e.message}`, }); }, - }); + }), + ); - const { mutateAsync: resendInvite, isPending: isResendPending } = - api.invites.resend.useMutation({ + const { mutateAsync: resendInvite, isPending: isResendPending } = useMutation( + api.invites.resend.mutationOptions({ onSuccess: () => { toast({ description: "Invite resent successfully", }); - invalidateInvitesList(); + queryClient.invalidateQueries(api.invites.list.pathFilter()); }, onError: (e) => { toast({ @@ -53,7 +60,8 @@ export default function InvitesList() { description: `Failed to resend invite: ${e.message}`, }); }, - }); + }), + ); const activeInvites = invites?.invites || []; diff --git a/apps/web/components/admin/ResetPasswordDialog.tsx b/apps/web/components/admin/ResetPasswordDialog.tsx index 4e71d42b..59886d54 100644 --- a/apps/web/components/admin/ResetPasswordDialog.tsx +++ b/apps/web/components/admin/ResetPasswordDialog.tsx @@ -1,145 +1,149 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { toast } from "@/components/ui/sonner";
-import { api } from "@/lib/trpc"; // Adjust the import path as needed
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { resetPasswordSchema } from "@karakeep/shared/types/admin";
-
-interface ResetPasswordDialogProps {
- userId: string;
- children?: React.ReactNode;
-}
-
-type ResetPasswordSchema = z.infer<typeof resetPasswordSchema>;
-
-export default function ResetPasswordDialog({
- children,
- userId,
-}: ResetPasswordDialogProps) {
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<ResetPasswordSchema>({
- resolver: zodResolver(resetPasswordSchema),
- defaultValues: {
- userId,
- newPassword: "",
- newPasswordConfirm: "",
- },
- });
- const { mutate, isPending } = api.admin.resetPassword.useMutation({
- onSuccess: () => {
- toast({
- description: "Password reset successfully",
- });
- onOpenChange(false);
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to reset password",
- });
- }
- },
- });
-
- useEffect(() => {
- if (isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Reset Password</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="newPassword"
- render={({ field }) => (
- <FormItem>
- <FormLabel>New Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="New Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="newPasswordConfirm"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Confirm New Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm New Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Reset
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
+import { useEffect, useState } from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { toast } from "@/components/ui/sonner"; +import { useTRPC } from "@/lib/trpc"; // Adjust the import path as needed +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { TRPCClientError } from "@trpc/client"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { resetPasswordSchema } from "@karakeep/shared/types/admin"; + +interface ResetPasswordDialogProps { + userId: string; + children?: React.ReactNode; +} + +type ResetPasswordSchema = z.infer<typeof resetPasswordSchema>; + +export default function ResetPasswordDialog({ + children, + userId, +}: ResetPasswordDialogProps) { + const api = useTRPC(); + const [isOpen, onOpenChange] = useState(false); + const form = useForm<ResetPasswordSchema>({ + resolver: zodResolver(resetPasswordSchema), + defaultValues: { + userId, + newPassword: "", + newPasswordConfirm: "", + }, + }); + const { mutate, isPending } = useMutation( + api.admin.resetPassword.mutationOptions({ + onSuccess: () => { + toast({ + description: "Password reset successfully", + }); + onOpenChange(false); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to reset password", + }); + } + }, + }), + ); + + useEffect(() => { + if (isOpen) { + form.reset(); + } + }, [isOpen, form]); + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogTrigger asChild>{children}</DialogTrigger> + <DialogContent> + <DialogHeader> + <DialogTitle>Reset Password</DialogTitle> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit((val) => mutate(val))}> + <div className="flex w-full flex-col space-y-2"> + <FormField + control={form.control} + name="newPassword" + render={({ field }) => ( + <FormItem> + <FormLabel>New Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="New Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="newPasswordConfirm" + render={({ field }) => ( + <FormItem> + <FormLabel>Confirm New Password</FormLabel> + <FormControl> + <Input + type="password" + placeholder="Confirm New Password" + {...field} + className="w-full rounded border p-2" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <DialogFooter className="sm:justify-end"> + <DialogClose asChild> + <Button type="button" variant="secondary"> + Close + </Button> + </DialogClose> + <ActionButton + type="submit" + loading={isPending} + disabled={isPending} + > + Reset + </ActionButton> + </DialogFooter> + </div> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/components/admin/ServiceConnections.tsx b/apps/web/components/admin/ServiceConnections.tsx index 8d79d8bb..0509f352 100644 --- a/apps/web/components/admin/ServiceConnections.tsx +++ b/apps/web/components/admin/ServiceConnections.tsx @@ -2,7 +2,8 @@ import { AdminCard } from "@/components/admin/AdminCard"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; function ConnectionStatus({ label, @@ -105,10 +106,13 @@ function ConnectionsSkeleton() { } export default function ServiceConnections() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: connections } = api.admin.checkConnections.useQuery(undefined, { - refetchInterval: 10000, - }); + const { data: connections } = useQuery( + api.admin.checkConnections.queryOptions(undefined, { + refetchInterval: 10000, + }), + ); if (!connections) { return <ConnectionsSkeleton />; diff --git a/apps/web/components/admin/UpdateUserDialog.tsx b/apps/web/components/admin/UpdateUserDialog.tsx index 453f4fab..aeec9d4e 100644 --- a/apps/web/components/admin/UpdateUserDialog.tsx +++ b/apps/web/components/admin/UpdateUserDialog.tsx @@ -27,8 +27,9 @@ import { SelectValue, } from "@/components/ui/select"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -51,7 +52,8 @@ export default function UpdateUserDialog({ currentStorageQuota, children, }: UpdateUserDialogProps) { - const apiUtils = api.useUtils(); + const api = useTRPC(); + const queryClient = useQueryClient(); const [isOpen, onOpenChange] = useState(false); const defaultValues = { userId, @@ -63,28 +65,30 @@ export default function UpdateUserDialog({ resolver: zodResolver(updateUserSchema), defaultValues, }); - const { mutate, isPending } = api.admin.updateUser.useMutation({ - onSuccess: () => { - toast({ - description: "User updated successfully", - }); - apiUtils.users.list.invalidate(); - onOpenChange(false); - }, - onError: (error) => { - if (error instanceof TRPCClientError) { + const { mutate, isPending } = useMutation( + api.admin.updateUser.mutationOptions({ + onSuccess: () => { toast({ - variant: "destructive", - description: error.message, + description: "User updated successfully", }); - } else { - toast({ - variant: "destructive", - description: "Failed to update user", - }); - } - }, - }); + queryClient.invalidateQueries(api.users.list.pathFilter()); + onOpenChange(false); + }, + onError: (error) => { + if (error instanceof TRPCClientError) { + toast({ + variant: "destructive", + description: error.message, + }); + } else { + toast({ + variant: "destructive", + description: "Failed to update user", + }); + } + }, + }), + ); useEffect(() => { if (isOpen) { diff --git a/apps/web/components/admin/UserList.tsx b/apps/web/components/admin/UserList.tsx index 69f9e3b9..810a945f 100644 --- a/apps/web/components/admin/UserList.tsx +++ b/apps/web/components/admin/UserList.tsx @@ -13,7 +13,12 @@ import { } from "@/components/ui/table"; import { useSession } from "@/lib/auth/client"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; import { Check, KeyRound, Pencil, Trash, UserPlus, X } from "lucide-react"; import ActionConfirmingDialog from "../ui/action-confirming-dialog"; @@ -30,18 +35,23 @@ function toHumanReadableSize(size: number) { } export default function UsersSection() { + const api = useTRPC(); + const queryClient = useQueryClient(); const { t } = useTranslation(); const { data: session } = useSession(); - const invalidateUserList = api.useUtils().users.list.invalidate; - const [{ users }] = api.users.list.useSuspenseQuery(); - const [userStats] = api.admin.userStats.useSuspenseQuery(); - const { mutateAsync: deleteUser, isPending: isDeletionPending } = - api.users.delete.useMutation({ + const { + data: { users }, + } = useSuspenseQuery(api.users.list.queryOptions()); + const { data: userStats } = useSuspenseQuery( + api.admin.userStats.queryOptions(), + ); + const { mutateAsync: deleteUser, isPending: isDeletionPending } = useMutation( + api.users.delete.mutationOptions({ onSuccess: () => { toast({ description: "User deleted", }); - invalidateUserList(); + queryClient.invalidateQueries(api.users.list.pathFilter()); }, onError: (e) => { toast({ @@ -49,7 +59,8 @@ export default function UsersSection() { description: `Something went wrong: ${e.message}`, }); }, - }); + }), + ); return ( <AdminCard> diff --git a/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx index 595a9e00..4d2b58e7 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx @@ -1,4 +1,5 @@ -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { getBookmarkRefreshInterval } from "@karakeep/shared/utils/bookmarkUtils"; @@ -15,20 +16,23 @@ export default function BookmarkCard({ bookmark: ZBookmark; className?: string; }) { - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: initialData.id, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const api = useTRPC(); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId: initialData.id, }, - }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }, + ), ); switch (bookmark.content.type) { diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx index 3e27dbcb..82387325 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx @@ -7,13 +7,14 @@ import Image from "next/image"; import Link from "next/link"; import { useSession } from "@/lib/auth/client"; import useBulkActionsStore from "@/lib/bulkActions"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { bookmarkLayoutSwitch, useBookmarkDisplaySettings, useBookmarkLayout, } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { Check, Image as ImageIcon, NotebookPen } from "lucide-react"; import { useTheme } from "next-themes"; @@ -64,15 +65,18 @@ function BottomRow({ } function OwnerIndicator({ bookmark }: { bookmark: ZBookmark }) { + const api = useTRPC(); const listContext = useBookmarkListContext(); - const collaborators = api.lists.getCollaborators.useQuery( - { - listId: listContext?.id ?? "", - }, - { - refetchOnWindowFocus: false, - enabled: !!listContext?.hasCollaborators, - }, + const collaborators = useQuery( + api.lists.getCollaborators.queryOptions( + { + listId: listContext?.id ?? "", + }, + { + refetchOnWindowFocus: false, + enabled: !!listContext?.hasCollaborators, + }, + ), ); if (!listContext || listContext.userRole === "owner" || !collaborators.data) { diff --git a/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx b/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx index 53d2d013..96cf1fed 100644 --- a/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx +++ b/apps/web/components/dashboard/bookmarks/BulkTagModal.tsx @@ -8,9 +8,10 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { toast } from "@/components/ui/sonner"; +import { useTRPC } from "@/lib/trpc"; +import { useQueries } from "@tanstack/react-query"; import { useUpdateBookmarkTags } from "@karakeep/shared-react/hooks/bookmarks"; -import { api } from "@karakeep/shared-react/trpc"; import { limitConcurrency } from "@karakeep/shared/concurrency"; import { ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -25,9 +26,12 @@ export default function BulkTagModal({ open: boolean; setOpen: (open: boolean) => void; }) { - const results = api.useQueries((t) => - bookmarkIds.map((id) => t.bookmarks.getBookmark({ bookmarkId: id })), - ); + const api = useTRPC(); + const results = useQueries({ + queries: bookmarkIds.map((id) => + api.bookmarks.getBookmark.queryOptions({ bookmarkId: id }), + ), + }); const bookmarks = results .map((r) => r.data) diff --git a/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx index de6c1ff6..922cea2a 100644 --- a/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx +++ b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx @@ -29,9 +29,10 @@ import { toast } from "@/components/ui/sonner"; import { Textarea } from "@/components/ui/textarea"; import { useDialogFormReset } from "@/lib/hooks/useDialogFormReset"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -60,10 +61,11 @@ export function EditBookmarkDialog({ open: boolean; setOpen: (v: boolean) => void; }) { + const api = useTRPC(); const { t } = useTranslation(); - const { data: assetContent, isLoading: isAssetContentLoading } = - api.bookmarks.getBookmark.useQuery( + const { data: assetContent, isLoading: isAssetContentLoading } = useQuery( + api.bookmarks.getBookmark.queryOptions( { bookmarkId: bookmark.id, includeContent: true, @@ -73,7 +75,8 @@ export function EditBookmarkDialog({ select: (b) => b.content.type == BookmarkTypes.ASSET ? b.content.content : null, }, - ); + ), + ); const bookmarkToDefault = (bookmark: ZBookmark) => ({ bookmarkId: bookmark.id, diff --git a/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx b/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx index 34d797a6..ee92dc5a 100644 --- a/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx +++ b/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx @@ -19,8 +19,9 @@ import { import { toast } from "@/components/ui/sonner"; import LoadingSpinner from "@/components/ui/spinner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { Archive, X } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -43,6 +44,7 @@ export default function ManageListsModal({ open: boolean; setOpen: (open: boolean) => void; }) { + const api = useTRPC(); const { t } = useTranslation(); const formSchema = z.object({ listId: z.string({ @@ -61,13 +63,14 @@ export default function ManageListsModal({ { enabled: open }, ); - const { data: alreadyInList, isPending: isAlreadyInListPending } = - api.lists.getListsOfBookmark.useQuery( + const { data: alreadyInList, isPending: isAlreadyInListPending } = useQuery( + api.lists.getListsOfBookmark.queryOptions( { bookmarkId, }, { enabled: open }, - ); + ), + ); const isLoading = isAllListsPending || isAlreadyInListPending; diff --git a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx index bc06c647..a23f06ed 100644 --- a/apps/web/components/dashboard/bookmarks/TagsEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/TagsEditor.tsx @@ -13,9 +13,9 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; -import { keepPreviousData } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Command as CommandPrimitive } from "cmdk"; import { Check, Loader2, Plus, Sparkles, X } from "lucide-react"; @@ -32,6 +32,7 @@ export function TagsEditor({ onDetach: (tag: { tagName: string; tagId: string }) => void; disabled?: boolean; }) { + const api = useTRPC(); const demoMode = !!useClientConfig().demoMode; const isDisabled = demoMode || disabled; const inputRef = React.useRef<HTMLInputElement>(null); @@ -71,8 +72,8 @@ export function TagsEditor({ }); }, [_tags]); - const { data: filteredOptions, isLoading: isExistingTagsLoading } = - api.tags.list.useQuery( + const { data: filteredOptions, isLoading: isExistingTagsLoading } = useQuery( + api.tags.list.queryOptions( { nameContains: inputValue, limit: 50, @@ -91,7 +92,8 @@ export function TagsEditor({ placeholderData: keepPreviousData, gcTime: inputValue.length > 0 ? 60_000 : 3_600_000, }, - ); + ), + ); const selectedValues = optimisticTags.map((tag) => tag.id); diff --git a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx index 968d0326..817d975d 100644 --- a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx @@ -3,7 +3,8 @@ import { useEffect } from "react"; import UploadDropzone from "@/components/dashboard/UploadDropzone"; import { useSortOrderStore } from "@/lib/store/useSortOrderStore"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useInfiniteQuery } from "@tanstack/react-query"; import type { ZGetBookmarksRequest, @@ -23,6 +24,7 @@ export default function UpdatableBookmarksGrid({ showEditorCard?: boolean; itemsPerPage?: number; }) { + const api = useTRPC(); let sortOrder = useSortOrderStore((state) => state.sortOrder); if (sortOrder === "relevance") { // Relevance is not supported in the `getBookmarks` endpoint. @@ -32,17 +34,19 @@ export default function UpdatableBookmarksGrid({ const finalQuery = { ...query, sortOrder, includeContent: false }; const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = - api.bookmarks.getBookmarks.useInfiniteQuery( - { ...finalQuery, useCursorV2: true }, - { - initialData: () => ({ - pages: [initialBookmarks], - pageParams: [query.cursor], - }), - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - refetchOnMount: true, - }, + useInfiniteQuery( + api.bookmarks.getBookmarks.infiniteQueryOptions( + { ...finalQuery, useCursorV2: true }, + { + initialData: () => ({ + pages: [initialBookmarks], + pageParams: [query.cursor ?? null], + }), + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + refetchOnMount: true, + }, + ), ); useEffect(() => { diff --git a/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx b/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx index fd2780cd..4fd503c1 100644 --- a/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx +++ b/apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx @@ -1,7 +1,8 @@ import React from "react"; import { ActionButton, ActionButtonProps } from "@/components/ui/action-button"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { useUpdateBookmark } from "@karakeep/shared-react/hooks/bookmarks"; @@ -15,13 +16,16 @@ const ArchiveBookmarkButton = React.forwardRef< HTMLButtonElement, ArchiveBookmarkButtonProps >(({ bookmarkId, onDone, ...props }, ref) => { - const { data } = api.bookmarks.getBookmark.useQuery( - { bookmarkId }, - { - select: (data) => ({ - archived: data.archived, - }), - }, + const api = useTRPC(); + const { data } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { bookmarkId }, + { + select: (data) => ({ + archived: data.archived, + }), + }, + ), ); const { mutate: updateBookmark, isPending: isArchivingBookmark } = diff --git a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx index 89aff598..dde72457 100644 --- a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx +++ b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx @@ -22,8 +22,9 @@ import { TableRow, } from "@/components/ui/table"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { distance } from "fastest-levenshtein"; import { Check, Combine, X } from "lucide-react"; @@ -199,12 +200,15 @@ function SuggestionRow({ } export function TagDuplicationDetection() { + const api = useTRPC(); const [expanded, setExpanded] = useState(false); - let { data: allTags } = api.tags.list.useQuery( - {}, - { - refetchOnWindowFocus: false, - }, + let { data: allTags } = useQuery( + api.tags.list.queryOptions( + {}, + { + refetchOnWindowFocus: false, + }, + ), ); const { suggestions, updateMergeInto, setSuggestions, deleteSuggestion } = diff --git a/apps/web/components/dashboard/feeds/FeedSelector.tsx b/apps/web/components/dashboard/feeds/FeedSelector.tsx index db95a042..740dc345 100644 --- a/apps/web/components/dashboard/feeds/FeedSelector.tsx +++ b/apps/web/components/dashboard/feeds/FeedSelector.tsx @@ -7,8 +7,9 @@ import { SelectValue, } from "@/components/ui/select"; import LoadingSpinner from "@/components/ui/spinner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; export function FeedSelector({ value, @@ -21,9 +22,12 @@ export function FeedSelector({ onChange: (value: string) => void; placeholder?: string; }) { - const { data, isPending } = api.feeds.list.useQuery(undefined, { - select: (data) => data.feeds, - }); + const api = useTRPC(); + const { data, isPending } = useQuery( + api.feeds.list.queryOptions(undefined, { + select: (data) => data.feeds, + }), + ); if (isPending) { return <LoadingSpinner />; diff --git a/apps/web/components/dashboard/highlights/AllHighlights.tsx b/apps/web/components/dashboard/highlights/AllHighlights.tsx index 928f4e05..3965d06a 100644 --- a/apps/web/components/dashboard/highlights/AllHighlights.tsx +++ b/apps/web/components/dashboard/highlights/AllHighlights.tsx @@ -5,8 +5,9 @@ import Link from "next/link"; import { ActionButton } from "@/components/ui/action-button"; import { Input } from "@/components/ui/input"; import useRelativeTime from "@/lib/hooks/relative-time"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { Separator } from "@radix-ui/react-dropdown-menu"; +import { useInfiniteQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { Dot, LinkIcon, Search, X } from "lucide-react"; @@ -49,6 +50,7 @@ export default function AllHighlights({ }: { highlights: ZGetAllHighlightsResponse; }) { + const api = useTRPC(); const { t } = useTranslation(); const [searchInput, setSearchInput] = useState(""); const debouncedSearch = useDebounce(searchInput, 300); @@ -56,28 +58,32 @@ export default function AllHighlights({ // Use search endpoint if searchQuery is provided, otherwise use getAll const useSearchQuery = debouncedSearch.trim().length > 0; - const getAllQuery = api.highlights.getAll.useInfiniteQuery( - {}, - { - enabled: !useSearchQuery, - initialData: !useSearchQuery - ? () => ({ - pages: [initialHighlights], - pageParams: [null], - }) - : undefined, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + const getAllQuery = useInfiniteQuery( + api.highlights.getAll.infiniteQueryOptions( + {}, + { + enabled: !useSearchQuery, + initialData: !useSearchQuery + ? () => ({ + pages: [initialHighlights], + pageParams: [null], + }) + : undefined, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); - const searchQueryResult = api.highlights.search.useInfiniteQuery( - { text: debouncedSearch }, - { - enabled: useSearchQuery, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + const searchQueryResult = useInfiniteQuery( + api.highlights.search.infiniteQueryOptions( + { text: debouncedSearch }, + { + enabled: useSearchQuery, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = diff --git a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx index 2bb5f41b..626d0757 100644 --- a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx +++ b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; -import { api } from "@/lib/trpc"; -import { keepPreviousData } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -101,6 +101,7 @@ export function CollapsibleBookmarkLists({ filter?: (node: ZBookmarkListTreeNode) => boolean; indentOffset?: number; }) { + const api = useTRPC(); // If listsData is provided, use it directly. Otherwise, fetch it. let { data: fetchedData } = useBookmarkLists(undefined, { initialData: initialData ? { lists: initialData } : undefined, @@ -108,9 +109,11 @@ export function CollapsibleBookmarkLists({ }); const data = listsData || fetchedData; - const { data: listStats } = api.lists.stats.useQuery(undefined, { - placeholderData: keepPreviousData, - }); + const { data: listStats } = useQuery( + api.lists.stats.queryOptions(undefined, { + placeholderData: keepPreviousData, + }), + ); if (!data) { return <FullPageSpinner />; diff --git a/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx b/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx index f2a48062..79d23b6a 100644 --- a/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx +++ b/apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx @@ -4,7 +4,8 @@ import { ActionButton } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -19,34 +20,37 @@ export default function LeaveListConfirmationDialog({ open: boolean; setOpen: (v: boolean) => void; }) { + const api = useTRPC(); const { t } = useTranslation(); const currentPath = usePathname(); const router = useRouter(); - const utils = api.useUtils(); + const queryClient = useQueryClient(); - const { mutate: leaveList, isPending } = api.lists.leaveList.useMutation({ - onSuccess: () => { - toast({ - description: t("lists.leave_list.success", { - icon: list.icon, - name: list.name, - }), - }); - setOpen(false); - // Invalidate the lists cache - utils.lists.list.invalidate(); - // If currently viewing this list, redirect to lists page - if (currentPath.includes(list.id)) { - router.push("/dashboard/lists"); - } - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("common.something_went_wrong"), - }); - }, - }); + const { mutate: leaveList, isPending } = useMutation( + api.lists.leaveList.mutationOptions({ + onSuccess: () => { + toast({ + description: t("lists.leave_list.success", { + icon: list.icon, + name: list.name, + }), + }); + setOpen(false); + // Invalidate the lists cache + queryClient.invalidateQueries(api.lists.list.pathFilter()); + // If currently viewing this list, redirect to lists page + if (currentPath.includes(list.id)) { + router.push("/dashboard/lists"); + } + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("common.something_went_wrong"), + }); + }, + }), + ); return ( <ActionConfirmingDialog diff --git a/apps/web/components/dashboard/lists/ListHeader.tsx b/apps/web/components/dashboard/lists/ListHeader.tsx index ecbb6431..4176a80e 100644 --- a/apps/web/components/dashboard/lists/ListHeader.tsx +++ b/apps/web/components/dashboard/lists/ListHeader.tsx @@ -10,9 +10,10 @@ import { } from "@/components/ui/tooltip"; import { UserAvatar } from "@/components/ui/user-avatar"; import { useTranslation } from "@/lib/i18n/client"; +import { useQuery } from "@tanstack/react-query"; import { MoreHorizontal, SearchIcon } from "lucide-react"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -24,25 +25,30 @@ export default function ListHeader({ }: { initialData: ZBookmarkList; }) { + const api = useTRPC(); const { t } = useTranslation(); const router = useRouter(); - const { data: list, error } = api.lists.get.useQuery( - { - listId: initialData.id, - }, - { - initialData, - }, + const { data: list, error } = useQuery( + api.lists.get.queryOptions( + { + listId: initialData.id, + }, + { + initialData, + }, + ), ); - const { data: collaboratorsData } = api.lists.getCollaborators.useQuery( - { - listId: initialData.id, - }, - { - refetchOnWindowFocus: false, - enabled: list.hasCollaborators, - }, + const { data: collaboratorsData } = useQuery( + api.lists.getCollaborators.queryOptions( + { + listId: initialData.id, + }, + { + refetchOnWindowFocus: false, + enabled: list.hasCollaborators, + }, + ), ); const parsedQuery = useMemo(() => { diff --git a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx index 6c5dac1e..354e0dfe 100644 --- a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx +++ b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx @@ -25,7 +25,8 @@ import { import { toast } from "@/components/ui/sonner"; import { UserAvatar } from "@/components/ui/user-avatar"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, Trash2, UserPlus, Users } from "lucide-react"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -43,6 +44,7 @@ export function ManageCollaboratorsModal({ children?: React.ReactNode; readOnly?: boolean; }) { + const api = useTRPC(); if ( (userOpen !== undefined && !userSetOpen) || (userOpen === undefined && userSetOpen) @@ -61,82 +63,102 @@ export function ManageCollaboratorsModal({ >("viewer"); const { t } = useTranslation(); - const utils = api.useUtils(); + const queryClient = useQueryClient(); const invalidateListCaches = () => Promise.all([ - utils.lists.getCollaborators.invalidate({ listId: list.id }), - utils.lists.get.invalidate({ listId: list.id }), - utils.lists.list.invalidate(), - utils.bookmarks.getBookmarks.invalidate({ listId: list.id }), + queryClient.invalidateQueries( + api.lists.getCollaborators.queryFilter({ listId: list.id }), + ), + queryClient.invalidateQueries( + api.lists.get.queryFilter({ listId: list.id }), + ), + queryClient.invalidateQueries(api.lists.list.pathFilter()), + queryClient.invalidateQueries( + api.bookmarks.getBookmarks.queryFilter({ listId: list.id }), + ), ]); // Fetch collaborators - const { data: collaboratorsData, isLoading } = - api.lists.getCollaborators.useQuery({ listId: list.id }, { enabled: open }); + const { data: collaboratorsData, isLoading } = useQuery( + api.lists.getCollaborators.queryOptions( + { listId: list.id }, + { enabled: open }, + ), + ); // Mutations - const addCollaborator = api.lists.addCollaborator.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.invitation_sent"), - }); - setNewCollaboratorEmail(""); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.collaborators.failed_to_add"), - }); - }, - }); + const addCollaborator = useMutation( + api.lists.addCollaborator.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.invitation_sent"), + }); + setNewCollaboratorEmail(""); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("lists.collaborators.failed_to_add"), + }); + }, + }), + ); - const removeCollaborator = api.lists.removeCollaborator.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.removed"), - }); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.collaborators.failed_to_remove"), - }); - }, - }); + const removeCollaborator = useMutation( + api.lists.removeCollaborator.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.removed"), + }); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.collaborators.failed_to_remove"), + }); + }, + }), + ); - const updateCollaboratorRole = api.lists.updateCollaboratorRole.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.role_updated"), - }); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: - error.message || t("lists.collaborators.failed_to_update_role"), - }); - }, - }); + const updateCollaboratorRole = useMutation( + api.lists.updateCollaboratorRole.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.role_updated"), + }); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.collaborators.failed_to_update_role"), + }); + }, + }), + ); - const revokeInvitation = api.lists.revokeInvitation.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.collaborators.invitation_revoked"), - }); - await invalidateListCaches(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.collaborators.failed_to_revoke"), - }); - }, - }); + const revokeInvitation = useMutation( + api.lists.revokeInvitation.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.collaborators.invitation_revoked"), + }); + await invalidateListCaches(); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.collaborators.failed_to_revoke"), + }); + }, + }), + ); const handleAddCollaborator = () => { if (!newCollaboratorEmail.trim()) { diff --git a/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx b/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx index 95a916ff..5d70daaf 100644 --- a/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx +++ b/apps/web/components/dashboard/lists/PendingInvitationsCard.tsx @@ -10,7 +10,8 @@ import { } from "@/components/ui/card"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Check, Loader2, Mail, X } from "lucide-react"; interface Invitation { @@ -27,41 +28,51 @@ interface Invitation { } function InvitationRow({ invitation }: { invitation: Invitation }) { + const api = useTRPC(); const { t } = useTranslation(); - const utils = api.useUtils(); + const queryClient = useQueryClient(); - const acceptInvitation = api.lists.acceptInvitation.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.invitations.accepted"), - }); - await Promise.all([ - utils.lists.getPendingInvitations.invalidate(), - utils.lists.list.invalidate(), - ]); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.invitations.failed_to_accept"), - }); - }, - }); + const acceptInvitation = useMutation( + api.lists.acceptInvitation.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.invitations.accepted"), + }); + await Promise.all([ + queryClient.invalidateQueries( + api.lists.getPendingInvitations.pathFilter(), + ), + queryClient.invalidateQueries(api.lists.list.pathFilter()), + ]); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: error.message || t("lists.invitations.failed_to_accept"), + }); + }, + }), + ); - const declineInvitation = api.lists.declineInvitation.useMutation({ - onSuccess: async () => { - toast({ - description: t("lists.invitations.declined"), - }); - await utils.lists.getPendingInvitations.invalidate(); - }, - onError: (error) => { - toast({ - variant: "destructive", - description: error.message || t("lists.invitations.failed_to_decline"), - }); - }, - }); + const declineInvitation = useMutation( + api.lists.declineInvitation.mutationOptions({ + onSuccess: async () => { + toast({ + description: t("lists.invitations.declined"), + }); + await queryClient.invalidateQueries( + api.lists.getPendingInvitations.pathFilter(), + ); + }, + onError: (error) => { + toast({ + variant: "destructive", + description: + error.message || t("lists.invitations.failed_to_decline"), + }); + }, + }), + ); return ( <div className="flex items-center justify-between rounded-lg border p-4"> @@ -126,10 +137,12 @@ function InvitationRow({ invitation }: { invitation: Invitation }) { } export function PendingInvitationsCard() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: invitations, isLoading } = - api.lists.getPendingInvitations.useQuery(); + const { data: invitations, isLoading } = useQuery( + api.lists.getPendingInvitations.queryOptions(), + ); if (isLoading) { return null; diff --git a/apps/web/components/dashboard/lists/RssLink.tsx b/apps/web/components/dashboard/lists/RssLink.tsx index 1be48681..5bc0fdf0 100644 --- a/apps/web/components/dashboard/lists/RssLink.tsx +++ b/apps/web/components/dashboard/lists/RssLink.tsx @@ -7,29 +7,38 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, RotateCcw } from "lucide-react"; import { useTranslation } from "react-i18next"; export default function RssLink({ listId }: { listId: string }) { + const api = useTRPC(); const { t } = useTranslation(); const clientConfig = useClientConfig(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); - const { mutate: regenRssToken, isPending: isRegenPending } = - api.lists.regenRssToken.useMutation({ + const { mutate: regenRssToken, isPending: isRegenPending } = useMutation( + api.lists.regenRssToken.mutationOptions({ onSuccess: () => { - apiUtils.lists.getRssToken.invalidate({ listId }); + queryClient.invalidateQueries( + api.lists.getRssToken.queryFilter({ listId }), + ); }, - }); - const { mutate: clearRssToken, isPending: isClearPending } = - api.lists.clearRssToken.useMutation({ + }), + ); + const { mutate: clearRssToken, isPending: isClearPending } = useMutation( + api.lists.clearRssToken.mutationOptions({ onSuccess: () => { - apiUtils.lists.getRssToken.invalidate({ listId }); + queryClient.invalidateQueries( + api.lists.getRssToken.queryFilter({ listId }), + ); }, - }); - const { data: rssToken, isLoading: isTokenLoading } = - api.lists.getRssToken.useQuery({ listId }); + }), + ); + const { data: rssToken, isLoading: isTokenLoading } = useQuery( + api.lists.getRssToken.queryOptions({ listId }), + ); const rssUrl = useMemo(() => { if (!rssToken || !rssToken.token) { diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index d56bfb6a..b9b8ff81 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -16,7 +16,8 @@ import { import { useSession } from "@/lib/auth/client"; import useRelativeTime from "@/lib/hooks/relative-time"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Building, CalendarDays, ExternalLink, User } from "lucide-react"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -116,24 +117,27 @@ export default function BookmarkPreview({ bookmarkId: string; initialData?: ZBookmark; }) { + const api = useTRPC(); const { t } = useTranslation(); const [activeTab, setActiveTab] = useState<string>("content"); const { data: session } = useSession(); - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - return getBookmarkRefreshInterval(data); + const { data: bookmark } = useQuery( + api.bookmarks.getBookmark.queryOptions( + { + bookmarkId, }, - }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + return getBookmarkRefreshInterval(data); + }, + }, + ), ); if (!bookmark) { diff --git a/apps/web/components/dashboard/preview/HighlightsBox.tsx b/apps/web/components/dashboard/preview/HighlightsBox.tsx index 41ab7d74..d4821655 100644 --- a/apps/web/components/dashboard/preview/HighlightsBox.tsx +++ b/apps/web/components/dashboard/preview/HighlightsBox.tsx @@ -5,8 +5,9 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { Separator } from "@radix-ui/react-dropdown-menu"; +import { useQuery } from "@tanstack/react-query"; import { ChevronsDownUp } from "lucide-react"; import HighlightCard from "../highlights/HighlightCard"; @@ -18,10 +19,12 @@ export default function HighlightsBox({ bookmarkId: string; readOnly: boolean; }) { + const api = useTRPC(); const { t } = useTranslation(); - const { data: highlights, isPending: isLoading } = - api.highlights.getForBookmark.useQuery({ bookmarkId }); + const { data: highlights, isPending: isLoading } = useQuery( + api.highlights.getForBookmark.queryOptions({ bookmarkId }), + ); if (isLoading || !highlights || highlights?.highlights.length === 0) { return null; diff --git a/apps/web/components/dashboard/preview/ReaderView.tsx b/apps/web/components/dashboard/preview/ReaderView.tsx index 9b765d55..4d9bcd6c 100644 --- a/apps/web/components/dashboard/preview/ReaderView.tsx +++ b/apps/web/components/dashboard/preview/ReaderView.tsx @@ -1,6 +1,7 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { toast } from "@/components/ui/sonner"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { useCreateHighlight, @@ -22,11 +23,14 @@ export default function ReaderView({ style?: React.CSSProperties; readOnly: boolean; }) { - const { data: highlights } = api.highlights.getForBookmark.useQuery({ - bookmarkId, - }); - const { data: cachedContent, isPending: isCachedContentLoading } = - api.bookmarks.getBookmark.useQuery( + const api = useTRPC(); + const { data: highlights } = useQuery( + api.highlights.getForBookmark.queryOptions({ + bookmarkId, + }), + ); + const { data: cachedContent, isPending: isCachedContentLoading } = useQuery( + api.bookmarks.getBookmark.queryOptions( { bookmarkId, includeContent: true, @@ -37,7 +41,8 @@ export default function ReaderView({ ? data.content.htmlContent : null, }, - ); + ), + ); const { mutate: createHighlight } = useCreateHighlight({ onSuccess: () => { diff --git a/apps/web/components/dashboard/search/useSearchAutocomplete.ts b/apps/web/components/dashboard/search/useSearchAutocomplete.ts index 7ddfa39a..380a79b6 100644 --- a/apps/web/components/dashboard/search/useSearchAutocomplete.ts +++ b/apps/web/components/dashboard/search/useSearchAutocomplete.ts @@ -2,7 +2,8 @@ import type translation from "@/lib/i18n/locales/en/translation.json"; import type { TFunction } from "i18next"; import type { LucideIcon } from "lucide-react"; import { useCallback, useMemo } from "react"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { History, ListTree, @@ -293,6 +294,7 @@ const useTagSuggestions = ( const useFeedSuggestions = ( parsed: ParsedSearchState, ): AutocompleteSuggestionItem[] => { + const api = useTRPC(); const shouldSuggestFeeds = parsed.normalizedTokenWithoutMinus.startsWith("feed:"); const feedSearchTermRaw = shouldSuggestFeeds @@ -300,9 +302,11 @@ const useFeedSuggestions = ( : ""; const feedSearchTerm = stripSurroundingQuotes(feedSearchTermRaw); const normalizedFeedSearchTerm = feedSearchTerm.toLowerCase(); - const { data: feedResults } = api.feeds.list.useQuery(undefined, { - enabled: parsed.activeToken.length > 0, - }); + const { data: feedResults } = useQuery( + api.feeds.list.queryOptions(undefined, { + enabled: parsed.activeToken.length > 0, + }), + ); const feedSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => { if (!shouldSuggestFeeds) { diff --git a/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx b/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx index e4d7b39f..547b8a76 100644 --- a/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx +++ b/apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx @@ -1,13 +1,14 @@ "use client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; export function InvitationNotificationBadge() { - const { data: pendingInvitations } = api.lists.getPendingInvitations.useQuery( - undefined, - { + const api = useTRPC(); + const { data: pendingInvitations } = useQuery( + api.lists.getPendingInvitations.queryOptions(undefined, { refetchInterval: 1000 * 60 * 5, - }, + }), ); const pendingInvitationsCount = pendingInvitations?.length ?? 0; diff --git a/apps/web/components/dashboard/tags/TagAutocomplete.tsx b/apps/web/components/dashboard/tags/TagAutocomplete.tsx index 8164dc81..656d4c5a 100644 --- a/apps/web/components/dashboard/tags/TagAutocomplete.tsx +++ b/apps/web/components/dashboard/tags/TagAutocomplete.tsx @@ -15,11 +15,12 @@ import { } from "@/components/ui/popover"; import LoadingSpinner from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; import { Check, ChevronsUpDown, X } from "lucide-react"; import { useTagAutocomplete } from "@karakeep/shared-react/hooks/tags"; import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; interface TagAutocompleteProps { tagId: string; @@ -32,6 +33,7 @@ export function TagAutocomplete({ onChange, className, }: TagAutocompleteProps) { + const api = useTRPC(); const [open, setOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const searchQueryDebounced = useDebounce(searchQuery, 500); @@ -41,8 +43,8 @@ export function TagAutocomplete({ select: (data) => data.tags, }); - const { data: selectedTag, isLoading: isSelectedTagLoading } = - api.tags.get.useQuery( + const { data: selectedTag, isLoading: isSelectedTagLoading } = useQuery( + api.tags.get.queryOptions( { tagId, }, @@ -53,7 +55,8 @@ export function TagAutocomplete({ }), enabled: !!tagId, }, - ); + ), + ); const handleSelect = (currentValue: string) => { setOpen(false); diff --git a/apps/web/components/invite/InviteAcceptForm.tsx b/apps/web/components/invite/InviteAcceptForm.tsx index 5fa166c0..9ad4de1c 100644 --- a/apps/web/components/invite/InviteAcceptForm.tsx +++ b/apps/web/components/invite/InviteAcceptForm.tsx @@ -22,8 +22,9 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { signIn } from "@/lib/auth/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, Clock, Loader2, Mail, UserPlus } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -47,6 +48,7 @@ interface InviteAcceptFormProps { } export default function InviteAcceptForm({ token }: InviteAcceptFormProps) { + const api = useTRPC(); const router = useRouter(); const form = useForm<z.infer<typeof inviteAcceptSchema>>({ @@ -59,7 +61,7 @@ export default function InviteAcceptForm({ token }: InviteAcceptFormProps) { isPending: loading, data: inviteData, error, - } = api.invites.get.useQuery({ token }); + } = useQuery(api.invites.get.queryOptions({ token })); useEffect(() => { if (error) { @@ -67,7 +69,9 @@ export default function InviteAcceptForm({ token }: InviteAcceptFormProps) { } }, [error]); - const acceptInviteMutation = api.invites.accept.useMutation(); + const acceptInviteMutation = useMutation( + api.invites.accept.mutationOptions(), + ); const handleBackToSignIn = () => { router.push("/signin"); diff --git a/apps/web/components/public/lists/PublicBookmarkGrid.tsx b/apps/web/components/public/lists/PublicBookmarkGrid.tsx index 18e42baa..9832c5b1 100644 --- a/apps/web/components/public/lists/PublicBookmarkGrid.tsx +++ b/apps/web/components/public/lists/PublicBookmarkGrid.tsx @@ -9,9 +9,10 @@ import { ActionButton } from "@/components/ui/action-button"; import { badgeVariants } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import tailwindConfig from "@/tailwind.config"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { Expand, FileIcon, ImageIcon } from "lucide-react"; import { useInView } from "react-intersection-observer"; import Masonry from "react-masonry-css"; @@ -199,19 +200,22 @@ export default function PublicBookmarkGrid({ bookmarks: ZPublicBookmark[]; nextCursor: ZCursor | null; }) { + const api = useTRPC(); const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView(); const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - api.publicBookmarks.getPublicBookmarksInList.useInfiniteQuery( - { listId: list.id }, - { - initialData: () => ({ - pages: [{ bookmarks: initialBookmarks, nextCursor, list }], - pageParams: [null], - }), - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - refetchOnMount: true, - }, + useInfiniteQuery( + api.publicBookmarks.getPublicBookmarksInList.infiniteQueryOptions( + { listId: list.id }, + { + initialData: () => ({ + pages: [{ bookmarks: initialBookmarks, nextCursor, list }], + pageParams: [null], + }), + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + refetchOnMount: true, + }, + ), ); useEffect(() => { diff --git a/apps/web/components/settings/AISettings.tsx b/apps/web/components/settings/AISettings.tsx index 78e3ef56..6d8565da 100644 --- a/apps/web/components/settings/AISettings.tsx +++ b/apps/web/components/settings/AISettings.tsx @@ -40,10 +40,11 @@ import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useUserSettings } from "@/lib/userSettings"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Info, Plus, Save, Trash2 } from "lucide-react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; @@ -340,8 +341,9 @@ export function TagStyleSelector() { } export function PromptEditor() { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const form = useForm<z.infer<typeof zNewPromptSchema>>({ resolver: zodResolver(zNewPromptSchema), @@ -351,15 +353,16 @@ export function PromptEditor() { }, }); - const { mutateAsync: createPrompt, isPending: isCreating } = - api.prompts.create.useMutation({ + const { mutateAsync: createPrompt, isPending: isCreating } = useMutation( + api.prompts.create.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been created!", }); - apiUtils.prompts.list.invalidate(); + queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, - }); + }), + ); return ( <Form {...form}> @@ -441,26 +444,29 @@ export function PromptEditor() { } export function PromptRow({ prompt }: { prompt: ZPrompt }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { mutateAsync: updatePrompt, isPending: isUpdating } = - api.prompts.update.useMutation({ + const queryClient = useQueryClient(); + const { mutateAsync: updatePrompt, isPending: isUpdating } = useMutation( + api.prompts.update.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been updated!", }); - apiUtils.prompts.list.invalidate(); + queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, - }); - const { mutate: deletePrompt, isPending: isDeleting } = - api.prompts.delete.useMutation({ + }), + ); + const { mutate: deletePrompt, isPending: isDeleting } = useMutation( + api.prompts.delete.mutationOptions({ onSuccess: () => { toast({ description: "Prompt has been deleted!", }); - apiUtils.prompts.list.invalidate(); + queryClient.invalidateQueries(api.prompts.list.pathFilter()); }, - }); + }), + ); const form = useForm<z.infer<typeof zUpdatePromptSchema>>({ resolver: zodResolver(zUpdatePromptSchema), @@ -574,8 +580,11 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) { } export function TaggingRules() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: prompts, isLoading } = api.prompts.list.useQuery(); + const { data: prompts, isLoading } = useQuery( + api.prompts.list.queryOptions(), + ); return ( <SettingsSection @@ -601,8 +610,9 @@ export function TaggingRules() { } export function PromptDemo() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: prompts } = api.prompts.list.useQuery(); + const { data: prompts } = useQuery(api.prompts.list.queryOptions()); const settings = useUserSettings(); const clientConfig = useClientConfig(); diff --git a/apps/web/components/settings/AddApiKey.tsx b/apps/web/components/settings/AddApiKey.tsx index 9ef9047c..11107333 100644 --- a/apps/web/components/settings/AddApiKey.tsx +++ b/apps/web/components/settings/AddApiKey.tsx @@ -26,8 +26,9 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { PlusCircle } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -35,23 +36,26 @@ import { z } from "zod"; import ApiKeySuccess from "./ApiKeySuccess"; function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) { + const api = useTRPC(); const { t } = useTranslation(); const formSchema = z.object({ name: z.string(), }); const router = useRouter(); - const mutator = api.apiKeys.create.useMutation({ - onSuccess: (resp) => { - onSuccess(resp.key); - router.refresh(); - }, - onError: () => { - toast({ - description: t("common.something_went_wrong"), - variant: "destructive", - }); - }, - }); + const mutator = useMutation( + api.apiKeys.create.mutationOptions({ + onSuccess: (resp) => { + onSuccess(resp.key); + router.refresh(); + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + }, + }), + ); const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), diff --git a/apps/web/components/settings/BackupSettings.tsx b/apps/web/components/settings/BackupSettings.tsx index ad2b66c6..78418f6c 100644 --- a/apps/web/components/settings/BackupSettings.tsx +++ b/apps/web/components/settings/BackupSettings.tsx @@ -24,9 +24,10 @@ import { import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { useUserSettings } from "@/lib/userSettings"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle, Download, @@ -207,16 +208,17 @@ function BackupConfigurationForm() { } function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); - const { mutate: deleteBackup, isPending: isDeleting } = - api.backups.delete.useMutation({ + const { mutate: deleteBackup, isPending: isDeleting } = useMutation( + api.backups.delete.mutationOptions({ onSuccess: () => { toast({ description: t("settings.backups.toasts.backup_deleted"), }); - apiUtils.backups.list.invalidate(); + queryClient.invalidateQueries(api.backups.list.pathFilter()); }, onError: (error) => { toast({ @@ -224,7 +226,8 @@ function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) { variant: "destructive", }); }, - }); + }), + ); const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; @@ -330,25 +333,28 @@ function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) { } function BackupsList() { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { data: backups, isLoading } = api.backups.list.useQuery(undefined, { - refetchInterval: (query) => { - const data = query.state.data; - // Poll every 3 seconds if there's a pending backup, otherwise don't poll - return data?.backups.some((backup) => backup.status === "pending") - ? 3000 - : false; - }, - }); + const queryClient = useQueryClient(); + const { data: backups, isLoading } = useQuery( + api.backups.list.queryOptions(undefined, { + refetchInterval: (query) => { + const data = query.state.data; + // Poll every 3 seconds if there's a pending backup, otherwise don't poll + return data?.backups.some((backup) => backup.status === "pending") + ? 3000 + : false; + }, + }), + ); - const { mutate: triggerBackup, isPending: isTriggering } = - api.backups.triggerBackup.useMutation({ + const { mutate: triggerBackup, isPending: isTriggering } = useMutation( + api.backups.triggerBackup.mutationOptions({ onSuccess: () => { toast({ description: t("settings.backups.toasts.backup_queued"), }); - apiUtils.backups.list.invalidate(); + queryClient.invalidateQueries(api.backups.list.pathFilter()); }, onError: (error) => { toast({ @@ -356,7 +362,8 @@ function BackupsList() { variant: "destructive", }); }, - }); + }), + ); return ( <div className="rounded-md border bg-background p-4"> diff --git a/apps/web/components/settings/ChangePassword.tsx b/apps/web/components/settings/ChangePassword.tsx index 1da92267..ae764dd3 100644 --- a/apps/web/components/settings/ChangePassword.tsx +++ b/apps/web/components/settings/ChangePassword.tsx @@ -14,8 +14,9 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { Eye, EyeOff, Lock } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -25,6 +26,7 @@ import { Button } from "../ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; export function ChangePassword() { + const api = useTRPC(); const { t } = useTranslation(); const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); @@ -38,22 +40,27 @@ export function ChangePassword() { }, }); - const mutator = api.users.changePassword.useMutation({ - onSuccess: () => { - toast({ description: "Password changed successfully" }); - form.reset(); - }, - onError: (e) => { - if (e.data?.code == "UNAUTHORIZED") { - toast({ - description: "Your current password is incorrect", - variant: "destructive", - }); - } else { - toast({ description: "Something went wrong", variant: "destructive" }); - } - }, - }); + const mutator = useMutation( + api.users.changePassword.mutationOptions({ + onSuccess: () => { + toast({ description: "Password changed successfully" }); + form.reset(); + }, + onError: (e) => { + if (e.data?.code == "UNAUTHORIZED") { + toast({ + description: "Your current password is incorrect", + variant: "destructive", + }); + } else { + toast({ + description: "Something went wrong", + variant: "destructive", + }); + } + }, + }), + ); async function onSubmit(value: z.infer<typeof zChangePasswordSchema>) { mutator.mutate({ diff --git a/apps/web/components/settings/DeleteApiKey.tsx b/apps/web/components/settings/DeleteApiKey.tsx index b26b40e6..78a895ac 100644 --- a/apps/web/components/settings/DeleteApiKey.tsx +++ b/apps/web/components/settings/DeleteApiKey.tsx @@ -5,7 +5,8 @@ import { ActionButton } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { Button } from "@/components/ui/button"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { Trash } from "lucide-react"; import { toast } from "sonner"; @@ -16,14 +17,17 @@ export default function DeleteApiKey({ name: string; id: string; }) { + const api = useTRPC(); const { t } = useTranslation(); const router = useRouter(); - const mutator = api.apiKeys.revoke.useMutation({ - onSuccess: () => { - toast.success("Key was successfully deleted"); - router.refresh(); - }, - }); + const mutator = useMutation( + api.apiKeys.revoke.mutationOptions({ + onSuccess: () => { + toast.success("Key was successfully deleted"); + router.refresh(); + }, + }), + ); return ( <ActionConfirmingDialog diff --git a/apps/web/components/settings/FeedSettings.tsx b/apps/web/components/settings/FeedSettings.tsx index a49bb0b2..acf947a3 100644 --- a/apps/web/components/settings/FeedSettings.tsx +++ b/apps/web/components/settings/FeedSettings.tsx @@ -16,9 +16,10 @@ import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowDownToLine, CheckCircle, @@ -61,9 +62,10 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; export function FeedsEditorDialog() { + const api = useTRPC(); const { t } = useTranslation(); const [open, setOpen] = React.useState(false); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const form = useForm<z.infer<typeof zNewFeedSchema>>({ resolver: zodResolver(zNewFeedSchema), @@ -81,16 +83,17 @@ export function FeedsEditorDialog() { } }, [open]); - const { mutateAsync: createFeed, isPending: isCreating } = - api.feeds.create.useMutation({ + const { mutateAsync: createFeed, isPending: isCreating } = useMutation( + api.feeds.create.mutationOptions({ onSuccess: () => { toast({ description: "Feed has been created!", }); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); setOpen(false); }, - }); + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> @@ -191,8 +194,9 @@ export function FeedsEditorDialog() { } export function EditFeedDialog({ feed }: { feed: ZFeed }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -204,16 +208,17 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { }); } }, [open]); - const { mutateAsync: updateFeed, isPending: isUpdating } = - api.feeds.update.useMutation({ + const { mutateAsync: updateFeed, isPending: isUpdating } = useMutation( + api.feeds.update.mutationOptions({ onSuccess: () => { toast({ description: "Feed has been updated!", }); setOpen(false); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); }, - }); + }), + ); const form = useForm<z.infer<typeof zUpdateFeedSchema>>({ resolver: zodResolver(zUpdateFeedSchema), defaultValues: { @@ -339,44 +344,49 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { } export function FeedRow({ feed }: { feed: ZFeed }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { mutate: deleteFeed, isPending: isDeleting } = - api.feeds.delete.useMutation({ + const queryClient = useQueryClient(); + const { mutate: deleteFeed, isPending: isDeleting } = useMutation( + api.feeds.delete.mutationOptions({ onSuccess: () => { toast({ description: "Feed has been deleted!", }); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); }, - }); + }), + ); - const { mutate: fetchNow, isPending: isFetching } = - api.feeds.fetchNow.useMutation({ + const { mutate: fetchNow, isPending: isFetching } = useMutation( + api.feeds.fetchNow.mutationOptions({ onSuccess: () => { toast({ description: "Feed fetch has been enqueued!", }); - apiUtils.feeds.list.invalidate(); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); }, - }); + }), + ); - const { mutate: updateFeedEnabled } = api.feeds.update.useMutation({ - onSuccess: () => { - toast({ - description: feed.enabled - ? t("settings.feeds.feed_disabled") - : t("settings.feeds.feed_enabled"), - }); - apiUtils.feeds.list.invalidate(); - }, - onError: (error) => { - toast({ - description: `Error: ${error.message}`, - variant: "destructive", - }); - }, - }); + const { mutate: updateFeedEnabled } = useMutation( + api.feeds.update.mutationOptions({ + onSuccess: () => { + toast({ + description: feed.enabled + ? t("settings.feeds.feed_disabled") + : t("settings.feeds.feed_enabled"), + }); + queryClient.invalidateQueries(api.feeds.list.pathFilter()); + }, + onError: (error) => { + toast({ + description: `Error: ${error.message}`, + variant: "destructive", + }); + }, + }), + ); const handleToggle = (checked: boolean) => { updateFeedEnabled({ feedId: feed.id, enabled: checked }); @@ -456,8 +466,9 @@ export function FeedRow({ feed }: { feed: ZFeed }) { } export default function FeedSettings() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: feeds, isLoading } = api.feeds.list.useQuery(); + const { data: feeds, isLoading } = useQuery(api.feeds.list.queryOptions()); return ( <> <div className="rounded-md border bg-background p-4"> diff --git a/apps/web/components/settings/RegenerateApiKey.tsx b/apps/web/components/settings/RegenerateApiKey.tsx index f4914598..9ee0e216 100644 --- a/apps/web/components/settings/RegenerateApiKey.tsx +++ b/apps/web/components/settings/RegenerateApiKey.tsx @@ -16,7 +16,8 @@ import { } from "@/components/ui/dialog"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; import { RefreshCcw } from "lucide-react"; import ApiKeySuccess from "./ApiKeySuccess"; @@ -28,25 +29,28 @@ export default function RegenerateApiKey({ id: string; name: string; }) { + const api = useTRPC(); const { t } = useTranslation(); const router = useRouter(); const [key, setKey] = useState<string | undefined>(undefined); const [dialogOpen, setDialogOpen] = useState<boolean>(false); - const mutator = api.apiKeys.regenerate.useMutation({ - onSuccess: (resp) => { - setKey(resp.key); - router.refresh(); - }, - onError: () => { - toast({ - description: t("common.something_went_wrong"), - variant: "destructive", - }); - setDialogOpen(false); - }, - }); + const mutator = useMutation( + api.apiKeys.regenerate.mutationOptions({ + onSuccess: (resp) => { + setKey(resp.key); + router.refresh(); + }, + onError: () => { + toast({ + description: t("common.something_went_wrong"), + variant: "destructive", + }); + setDialogOpen(false); + }, + }), + ); const handleRegenerate = () => { mutator.mutate({ id }); diff --git a/apps/web/components/settings/SubscriptionSettings.tsx b/apps/web/components/settings/SubscriptionSettings.tsx index 03337c1b..3799d08b 100644 --- a/apps/web/components/settings/SubscriptionSettings.tsx +++ b/apps/web/components/settings/SubscriptionSettings.tsx @@ -3,7 +3,8 @@ import { useEffect } from "react"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { CreditCard, Loader2 } from "lucide-react"; import { Alert, AlertDescription } from "../ui/alert"; @@ -19,24 +20,27 @@ import { import { Skeleton } from "../ui/skeleton"; export default function SubscriptionSettings() { + const api = useTRPC(); const { t } = useTranslation(); const { data: subscriptionStatus, refetch, isLoading: isQueryLoading, - } = api.subscriptions.getSubscriptionStatus.useQuery(); + } = useQuery(api.subscriptions.getSubscriptionStatus.queryOptions()); - const { data: subscriptionPrice } = - api.subscriptions.getSubscriptionPrice.useQuery(); + const { data: subscriptionPrice } = useQuery( + api.subscriptions.getSubscriptionPrice.queryOptions(), + ); - const { mutate: syncStripeState } = - api.subscriptions.syncWithStripe.useMutation({ + const { mutate: syncStripeState } = useMutation( + api.subscriptions.syncWithStripe.mutationOptions({ onSuccess: () => { refetch(); }, - }); - const createCheckoutSession = - api.subscriptions.createCheckoutSession.useMutation({ + }), + ); + const createCheckoutSession = useMutation( + api.subscriptions.createCheckoutSession.mutationOptions({ onSuccess: (resp) => { if (resp.url) { window.location.href = resp.url; @@ -48,9 +52,10 @@ export default function SubscriptionSettings() { variant: "destructive", }); }, - }); - const createPortalSession = api.subscriptions.createPortalSession.useMutation( - { + }), + ); + const createPortalSession = useMutation( + api.subscriptions.createPortalSession.mutationOptions({ onSuccess: (resp) => { if (resp.url) { window.location.href = resp.url; @@ -62,7 +67,7 @@ export default function SubscriptionSettings() { variant: "destructive", }); }, - }, + }), ); const isLoading = diff --git a/apps/web/components/settings/WebhookSettings.tsx b/apps/web/components/settings/WebhookSettings.tsx index 73671787..89f11b66 100644 --- a/apps/web/components/settings/WebhookSettings.tsx +++ b/apps/web/components/settings/WebhookSettings.tsx @@ -14,8 +14,9 @@ import { FullPageSpinner } from "@/components/ui/full-page-spinner"; import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Edit, KeyRound, @@ -56,9 +57,10 @@ import { import { WebhookEventSelector } from "./WebhookEventSelector"; export function WebhooksEditorDialog() { + const api = useTRPC(); const { t } = useTranslation(); const [open, setOpen] = React.useState(false); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const form = useForm<z.infer<typeof zNewWebhookSchema>>({ resolver: zodResolver(zNewWebhookSchema), @@ -75,16 +77,17 @@ export function WebhooksEditorDialog() { } }, [open]); - const { mutateAsync: createWebhook, isPending: isCreating } = - api.webhooks.create.useMutation({ + const { mutateAsync: createWebhook, isPending: isCreating } = useMutation( + api.webhooks.create.mutationOptions({ onSuccess: () => { toast({ description: "Webhook has been created!", }); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); setOpen(false); }, - }); + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> @@ -179,8 +182,9 @@ export function WebhooksEditorDialog() { } export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -191,16 +195,17 @@ export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) { }); } }, [open]); - const { mutateAsync: updateWebhook, isPending: isUpdating } = - api.webhooks.update.useMutation({ + const { mutateAsync: updateWebhook, isPending: isUpdating } = useMutation( + api.webhooks.update.mutationOptions({ onSuccess: () => { toast({ description: "Webhook has been updated!", }); setOpen(false); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); }, - }); + }), + ); const updateSchema = zUpdateWebhookSchema.required({ events: true, url: true, @@ -302,8 +307,9 @@ export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) { } export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); React.useEffect(() => { if (open) { @@ -331,16 +337,17 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) { }, }); - const { mutateAsync: updateWebhook, isPending: isUpdating } = - api.webhooks.update.useMutation({ + const { mutateAsync: updateWebhook, isPending: isUpdating } = useMutation( + api.webhooks.update.mutationOptions({ onSuccess: () => { toast({ description: "Webhook token has been updated!", }); setOpen(false); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); }, - }); + }), + ); return ( <Dialog open={open} onOpenChange={setOpen}> @@ -432,17 +439,19 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) { } export function WebhookRow({ webhook }: { webhook: ZWebhook }) { + const api = useTRPC(); const { t } = useTranslation(); - const apiUtils = api.useUtils(); - const { mutate: deleteWebhook, isPending: isDeleting } = - api.webhooks.delete.useMutation({ + const queryClient = useQueryClient(); + const { mutate: deleteWebhook, isPending: isDeleting } = useMutation( + api.webhooks.delete.mutationOptions({ onSuccess: () => { toast({ description: "Webhook has been deleted!", }); - apiUtils.webhooks.list.invalidate(); + queryClient.invalidateQueries(api.webhooks.list.pathFilter()); }, - }); + }), + ); return ( <TableRow> @@ -479,8 +488,11 @@ export function WebhookRow({ webhook }: { webhook: ZWebhook }) { } export default function WebhookSettings() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: webhooks, isLoading } = api.webhooks.list.useQuery(); + const { data: webhooks, isLoading } = useQuery( + api.webhooks.list.queryOptions(), + ); return ( <div className="rounded-md border bg-background p-4"> <div className="flex flex-col gap-2"> diff --git a/apps/web/components/signin/ForgotPasswordForm.tsx b/apps/web/components/signin/ForgotPasswordForm.tsx index 29d55f2b..6349c300 100644 --- a/apps/web/components/signin/ForgotPasswordForm.tsx +++ b/apps/web/components/signin/ForgotPasswordForm.tsx @@ -20,8 +20,9 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, CheckCircle } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -32,6 +33,7 @@ const forgotPasswordSchema = z.object({ }); export default function ForgotPasswordForm() { + const api = useTRPC(); const [isSubmitted, setIsSubmitted] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const router = useRouter(); @@ -40,7 +42,9 @@ export default function ForgotPasswordForm() { resolver: zodResolver(forgotPasswordSchema), }); - const forgotPasswordMutation = api.users.forgotPassword.useMutation(); + const forgotPasswordMutation = useMutation( + api.users.forgotPassword.mutationOptions(), + ); const onSubmit = async (values: z.infer<typeof forgotPasswordSchema>) => { try { diff --git a/apps/web/components/signin/ResetPasswordForm.tsx b/apps/web/components/signin/ResetPasswordForm.tsx index d4d8a285..b6e5f5ae 100644 --- a/apps/web/components/signin/ResetPasswordForm.tsx +++ b/apps/web/components/signin/ResetPasswordForm.tsx @@ -20,8 +20,9 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, CheckCircle } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -44,6 +45,7 @@ interface ResetPasswordFormProps { } export default function ResetPasswordForm({ token }: ResetPasswordFormProps) { + const api = useTRPC(); const [isSuccess, setIsSuccess] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const router = useRouter(); @@ -52,7 +54,9 @@ export default function ResetPasswordForm({ token }: ResetPasswordFormProps) { resolver: zodResolver(resetPasswordSchema), }); - const resetPasswordMutation = api.users.resetPassword.useMutation(); + const resetPasswordMutation = useMutation( + api.users.resetPassword.mutationOptions(), + ); const onSubmit = async (values: z.infer<typeof resetPasswordSchema>) => { try { diff --git a/apps/web/components/signup/SignUpForm.tsx b/apps/web/components/signup/SignUpForm.tsx index 845c6d1e..77d6ba02 100644 --- a/apps/web/components/signup/SignUpForm.tsx +++ b/apps/web/components/signup/SignUpForm.tsx @@ -25,9 +25,10 @@ import { import { Input } from "@/components/ui/input"; import { signIn } from "@/lib/auth/client"; import { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; import { Turnstile } from "@marsidev/react-turnstile"; +import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { AlertCircle, UserX } from "lucide-react"; import { useForm } from "react-hook-form"; @@ -38,6 +39,7 @@ import { zSignUpSchema } from "@karakeep/shared/types/users"; const VERIFY_EMAIL_ERROR = "Please verify your email address before signing in"; export default function SignUpForm() { + const api = useTRPC(); const form = useForm<z.infer<typeof zSignUpSchema>>({ resolver: zodResolver(zSignUpSchema), defaultValues: { @@ -54,7 +56,7 @@ export default function SignUpForm() { const turnstileSiteKey = clientConfig.turnstile?.siteKey; const turnstileRef = useRef<TurnstileInstance>(null); - const createUserMutation = api.users.create.useMutation(); + const createUserMutation = useMutation(api.users.create.mutationOptions()); if ( clientConfig.auth.disableSignups || diff --git a/apps/web/components/subscription/QuotaProgress.tsx b/apps/web/components/subscription/QuotaProgress.tsx index 525eae8f..b36baec3 100644 --- a/apps/web/components/subscription/QuotaProgress.tsx +++ b/apps/web/components/subscription/QuotaProgress.tsx @@ -1,7 +1,8 @@ "use client"; import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; import { Database, HardDrive } from "lucide-react"; import { @@ -110,9 +111,11 @@ function QuotaProgressItem({ } export function QuotaProgress() { + const api = useTRPC(); const { t } = useTranslation(); - const { data: quotaUsage, isLoading } = - api.subscriptions.getQuotaUsage.useQuery(); + const { data: quotaUsage, isLoading } = useQuery( + api.subscriptions.getQuotaUsage.queryOptions(), + ); if (isLoading) { return ( diff --git a/apps/web/components/utils/ValidAccountCheck.tsx b/apps/web/components/utils/ValidAccountCheck.tsx index 5ca5fd5c..49f42f9c 100644 --- a/apps/web/components/utils/ValidAccountCheck.tsx +++ b/apps/web/components/utils/ValidAccountCheck.tsx @@ -2,22 +2,26 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; +import { useQuery } from "@tanstack/react-query"; /** * This component is used to address a confusion when the JWT token exists but the user no longer exists in the database. * So this component synchronusly checks if the user is still valid and if not, signs out the user. */ export default function ValidAccountCheck() { + const api = useTRPC(); const router = useRouter(); - const { error } = api.users.whoami.useQuery(undefined, { - retry: (_failureCount, error) => { - if (error.data?.code === "UNAUTHORIZED") { - return false; - } - return true; - }, - }); + const { error } = useQuery( + api.users.whoami.queryOptions(undefined, { + retry: (_failureCount, error) => { + if (error.data?.code === "UNAUTHORIZED") { + return false; + } + return true; + }, + }), + ); useEffect(() => { if (error?.data?.code === "UNAUTHORIZED") { router.push("/logout"); diff --git a/apps/web/components/wrapped/WrappedModal.tsx b/apps/web/components/wrapped/WrappedModal.tsx index 25e376b0..93635d36 100644 --- a/apps/web/components/wrapped/WrappedModal.tsx +++ b/apps/web/components/wrapped/WrappedModal.tsx @@ -7,8 +7,9 @@ import { DialogOverlay, DialogTitle, } from "@/components/ui/dialog"; -import { api } from "@/lib/trpc"; +import { useTRPC } from "@/lib/trpc"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; +import { useQuery } from "@tanstack/react-query"; import { Loader2, X } from "lucide-react"; import { ShareButton } from "./ShareButton"; @@ -20,13 +21,18 @@ interface WrappedModalProps { } export function WrappedModal({ open, onClose }: WrappedModalProps) { + const api = useTRPC(); const contentRef = useRef<HTMLDivElement | null>(null); - const { data: stats, isLoading } = api.users.wrapped.useQuery(undefined, { - enabled: open, - }); - const { data: whoami } = api.users.whoami.useQuery(undefined, { - enabled: open, - }); + const { data: stats, isLoading } = useQuery( + api.users.wrapped.queryOptions(undefined, { + enabled: open, + }), + ); + const { data: whoami } = useQuery( + api.users.whoami.queryOptions(undefined, { + enabled: open, + }), + ); return ( <Dialog open={open} onOpenChange={onClose}> diff --git a/apps/web/lib/hooks/bookmark-search.ts b/apps/web/lib/hooks/bookmark-search.ts index f94e4691..0b6b229d 100644 --- a/apps/web/lib/hooks/bookmark-search.ts +++ b/apps/web/lib/hooks/bookmark-search.ts @@ -1,8 +1,8 @@ import { useEffect, useMemo, useRef } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useSortOrderStore } from "@/lib/store/useSortOrderStore"; -import { api } from "@/lib/trpc"; -import { keepPreviousData } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; @@ -55,6 +55,7 @@ export function useDoBookmarkSearch() { } export function useBookmarkSearch() { + const api = useTRPC(); const { searchQuery } = useSearchQuery(); const sortOrder = useSortOrderStore((state) => state.sortOrder); @@ -67,17 +68,19 @@ export function useBookmarkSearch() { fetchNextPage, isFetchingNextPage, refetch, - } = api.bookmarks.searchBookmarks.useInfiniteQuery( - { - text: searchQuery, - sortOrder, - }, - { - placeholderData: keepPreviousData, - gcTime: 0, - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, + } = useInfiniteQuery( + api.bookmarks.searchBookmarks.infiniteQueryOptions( + { + text: searchQuery, + sortOrder, + }, + { + placeholderData: keepPreviousData, + gcTime: 0, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ), ); useEffect(() => { diff --git a/apps/web/lib/hooks/useBookmarkImport.ts b/apps/web/lib/hooks/useBookmarkImport.ts index f2522d9a..71d06522 100644 --- a/apps/web/lib/hooks/useBookmarkImport.ts +++ b/apps/web/lib/hooks/useBookmarkImport.ts @@ -3,7 +3,8 @@ import { useState } from "react"; import { toast } from "@/components/ui/sonner"; import { useTranslation } from "@/lib/i18n/client"; -import { useMutation } from "@tanstack/react-query"; +import { useTRPC } from "@/lib/trpc"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useCreateBookmarkWithPostHook, @@ -13,7 +14,6 @@ import { useAddBookmarkToList, useCreateBookmarkList, } from "@karakeep/shared-react/hooks/lists"; -import { api } from "@karakeep/shared-react/trpc"; import { importBookmarksFromFile, ImportSource, @@ -34,13 +34,14 @@ export interface ImportProgress { export function useBookmarkImport() { const { t } = useTranslation(); + const api = useTRPC(); const [importProgress, setImportProgress] = useState<ImportProgress | null>( null, ); const [quotaError, setQuotaError] = useState<string | null>(null); - const apiUtils = api.useUtils(); + const queryClient = useQueryClient(); const { mutateAsync: createImportSession } = useCreateImportSession(); const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); const { mutateAsync: createList } = useCreateBookmarkList(); @@ -65,8 +66,9 @@ export function useBookmarkImport() { // Check quota before proceeding if (bookmarkCount > 0) { - const quotaUsage = - await apiUtils.client.subscriptions.getQuotaUsage.query(); + const quotaUsage = await queryClient.fetchQuery( + api.subscriptions.getQuotaUsage.queryOptions(), + ); if ( !quotaUsage.bookmarks.unlimited && diff --git a/apps/web/lib/hooks/useImportSessions.ts b/apps/web/lib/hooks/useImportSessions.ts index 1482f33d..133bb29b 100644 --- a/apps/web/lib/hooks/useImportSessions.ts +++ b/apps/web/lib/hooks/useImportSessions.ts @@ -1,62 +1,79 @@ "use client"; import { toast } from "@/components/ui/sonner"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { api } from "@karakeep/shared-react/trpc"; +import { useTRPC } from "@karakeep/shared-react/trpc"; export function useCreateImportSession() { - const apiUtils = api.useUtils(); - - return api.importSessions.createImportSession.useMutation({ - onSuccess: () => { - apiUtils.importSessions.listImportSessions.invalidate(); - }, - onError: (error) => { - toast({ - description: error.message || "Failed to create import session", - variant: "destructive", - }); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.importSessions.createImportSession.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries( + api.importSessions.listImportSessions.pathFilter(), + ); + }, + onError: (error) => { + toast({ + description: error.message || "Failed to create import session", + variant: "destructive", + }); + }, + }), + ); } export function useListImportSessions() { - return api.importSessions.listImportSessions.useQuery( - {}, - { - select: (data) => data.sessions, - }, + const api = useTRPC(); + return useQuery( + api.importSessions.listImportSessions.queryOptions( + {}, + { + select: (data) => data.sessions, + }, + ), ); } export function useImportSessionStats(importSessionId: string) { - return api.importSessions.getImportSessionStats.useQuery( - { - importSessionId, - }, - { - refetchInterval: 5000, // Refetch every 5 seconds to show progress - enabled: !!importSessionId, - }, + const api = useTRPC(); + return useQuery( + api.importSessions.getImportSessionStats.queryOptions( + { + importSessionId, + }, + { + refetchInterval: 5000, // Refetch every 5 seconds to show progress + enabled: !!importSessionId, + }, + ), ); } export function useDeleteImportSession() { - const apiUtils = api.useUtils(); - - return api.importSessions.deleteImportSession.useMutation({ - onSuccess: () => { - apiUtils.importSessions.listImportSessions.invalidate(); - toast({ - description: "Import session deleted successfully", - variant: "default", - }); - }, - onError: (error) => { - toast({ - description: error.message || "Failed to delete import session", - variant: "destructive", - }); - }, - }); + const api = useTRPC(); + const queryClient = useQueryClient(); + + return useMutation( + api.importSessions.deleteImportSession.mutationOptions({ + onSuccess: () => { + queryClient.invalidateQueries( + api.importSessions.listImportSessions.pathFilter(), + ); + toast({ + description: "Import session deleted successfully", + variant: "default", + }); + }, + onError: (error) => { + toast({ + description: error.message || "Failed to delete import session", + variant: "destructive", + }); + }, + }), + ); } diff --git a/apps/web/lib/providers.tsx b/apps/web/lib/providers.tsx index dd4e62e7..a56b77c7 100644 --- a/apps/web/lib/providers.tsx +++ b/apps/web/lib/providers.tsx @@ -7,14 +7,15 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { Session, SessionProvider } from "@/lib/auth/client"; import { UserLocalSettingsCtx } from "@/lib/userLocalSettings/bookmarksLayout"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { httpBatchLink, loggerLink } from "@trpc/client"; +import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client"; import superjson from "superjson"; import type { ClientConfig } from "@karakeep/shared/config"; +import type { AppRouter } from "@karakeep/trpc/routers/_app"; import { ClientConfigCtx } from "./clientConfig"; import CustomI18nextProvider from "./i18n/provider"; -import { api } from "./trpc"; +import { TRPCProvider } from "./trpc"; function makeQueryClient() { return new QueryClient({ @@ -58,7 +59,7 @@ export default function Providers({ const queryClient = getQueryClient(); const [trpcClient] = useState(() => - api.createClient({ + createTRPCClient<AppRouter>({ links: [ loggerLink({ enabled: (op) => @@ -79,8 +80,8 @@ export default function Providers({ <ClientConfigCtx.Provider value={clientConfig}> <UserLocalSettingsCtx.Provider value={userLocalSettings}> <SessionProvider session={session}> - <api.Provider client={trpcClient} queryClient={queryClient}> - <QueryClientProvider client={queryClient}> + <QueryClientProvider client={queryClient}> + <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}> <CustomI18nextProvider lang={userLocalSettings.lang}> <ThemeProvider attribute="class" @@ -93,8 +94,8 @@ export default function Providers({ </TooltipProvider> </ThemeProvider> </CustomI18nextProvider> - </QueryClientProvider> - </api.Provider> + </TRPCProvider> + </QueryClientProvider> </SessionProvider> </UserLocalSettingsCtx.Provider> </ClientConfigCtx.Provider> diff --git a/apps/web/lib/trpc.tsx b/apps/web/lib/trpc.tsx index 1478684f..d0b27ad6 100644 --- a/apps/web/lib/trpc.tsx +++ b/apps/web/lib/trpc.tsx @@ -1,7 +1,5 @@ "use client"; -import { createTRPCReact } from "@trpc/react-query"; - -import type { AppRouter } from "@karakeep/trpc/routers/_app"; - -export const api = createTRPCReact<AppRouter>(); +// Re-export from shared-react to ensure there's only one TRPCProvider context +// This is necessary because the hooks in shared-react use useTRPC from shared-react +export { TRPCProvider, useTRPC } from "@karakeep/shared-react/trpc"; diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx index 4789e2ba..54518884 100644 --- a/apps/web/lib/userSettings.tsx +++ b/apps/web/lib/userSettings.tsx @@ -1,10 +1,11 @@ "use client"; import { createContext, useContext } from "react"; +import { useQuery } from "@tanstack/react-query"; import { ZUserSettings } from "@karakeep/shared/types/users"; -import { api } from "./trpc"; +import { useTRPC } from "./trpc"; export const UserSettingsContext = createContext<ZUserSettings>({ bookmarkClickAction: "open_original_link", @@ -29,9 +30,12 @@ export function UserSettingsContextProvider({ userSettings: ZUserSettings; children: React.ReactNode; }) { - const { data } = api.users.settings.useQuery(undefined, { - initialData: userSettings, - }); + const api = useTRPC(); + const { data } = useQuery( + api.users.settings.queryOptions(undefined, { + initialData: userSettings, + }), + ); return ( <UserSettingsContext.Provider value={data}> diff --git a/apps/web/package.json b/apps/web/package.json index 154853fc..91c257e1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -55,9 +55,9 @@ "@svgr/webpack": "^8.1.0", "@tanstack/react-query": "5.90.2", "@tanstack/react-query-devtools": "5.90.2", - "@trpc/client": "^11.4.3", - "@trpc/react-query": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", + "@trpc/tanstack-react-query": "^11.9.0", "cheerio": "^1.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", |
