feat: add loading indicator to submit buttons

This commit is contained in:
trafficlunar 2025-04-23 18:38:21 +01:00
parent 99c3aa5add
commit e1d248853f
7 changed files with 53 additions and 33 deletions

View file

@ -7,6 +7,7 @@ import { createPortal } from "react-dom";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import LikeButton from "./like-button"; import LikeButton from "./like-button";
import SubmitButton from "./submit-button";
interface Props { interface Props {
miiId: number; miiId: number;
@ -20,7 +21,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes }: Props) {
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const submit = async () => { const handleSubmit = async () => {
const response = await fetch(`/api/mii/${miiId}/delete`, { method: "DELETE" }); const response = await fetch(`/api/mii/${miiId}/delete`, { method: "DELETE" });
if (!response.ok) { if (!response.ok) {
const { error } = await response.json(); const { error } = await response.json();
@ -92,9 +93,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes }: Props) {
<button onClick={close} className="pill button"> <button onClick={close} className="pill button">
Cancel Cancel
</button> </button>
<button onClick={submit} className="pill button !bg-red-400 !border-red-500 hover:!bg-red-500"> <SubmitButton onClick={handleSubmit} text="Delete" className="!bg-red-400 !border-red-500 hover:!bg-red-500" />
Delete
</button>
</div> </div>
</div> </div>
</div>, </div>,

View file

@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import SubmitButton from "../submit-button";
export default function DeleteAccount() { export default function DeleteAccount() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -11,7 +12,7 @@ export default function DeleteAccount() {
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const submit = async () => { const handleSubmit = async () => {
const response = await fetch("/api/auth/delete", { method: "DELETE" }); const response = await fetch("/api/auth/delete", { method: "DELETE" });
if (!response.ok) { if (!response.ok) {
const { error } = await response.json(); const { error } = await response.json();
@ -78,9 +79,7 @@ export default function DeleteAccount() {
<button onClick={close} className="pill button"> <button onClick={close} className="pill button">
Cancel Cancel
</button> </button>
<button onClick={submit} className="pill button !bg-red-400 !border-red-500 hover:!bg-red-500"> <SubmitButton onClick={handleSubmit} text="Delete" className="!bg-red-400 !border-red-500 hover:!bg-red-500" />
Delete
</button>
</div> </div>
</div> </div>
</div>, </div>,

View file

@ -3,6 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import SubmitButton from "../submit-button";
interface Props { interface Props {
title: string; title: string;
@ -71,9 +72,7 @@ export default function SubmitDialogButton({ title, description, onSubmit, error
<button onClick={close} className="pill button"> <button onClick={close} className="pill button">
Cancel Cancel
</button> </button>
<button onClick={submit} className="pill button"> <SubmitButton onClick={submit} />
Submit
</button>
</div> </div>
</div> </div>
</div>, </div>,

View file

@ -0,0 +1,32 @@
"use client";
import { useState } from "react";
import { Icon } from "@iconify/react";
interface Props {
onClick: () => void | Promise<void>;
text?: string;
className?: string;
}
export default function SubmitButton({ onClick, text = "Submit", className }: Props) {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async (event: React.FormEvent) => {
event.preventDefault();
setIsLoading(true);
try {
await onClick();
} finally {
setIsLoading(false);
}
};
return (
<button type="submit" onClick={handleClick} className={`pill button w-min ${className}`}>
{text}
{isLoading && <Icon icon="svg-spinners:180-ring-with-bg" className="ml-2" />}
</button>
);
}

View file

@ -13,6 +13,7 @@ import TagSelector from "../tag-selector";
import ImageList from "./image-list"; import ImageList from "./image-list";
import LikeButton from "../like-button"; import LikeButton from "../like-button";
import Carousel from "../carousel"; import Carousel from "../carousel";
import SubmitButton from "../submit-button";
interface Props { interface Props {
mii: Mii; mii: Mii;
@ -46,9 +47,7 @@ export default function EditForm({ mii, likes }: Props) {
const [tags, setTags] = useState(mii.tags); const [tags, setTags] = useState(mii.tags);
const hasFilesChanged = useRef(false); const hasFilesChanged = useRef(false);
const handleSubmit = async (event: React.FormEvent) => { const handleSubmit = async () => {
event.preventDefault();
// Validate before sending request // Validate before sending request
const nameValidation = nameSchema.safeParse(name); const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) { if (!nameValidation.success) {
@ -110,7 +109,7 @@ export default function EditForm({ mii, likes }: Props) {
}, [mii.id, mii.imageCount]); }, [mii.id, mii.imageCount]);
return ( return (
<form onSubmit={handleSubmit} className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center"> <form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-[18.75rem] h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3"> <div className="w-[18.75rem] h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
<Carousel images={[`/mii/${mii.id}/mii.webp`, `/mii/${mii.id}/qr-code.webp`, ...files.map((file) => URL.createObjectURL(file))]} /> <Carousel images={[`/mii/${mii.id}/mii.webp`, `/mii/${mii.id}/qr-code.webp`, ...files.map((file) => URL.createObjectURL(file))]} />
@ -201,9 +200,7 @@ export default function EditForm({ mii, likes }: Props) {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{error && <span className="text-red-400 font-bold">Error: {error}</span>} {error && <span className="text-red-400 font-bold">Error: {error}</span>}
<button type="submit" className="pill button w-min ml-auto"> <SubmitButton onClick={handleSubmit} text="Edit" className="ml-auto" />
Edit
</button>
</div> </div>
</div> </div>
</form> </form>

View file

@ -19,6 +19,7 @@ import QrUpload from "./qr-upload";
import QrScanner from "./qr-scanner"; import QrScanner from "./qr-scanner";
import LikeButton from "../like-button"; import LikeButton from "../like-button";
import Carousel from "../carousel"; import Carousel from "../carousel";
import SubmitButton from "../submit-button";
export default function SubmitForm() { export default function SubmitForm() {
const [files, setFiles] = useState<FileWithPath[]>([]); const [files, setFiles] = useState<FileWithPath[]>([]);
@ -49,9 +50,7 @@ export default function SubmitForm() {
const [tags, setTags] = useState<string[]>([]); const [tags, setTags] = useState<string[]>([]);
const [qrBytesRaw, setQrBytesRaw] = useState<number[]>([]); const [qrBytesRaw, setQrBytesRaw] = useState<number[]>([]);
const handleSubmit = async (event: React.FormEvent) => { const handleSubmit = async () => {
event.preventDefault();
// Validate before sending request // Validate before sending request
const nameValidation = nameSchema.safeParse(name); const nameValidation = nameSchema.safeParse(name);
if (!nameValidation.success) { if (!nameValidation.success) {
@ -129,7 +128,7 @@ export default function SubmitForm() {
}, [qrBytesRaw]); }, [qrBytesRaw]);
return ( return (
<form onSubmit={handleSubmit} className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center"> <form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-[18.75rem] h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3"> <div className="w-[18.75rem] h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
<Carousel <Carousel
@ -242,9 +241,7 @@ export default function SubmitForm() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{error && <span className="text-red-400 font-bold">Error: {error}</span>} {error && <span className="text-red-400 font-bold">Error: {error}</span>}
<button type="submit" className="pill button w-min ml-auto"> <SubmitButton onClick={handleSubmit} className="ml-auto" />
Submit
</button>
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,16 +1,15 @@
"use client"; "use client";
import { FormEvent, useState } from "react"; import { useState } from "react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { usernameSchema } from "@/lib/schemas"; import { usernameSchema } from "@/lib/schemas";
import SubmitButton from "./submit-button";
export default function UsernameForm() { export default function UsernameForm() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const handleSubmit = async (event: FormEvent) => { const handleSubmit = async () => {
event.preventDefault();
const parsed = usernameSchema.safeParse(username); const parsed = usernameSchema.safeParse(username);
if (!parsed.success) setError(parsed.error.errors[0].message); if (!parsed.success) setError(parsed.error.errors[0].message);
@ -30,7 +29,7 @@ export default function UsernameForm() {
}; };
return ( return (
<form onSubmit={handleSubmit} className="flex flex-col items-center"> <form className="flex flex-col items-center">
<input <input
type="text" type="text"
placeholder="Type your username..." placeholder="Type your username..."
@ -40,9 +39,7 @@ export default function UsernameForm() {
className="pill input w-96 mb-2" className="pill input w-96 mb-2"
/> />
<button type="submit" className="pill button w-min"> <SubmitButton onClick={handleSubmit} />
Submit
</button>
{error && <p className="text-red-400 font-semibold mt-4">Error: {error}</p>} {error && <p className="text-red-400 font-semibold mt-4">Error: {error}</p>}
</form> </form>
); );