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:
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user