aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-16 21:26:24 +0000
committerMohamedBassem <me@mbassem.com>2024-02-17 11:49:39 +0000
commit9235e9a6fbb364713105137b6bf5bba9d81ecd4c (patch)
tree80bc7871ca2b043c110c61b796c46af91cb26e2f
parent6febe13b3f4ad4eff3f205ece445b3577255bf41 (diff)
downloadkarakeep-9235e9a6fbb364713105137b6bf5bba9d81ecd4c.tar.zst
ui: Change action buttons to show a spinner when the request is loading
-rw-r--r--packages/web/app/dashboard/bookmarks/components/AddLink.tsx6
-rw-r--r--packages/web/app/dashboard/settings/components/AddApiKey.tsx28
-rw-r--r--packages/web/app/dashboard/settings/components/DeleteApiKey.tsx23
-rw-r--r--packages/web/components/ui/action-button.tsx25
-rw-r--r--packages/web/components/ui/spinner.tsx20
5 files changed, 84 insertions, 18 deletions
diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
index 34f043e7..0ef4d193 100644
--- a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Plus } from "lucide-react";
@@ -10,6 +9,7 @@ import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
+import { ActionButton } from "@/components/ui/action-button";
const formSchema = z.object({
url: z.string().url({ message: "The link must be a valid URL" }),
@@ -62,9 +62,9 @@ export default function AddLink() {
);
}}
/>
- <Button type="submit">
+ <ActionButton type="submit" loading={bookmarkLinkMutator.isPending}>
<Plus />
- </Button>
+ </ActionButton>
</div>
</form>
</Form>
diff --git a/packages/web/app/dashboard/settings/components/AddApiKey.tsx b/packages/web/app/dashboard/settings/components/AddApiKey.tsx
index c438f4b1..27111b87 100644
--- a/packages/web/app/dashboard/settings/components/AddApiKey.tsx
+++ b/packages/web/app/dashboard/settings/components/AddApiKey.tsx
@@ -29,9 +29,19 @@ import { useForm, SubmitErrorHandler } from "react-hook-form";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
import { useState } from "react";
-import { Copy } from "lucide-react";
+import { Check, Copy } from "lucide-react";
+import LoadingSpinner from "@/components/ui/spinner";
+import { ActionButton } from "@/components/ui/action-button";
function ApiKeySuccess({ apiKey }: { apiKey: string }) {
+ const [isCopied, setCopied] = useState(false);
+
+ const onCopy = () => {
+ navigator.clipboard.writeText(apiKey);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
return (
<div>
<div className="py-4">
@@ -40,8 +50,12 @@ function ApiKeySuccess({ apiKey }: { apiKey: string }) {
</div>
<div className="flex space-x-2 pt-2">
<Input value={apiKey} readOnly />
- <Button onClick={() => navigator.clipboard.writeText(apiKey)}>
- <Copy className="size-4" />
+ <Button onClick={onCopy}>
+ {!isCopied ? (
+ <Copy className="size-4" />
+ ) : (
+ <Check className="size-4" />
+ )}
</Button>
</div>
</div>
@@ -104,9 +118,13 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
);
}}
/>
- <Button className="h-full" type="submit">
+ <ActionButton
+ className="h-full"
+ type="submit"
+ loading={mutator.isPending}
+ >
Create
- </Button>
+ </ActionButton>
</form>
</Form>
);
diff --git a/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx b/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
index bc3e3c92..566136af 100644
--- a/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
+++ b/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx
@@ -16,6 +16,8 @@ import {
import { useRouter } from "next/navigation";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
+import { ActionButton } from "@/components/ui/action-button";
+import { useState } from "react";
export default function DeleteApiKey({
name,
@@ -24,18 +26,20 @@ export default function DeleteApiKey({
name: string;
id: string;
}) {
+ const [isDialogOpen, setDialogOpen] = useState(false);
const router = useRouter();
const mutator = api.apiKeys.revoke.useMutation({
onSuccess: () => {
toast({
description: "Key was successfully deleted",
});
+ setDialogOpen(false);
router.refresh();
},
});
return (
- <Dialog>
+ <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="destructive">
<Trash className="size-5" />
@@ -55,15 +59,14 @@ export default function DeleteApiKey({
Close
</Button>
</DialogClose>
- <DialogClose asChild>
- <Button
- type="button"
- variant="destructive"
- onClick={() => mutator.mutate({ id })}
- >
- Delete
- </Button>
- </DialogClose>
+ <ActionButton
+ type="button"
+ variant="destructive"
+ loading={mutator.isPending}
+ onClick={() => mutator.mutate({ id })}
+ >
+ Delete
+ </ActionButton>
</DialogFooter>
</DialogContent>
</Dialog>
diff --git a/packages/web/components/ui/action-button.tsx b/packages/web/components/ui/action-button.tsx
new file mode 100644
index 00000000..42e16f65
--- /dev/null
+++ b/packages/web/components/ui/action-button.tsx
@@ -0,0 +1,25 @@
+import { Button, ButtonProps } from "./button";
+import LoadingSpinner from "./spinner";
+
+export function ActionButton({
+ children,
+ loading,
+ spinner,
+ disabled,
+ ...props
+}: ButtonProps & {
+ loading: boolean;
+ spinner?: React.ReactNode;
+}) {
+ spinner ||= <LoadingSpinner />;
+ if (disabled !== undefined) {
+ disabled ||= loading;
+ } else if (loading) {
+ disabled = true;
+ }
+ return (
+ <Button {...props} disabled={disabled}>
+ {loading ? spinner : children}
+ </Button>
+ );
+}
diff --git a/packages/web/components/ui/spinner.tsx b/packages/web/components/ui/spinner.tsx
new file mode 100644
index 00000000..adcd2807
--- /dev/null
+++ b/packages/web/components/ui/spinner.tsx
@@ -0,0 +1,20 @@
+import { cn } from "@/lib/utils";
+
+export default function LoadingSpinner({ className }: { className?: string }) {
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className={cn("animate-spin", className)}
+ >
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+ </svg>
+ );
+}