diff --git a/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts b/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts
index baf5cc26..284c2f45 100644
--- a/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts
+++ b/apps/bff/src/modules/auth/guards/failed-login-throttle.guard.ts
@@ -6,7 +6,7 @@ import type { Request } from "express";
@Injectable()
export class FailedLoginThrottleGuard {
- constructor(@Inject("REDIS") private readonly redis: Redis) {}
+ constructor(@Inject("REDIS_CLIENT") private readonly redis: Redis) {}
private getTracker(req: Request): string {
// Track by IP address + User Agent for failed login attempts only
diff --git a/apps/portal/src/components/atoms/LoadingOverlay.tsx b/apps/portal/src/components/atoms/LoadingOverlay.tsx
new file mode 100644
index 00000000..592f81d2
--- /dev/null
+++ b/apps/portal/src/components/atoms/LoadingOverlay.tsx
@@ -0,0 +1,41 @@
+import { Spinner } from "./Spinner";
+
+interface LoadingOverlayProps {
+ /** Whether the overlay is visible */
+ isVisible: boolean;
+ /** Main loading message */
+ title: string;
+ /** Optional subtitle/description */
+ subtitle?: string;
+ /** Spinner size */
+ spinnerSize?: "xs" | "sm" | "md" | "lg" | "xl";
+ /** Custom spinner color */
+ spinnerClassName?: string;
+ /** Custom overlay background */
+ overlayClassName?: string;
+}
+
+export function LoadingOverlay({
+ isVisible,
+ title,
+ subtitle,
+ spinnerSize = "xl",
+ spinnerClassName = "text-blue-600",
+ overlayClassName = "bg-white/80 backdrop-blur-sm",
+}: LoadingOverlayProps) {
+ if (!isVisible) {
+ return null;
+ }
+
+ return (
+
+
+
+
{title}
+ {subtitle && (
+
{subtitle}
+ )}
+
+
+ );
+}
diff --git a/apps/portal/src/components/atoms/Spinner.tsx b/apps/portal/src/components/atoms/Spinner.tsx
new file mode 100644
index 00000000..604e483a
--- /dev/null
+++ b/apps/portal/src/components/atoms/Spinner.tsx
@@ -0,0 +1,42 @@
+import { cn } from "@/lib/utils";
+
+interface SpinnerProps {
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
+ className?: string;
+}
+
+const sizeClasses = {
+ xs: "h-3 w-3",
+ sm: "h-4 w-4",
+ md: "h-6 w-6",
+ lg: "h-8 w-8",
+ xl: "h-10 w-10",
+};
+
+export function Spinner({ size = "sm", className }: SpinnerProps) {
+ return (
+
+ );
+}
diff --git a/apps/portal/src/components/atoms/button.tsx b/apps/portal/src/components/atoms/button.tsx
index cb3bcde1..c6474aa2 100644
--- a/apps/portal/src/components/atoms/button.tsx
+++ b/apps/portal/src/components/atoms/button.tsx
@@ -2,7 +2,7 @@ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "reac
import { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
-// Loading spinner removed - using inline spinner for buttons
+import { Spinner } from "./Spinner";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
@@ -75,15 +75,11 @@ const Button = forwardRef((p
aria-busy={loading || undefined}
{...anchorProps}
>
-
- {loading ? (
-
- ) : (
- leftIcon
- )}
- {loading ? (loadingText ?? children) : children}
- {!loading && rightIcon ? {rightIcon} : null}
-
+
+ {loading ? : leftIcon}
+ {loading ? (loadingText ?? children) : children}
+ {!loading && rightIcon ? {rightIcon} : null}
+
);
}
@@ -104,12 +100,8 @@ const Button = forwardRef((p
aria-busy={loading || undefined}
{...buttonProps}
>
-
- {loading ? (
-
- ) : (
- leftIcon
- )}
+
+ {loading ? : leftIcon}
{loading ? (loadingText ?? children) : children}
{!loading && rightIcon ? {rightIcon} : null}
diff --git a/apps/portal/src/components/atoms/index.ts b/apps/portal/src/components/atoms/index.ts
index 60cc1587..6a2c2df0 100644
--- a/apps/portal/src/components/atoms/index.ts
+++ b/apps/portal/src/components/atoms/index.ts
@@ -26,7 +26,9 @@ export type { StatusPillProps } from "./status-pill";
export { Badge, badgeVariants } from "./badge";
export type { BadgeProps } from "./badge";
-// Loading components consolidated into skeleton loading
+// Loading components
+export { Spinner } from "./Spinner";
+export { LoadingOverlay } from "./LoadingOverlay";
export {
ErrorState,
diff --git a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx
index 7317d7ce..d9c25268 100644
--- a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx
+++ b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx
@@ -76,15 +76,14 @@ export function LinkWhmcsForm({ onTransferred, className = "" }: LinkWhmcsFormPr
{error && {error}}
-