diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-16 21:26:24 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-17 11:49:39 +0000 |
| commit | 9235e9a6fbb364713105137b6bf5bba9d81ecd4c (patch) | |
| tree | 80bc7871ca2b043c110c61b796c46af91cb26e2f /packages | |
| parent | 6febe13b3f4ad4eff3f205ece445b3577255bf41 (diff) | |
| download | karakeep-9235e9a6fbb364713105137b6bf5bba9d81ecd4c.tar.zst | |
ui: Change action buttons to show a spinner when the request is loading
Diffstat (limited to 'packages')
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> + ); +} |
