refactor(loading, auth, status): enhance UI components for improved aesthetics and functionality

- Redesigned Loading component to feature a full-screen loader with animated elements for a modern look.
- Updated SigninWithPassword component to include error handling and improved password visibility toggle.
- Refactored Status component to utilize a tone-based styling system for better visual feedback based on status types.
This commit is contained in:
AmirReza Jamali
2026-04-25 12:35:03 +03:30
parent f195bba03d
commit 2a4f3e7e4b
5 changed files with 350 additions and 52 deletions
+20 -5
View File
@@ -19,7 +19,12 @@ export default function SigninWithPassword() {
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
"password" | "text"
>("password");
const { handleSubmit, reset, register } = useForm<ILoginCredentials>();
const {
handleSubmit,
reset,
register,
formState: { errors },
} = useForm<ILoginCredentials>();
const { mutate } = useLoginQuery(reset);
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
@@ -46,6 +51,7 @@ export default function SigninWithPassword() {
className="mb-4 [&_input]:py-[15px]"
placeholder="Enter your Username"
name="username"
errors={errors}
icon={<UserIcon />}
/>
@@ -62,15 +68,24 @@ export default function SigninWithPassword() {
className="mb-5 [&_input]:py-[15px]"
placeholder="Enter your password"
name="password"
errors={errors}
icon={
<PasswordIcon
className="cursor-pointer text-dark-5 transition hover:text-primary dark:text-white/75"
<button
type="button"
onClick={() => {
setPasswordFieldInputType(
passwordFieldInputType === "password" ? "text" : "password",
);
}}
/>
className="absolute right-4.5 top-1/2 z-10 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded text-dark-5 transition hover:text-primary dark:text-white/75 [&>svg]:!static [&>svg]:!size-5 [&>svg]:!translate-y-0"
aria-label={
passwordFieldInputType === "password"
? "Show password"
: "Hide password"
}
>
<PasswordIcon className="pointer-events-none" />
</button>
}
/>
@@ -89,7 +104,7 @@ export default function SigninWithPassword() {
type="submit"
disabled={!!isMutating}
className={cn(
"w-full rounded-xl bg-gradient-to-r from-primary to-primary/85 p-4 font-semibold shadow-theme-xs transition-all duration-300 hover:-translate-y-0.5 hover:opacity-95",
"shadow-theme-xs w-full rounded-xl bg-gradient-to-r from-primary to-primary/85 p-4 font-semibold transition-all duration-300 hover:-translate-y-0.5 hover:opacity-95",
isMutating && "cursor-not-allowed opacity-70 hover:translate-y-0",
)}
/>
+94 -10
View File
@@ -3,20 +3,104 @@ import { cn } from "@/lib/utils";
interface IProps {
className?: string | null;
}
/**
* Grows with a flex `main` parent so the spinner sits in the middle of the content area.
* Guards: `min-h-svh flex-none`. Compact UI: `flex-none min-h-*`.
* Full-screen route-style loader (orbs, rings, shimmer). Pass `className` to adapt
* the shell—for example `min-h-48 flex-none` inside compact panels.
*/
const Loading = ({ className }: IProps) => {
return (
<div
className={cn(
"flex min-h-0 w-full min-w-0 flex-1 items-center justify-center",
className,
)}
>
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-primary" />
</div>
<>
<style>{`
@keyframes app-route-loading-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
`}</style>
<div
className={cn(
"relative flex min-h-svh w-full min-w-0 flex-1 items-center justify-center overflow-hidden bg-gray dark:bg-dark",
className,
)}
>
{/* Soft gradient wash */}
<div
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(87,80,241,0.22),transparent)] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(87,80,241,0.35),transparent)]"
aria-hidden
/>
{/* Floating orbs */}
<div
className="pointer-events-none absolute -left-32 top-1/4 h-72 w-72 animate-pulse rounded-full bg-primary/20 blur-3xl dark:bg-primary/25"
aria-hidden
/>
<div
className="pointer-events-none absolute -right-24 bottom-1/4 h-64 w-64 animate-pulse rounded-full bg-blue/25 blur-3xl dark:bg-blue/20"
style={{ animationDelay: "1s" }}
aria-hidden
/>
<div
className="pointer-events-none absolute left-1/2 top-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/10 blur-[100px] dark:bg-primary/15"
aria-hidden
/>
{/* Subtle grid */}
<div
className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,rgba(148,163,184,0.06)_1px,transparent_1px),linear-gradient(to_bottom,rgba(148,163,184,0.06)_1px,transparent_1px)] bg-[size:48px_48px] dark:bg-[linear-gradient(to_right,rgba(55,65,81,0.15)_1px,transparent_1px),linear-gradient(to_bottom,rgba(55,65,81,0.15)_1px,transparent_1px)]"
aria-hidden
/>
<div className="relative z-10 flex flex-col items-center gap-8 px-6">
<div className="relative flex h-28 w-28 items-center justify-center">
{/* Outer glow */}
<div className="absolute inset-0 rounded-full bg-primary/20 blur-xl dark:bg-primary/30" />
{/* Concentric rings */}
<div className="absolute inset-0 animate-spin rounded-full border-2 border-transparent border-r-primary/40 border-t-primary/90 shadow-[0_0_20px_rgba(87,80,241,0.35)]" />
<div
className="absolute inset-2 animate-[spin_2.5s_linear_infinite_reverse] rounded-full border-2 border-transparent border-b-blue/70 border-l-blue/30"
aria-hidden
/>
<div className="absolute inset-5 rounded-full border border-stroke/80 dark:border-stroke-dark/60" />
{/* Core */}
<div className="relative flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-blue shadow-lg ring-4 ring-white/60 dark:ring-dark-2/80">
<span className="text-lg font-bold tracking-tight text-white drop-shadow-sm">
OL
</span>
</div>
</div>
<div className="flex flex-col items-center gap-3 text-center">
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 animate-pulse rounded-full bg-primary" />
<span
className="h-2 w-2 animate-pulse rounded-full bg-primary/70"
style={{ animationDelay: "0.2s" }}
/>
<span
className="h-2 w-2 animate-pulse rounded-full bg-primary/40"
style={{ animationDelay: "0.4s" }}
/>
</div>
<p className="bg-gradient-to-r from-dark via-primary to-blue bg-clip-text text-body-sm font-medium text-transparent dark:from-gray-7 dark:via-primary dark:to-blue-light">
Preparing your workspace
</p>
</div>
{/* Shimmer bar */}
<div className="h-1 w-48 overflow-hidden rounded-full bg-stroke dark:bg-stroke-dark">
<div
className="h-full w-1/3 rounded-full bg-gradient-to-r from-transparent via-primary to-transparent"
style={{
animation:
"app-route-loading-shimmer 1.5s ease-in-out infinite",
}}
/>
</div>
</div>
</div>
</>
);
};
+117 -35
View File
@@ -6,48 +6,130 @@ interface IProps {
children: ReactNode;
}
const reds = [
"cancelled",
"inactive",
"rejected",
"failed",
"denied",
"blocked",
];
const greens = [
"active",
"approved",
"completed",
"success",
"verified",
"published",
"sent",
];
const blues = ["awarded", "processing", "in-review", "scheduled", "reviewed"];
const yellows = ["pending", "warning", "on-hold", "delayed"];
const oranges = ["expired", "expiring", "limited"];
const grays = ["draft", "disabled", "archived", "paused", "suspended"];
type Tone =
| "green"
| "red"
| "blue"
| "yellow"
| "orange"
| "gray"
| "new"
| "info"
| "urgent"
| "featured";
function resolveTone(status: string): Tone {
if (greens.includes(status)) return "green";
if (reds.includes(status)) return "red";
if (blues.includes(status)) return "blue";
if (yellows.includes(status)) return "yellow";
if (oranges.includes(status)) return "orange";
if (grays.includes(status)) return "gray";
if (status === "new") return "new";
if (status === "info") return "info";
if (status === "urgent") return "urgent";
if (status === "featured") return "featured";
return "gray";
}
const toneStyles: Record<
Tone,
{ root: string; dot: string; dotRing?: string }
> = {
green: {
root: "bg-gradient-to-br from-green via-green to-green-dark text-white shadow-[0_2px_10px_-2px_rgba(34,173,92,0.55)] ring-1 ring-inset ring-white/25",
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.85)]",
dotRing: "ring-1 ring-white/40",
},
red: {
root: "bg-gradient-to-br from-error via-error to-error-dark text-white shadow-[0_2px_10px_-2px_rgba(242,48,48,0.5)] ring-1 ring-inset ring-white/20",
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.75)]",
dotRing: "ring-1 ring-white/35",
},
blue: {
root: "bg-gradient-to-br from-blue via-blue to-blue-dark text-white shadow-[0_2px_10px_-2px_rgba(60,80,224,0.45)] ring-1 ring-inset ring-white/25",
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]",
dotRing: "ring-1 ring-white/35",
},
yellow: {
root: "border border-yellow-dark-2/15 bg-gradient-to-br from-yellow-light to-yellow-light/70 text-yellow-dark-2 shadow-sm shadow-yellow-dark/10 ring-1 ring-inset ring-white/60",
dot: "bg-yellow-dark-2 shadow-[0_0_8px_rgba(217,119,6,0.45)]",
dotRing: "ring-2 ring-yellow-dark-2/25",
},
orange: {
root: "bg-gradient-to-br from-orange-light to-orange-light/85 text-white shadow-[0_2px_10px_-2px_rgba(245,148,96,0.45)] ring-1 ring-inset ring-white/25",
dot: "bg-white shadow-[0_0_8px_rgba(255,255,255,0.7)]",
dotRing: "ring-1 ring-white/30",
},
gray: {
root: "border border-gray-4/60 bg-gradient-to-b from-gray-1 to-gray-2 text-gray-7 shadow-sm ring-1 ring-inset ring-white/80",
dot: "bg-gray-5 shadow-inner",
dotRing: "ring-1 ring-gray-4/50",
},
new: {
root: "border border-green-light-3/80 bg-gradient-to-br from-green-light-6 via-green-light-6 to-green-light-5 text-green-dark shadow-sm shadow-green/10 ring-1 ring-inset ring-white/70",
dot: "bg-green shadow-[0_0_8px_rgba(34,173,92,0.5)]",
dotRing: "ring-2 ring-green/20",
},
info: {
root: "border border-blue-light-3/80 bg-gradient-to-br from-blue-light-5 via-blue-light-5 to-blue-light-4 text-blue-dark shadow-sm shadow-blue/10 ring-1 ring-inset ring-white/70",
dot: "bg-blue shadow-[0_0_8px_rgba(60,80,224,0.45)]",
dotRing: "ring-2 ring-blue/15",
},
urgent: {
root: "border border-error-light-3/90 bg-gradient-to-br from-error-light-6 via-error-light-6 to-error-light-5 text-error-dark shadow-sm shadow-error/10 ring-1 ring-inset ring-white/70",
dot: "bg-error shadow-[0_0_8px_rgba(242,48,48,0.45)]",
dotRing: "ring-2 ring-error/20",
},
featured: {
root: "bg-gradient-to-br from-primary via-[#6B63F3] to-primary text-white shadow-[0_2px_12px_-2px_rgba(87,80,241,0.55)] ring-1 ring-inset ring-white/30",
dot: "bg-white shadow-[0_0_12px_rgba(255,255,255,0.9)]",
dotRing: "ring-1 ring-white/40",
},
};
const Status = ({ status = "draft", children }: IProps) => {
const reds = [
"cancelled",
"inactive",
"rejected",
"failed",
"denied",
"blocked",
];
const greens = [
"active",
"approved",
"completed",
"success",
"verified",
"published",
"sent",
];
const blues = ["awarded", "processing", "in-review", "scheduled", "reviewed"];
const yellows = ["pending", "warning", "on-hold", "delayed"];
const oranges = ["expired", "expiring", "limited"];
const grays = ["draft", "disabled", "archived", "paused", "suspended"];
const tone = resolveTone(status);
const { root, dot, dotRing } = toneStyles[tone];
return (
<span
className={cn(
"flex items-center justify-center rounded-2xl px-3 py-2 text-center text-sm font-medium capitalize",
{
"bg-green text-white": greens.includes(status),
"bg-orange-light text-white": oranges.includes(status),
"bg-error text-white": reds.includes(status),
"bg-blue text-white": blues.includes(status),
"bg-gray-3 text-gray-7": grays.includes(status),
"bg-yellow-light text-yellow-dark-2": yellows.includes(status),
"bg-green-light-6 text-green-dark": status === "new",
"bg-blue-light-5 text-blue-dark": status === "info",
"bg-error-light-6 text-error-dark": status === "urgent",
"bg-primary text-white": status === "featured",
},
"inline-flex max-w-full items-center justify-center gap-2 rounded-full px-3.5 py-1.5 text-center text-xs font-semibold capitalize tracking-wide",
root,
)}
>
{children}
<span
aria-hidden
className={cn(
"relative inline-flex h-2 w-2 shrink-0 rounded-full",
dot,
dotRing,
)}
/>
<span className="min-w-0 truncate">{children}</span>
</span>
);
};
+10 -2
View File
@@ -5,6 +5,11 @@ interface IProps {
totalPages: number;
onPageChange: ReactPaginateProps["onPageChange"];
}
/** Full-cell hit target: styles live on <li>, but react-paginate puts clicks on the inner <a>. */
const pageLinkClassName =
"absolute inset-0 z-[1] flex cursor-pointer items-center justify-center select-none";
const Pagination: FC<IProps> = ({ totalPages, currentPage, onPageChange }) => {
return (
<Fragment>
@@ -21,9 +26,12 @@ const Pagination: FC<IProps> = ({ totalPages, currentPage, onPageChange }) => {
onPageChange(e);
}}
className="mt-2 flex w-full items-center justify-center gap-2 text-sm text-gray-dark dark:text-white"
nextClassName="flex h-9 min-w-9 items-center justify-center rounded-xl border border-stroke/80 bg-white/70 px-2 text-sm font-semibold text-dark shadow-theme-xs backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/35 hover:text-primary dark:border-dark-3 dark:bg-dark-2/70 dark:text-white dark:hover:bg-white/[0.1]"
previousClassName="flex h-9 min-w-9 items-center justify-center rounded-xl border border-stroke/80 bg-white/70 px-2 text-sm font-semibold text-dark shadow-theme-xs backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/35 hover:text-primary dark:border-dark-3 dark:bg-dark-2/70 dark:text-white dark:hover:bg-white/[0.1]"
nextClassName="relative flex h-9 min-w-9 items-center justify-center rounded-xl border border-stroke/80 bg-white/70 px-2 text-sm font-semibold text-dark shadow-theme-xs backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/35 hover:text-primary dark:border-dark-3 dark:bg-dark-2/70 dark:text-white dark:hover:bg-white/[0.1]"
previousClassName="relative flex h-9 min-w-9 items-center justify-center rounded-xl border border-stroke/80 bg-white/70 px-2 text-sm font-semibold text-dark shadow-theme-xs backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/35 hover:text-primary dark:border-dark-3 dark:bg-dark-2/70 dark:text-white dark:hover:bg-white/[0.1]"
pageClassName="relative flex h-9 min-w-9 items-center justify-center rounded-xl border border-stroke/80 bg-white/70 px-3 text-sm font-semibold text-dark shadow-theme-xs backdrop-blur-md transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/35 hover:text-primary dark:border-dark-3 dark:bg-dark-2/70 dark:text-white dark:hover:bg-white/[0.1]"
pageLinkClassName={pageLinkClassName}
previousLinkClassName={pageLinkClassName}
nextLinkClassName={pageLinkClassName}
/>
</Fragment>
);