aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/admin/AddUserDialog.tsx430
-rw-r--r--apps/web/components/admin/AdminNotices.tsx6
-rw-r--r--apps/web/components/admin/BackgroundJobs.tsx131
-rw-r--r--apps/web/components/admin/BasicStats.tsx13
-rw-r--r--apps/web/components/admin/BookmarkDebugger.tsx116
-rw-r--r--apps/web/components/admin/CreateInviteDialog.tsx44
-rw-r--r--apps/web/components/admin/InvitesList.tsx30
-rw-r--r--apps/web/components/admin/ResetPasswordDialog.tsx294
-rw-r--r--apps/web/components/admin/ServiceConnections.tsx12
-rw-r--r--apps/web/components/admin/UpdateUserDialog.tsx48
-rw-r--r--apps/web/components/admin/UserList.tsx27
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkCard.tsx32
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx22
-rw-r--r--apps/web/components/dashboard/bookmarks/BulkTagModal.tsx12
-rw-r--r--apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx11
-rw-r--r--apps/web/components/dashboard/bookmarks/ManageListsModal.tsx11
-rw-r--r--apps/web/components/dashboard/bookmarks/TagsEditor.tsx12
-rw-r--r--apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx28
-rw-r--r--apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx20
-rw-r--r--apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx16
-rw-r--r--apps/web/components/dashboard/feeds/FeedSelector.tsx12
-rw-r--r--apps/web/components/dashboard/highlights/AllHighlights.tsx48
-rw-r--r--apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx13
-rw-r--r--apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx54
-rw-r--r--apps/web/components/dashboard/lists/ListHeader.tsx38
-rw-r--r--apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx154
-rw-r--r--apps/web/components/dashboard/lists/PendingInvitationsCard.tsx83
-rw-r--r--apps/web/components/dashboard/lists/RssLink.tsx33
-rw-r--r--apps/web/components/dashboard/preview/BookmarkPreview.tsx32
-rw-r--r--apps/web/components/dashboard/preview/HighlightsBox.tsx9
-rw-r--r--apps/web/components/dashboard/preview/ReaderView.tsx19
-rw-r--r--apps/web/components/dashboard/search/useSearchAutocomplete.ts12
-rw-r--r--apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx11
-rw-r--r--apps/web/components/dashboard/tags/TagAutocomplete.tsx11
-rw-r--r--apps/web/components/invite/InviteAcceptForm.tsx10
-rw-r--r--apps/web/components/public/lists/PublicBookmarkGrid.tsx28
-rw-r--r--apps/web/components/settings/AISettings.tsx44
-rw-r--r--apps/web/components/settings/AddApiKey.tsx30
-rw-r--r--apps/web/components/settings/BackupSettings.tsx47
-rw-r--r--apps/web/components/settings/ChangePassword.tsx41
-rw-r--r--apps/web/components/settings/DeleteApiKey.tsx18
-rw-r--r--apps/web/components/settings/FeedSettings.tsx85
-rw-r--r--apps/web/components/settings/RegenerateApiKey.tsx32
-rw-r--r--apps/web/components/settings/SubscriptionSettings.tsx31
-rw-r--r--apps/web/components/settings/WebhookSettings.tsx56
-rw-r--r--apps/web/components/signin/ForgotPasswordForm.tsx8
-rw-r--r--apps/web/components/signin/ResetPasswordForm.tsx8
-rw-r--r--apps/web/components/signup/SignUpForm.tsx6
-rw-r--r--apps/web/components/subscription/QuotaProgress.tsx9
-rw-r--r--apps/web/components/utils/ValidAccountCheck.tsx22
-rw-r--r--apps/web/components/wrapped/WrappedModal.tsx20
51 files changed, 1310 insertions, 1029 deletions
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}>