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:
@@ -0,0 +1,109 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
Cypress.env("AUTO_LOGIN", false);
|
||||||
|
|
||||||
|
describe("sign-in page", () => {
|
||||||
|
after(() => {
|
||||||
|
Cypress.env("AUTO_LOGIN", true);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.clearCookies();
|
||||||
|
cy.clearLocalStorage();
|
||||||
|
|
||||||
|
cy.intercept("POST", "**/api/proxy/profile/login*", (req) => {
|
||||||
|
req.reply({
|
||||||
|
delay: 2500,
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
success: true,
|
||||||
|
message: "ok",
|
||||||
|
data: {
|
||||||
|
access_token: "e2e-access-token",
|
||||||
|
refresh_token: "e2e-refresh-token",
|
||||||
|
expires_at: Date.now() + 60_000,
|
||||||
|
user: {
|
||||||
|
id: "507f1f77bcf86cd799439011",
|
||||||
|
username: "tester",
|
||||||
|
email: "tester@example.com",
|
||||||
|
role: "admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).as("loginRequest");
|
||||||
|
|
||||||
|
cy.visit("/auth/sign-in");
|
||||||
|
cy.location("pathname").should("eq", "/auth/sign-in");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the sign-in box centered and visible in viewport", () => {
|
||||||
|
cy.get("div[class*='max-w-'][class*='shadow-theme-xs'][class*='rounded-2xl']")
|
||||||
|
.first()
|
||||||
|
.should("be.visible")
|
||||||
|
.then(($card) => {
|
||||||
|
const rect = $card[0].getBoundingClientRect();
|
||||||
|
|
||||||
|
cy.window().then((win) => {
|
||||||
|
const viewportCenterX = win.innerWidth / 2;
|
||||||
|
const viewportCenterY = win.innerHeight / 2;
|
||||||
|
const cardCenterX = rect.left + rect.width / 2;
|
||||||
|
const cardCenterY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
expect(rect.top).to.be.gte(0);
|
||||||
|
expect(rect.left).to.be.gte(0);
|
||||||
|
expect(rect.bottom).to.be.lte(win.innerHeight);
|
||||||
|
expect(rect.right).to.be.lte(win.innerWidth);
|
||||||
|
expect(Math.abs(cardCenterX - viewportCenterX)).to.be.lessThan(80);
|
||||||
|
expect(Math.abs(cardCenterY - viewportCenterY)).to.be.lessThan(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles theme correctly", () => {
|
||||||
|
cy.get("html").should("not.have.class", "dark");
|
||||||
|
cy.contains("button", "Switch to dark mode").click();
|
||||||
|
cy.get("html").should("have.class", "dark");
|
||||||
|
|
||||||
|
cy.contains("button", "Switch to light mode").click();
|
||||||
|
cy.get("html").should("not.have.class", "dark");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows required errors under each field and does not call login API", () => {
|
||||||
|
cy.contains('button[type="submit"]', "Sign in").click();
|
||||||
|
|
||||||
|
cy.get("p.text-red")
|
||||||
|
.should("have.length", 2)
|
||||||
|
.each(($message) => {
|
||||||
|
expect($message.text().trim()).to.eq("This field is required");
|
||||||
|
});
|
||||||
|
cy.get("@loginRequest.all").should("have.length", 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows and hides password value when lock icon is clicked", () => {
|
||||||
|
cy.get('input[name="password"]').as("passwordInput");
|
||||||
|
cy.get("@passwordInput").should("have.attr", "type", "password");
|
||||||
|
|
||||||
|
cy.get('button[aria-label="Show password"]').click();
|
||||||
|
cy.get("@passwordInput").should("have.attr", "type", "text");
|
||||||
|
|
||||||
|
cy.get('button[aria-label="Hide password"]').click();
|
||||||
|
cy.get("@passwordInput").should("have.attr", "type", "password");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows default button text first, then spinner while submitting", () => {
|
||||||
|
cy.contains('button[type="submit"]', "Sign in")
|
||||||
|
.as("submitButton")
|
||||||
|
.should("be.visible")
|
||||||
|
.and("not.be.disabled");
|
||||||
|
|
||||||
|
cy.get('input[name="username"]').type("tester");
|
||||||
|
cy.get('input[name="password"]').type("Secret123!");
|
||||||
|
|
||||||
|
cy.get("@submitButton").click();
|
||||||
|
cy.get('button[type="submit"]')
|
||||||
|
.find("div.animate-spin", { timeout: 10000 })
|
||||||
|
.should("exist");
|
||||||
|
cy.get('button[type="submit"]').should("not.contain", "Sign in");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,7 +19,12 @@ export default function SigninWithPassword() {
|
|||||||
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
||||||
"password" | "text"
|
"password" | "text"
|
||||||
>("password");
|
>("password");
|
||||||
const { handleSubmit, reset, register } = useForm<ILoginCredentials>();
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<ILoginCredentials>();
|
||||||
const { mutate } = useLoginQuery(reset);
|
const { mutate } = useLoginQuery(reset);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
||||||
@@ -46,6 +51,7 @@ export default function SigninWithPassword() {
|
|||||||
className="mb-4 [&_input]:py-[15px]"
|
className="mb-4 [&_input]:py-[15px]"
|
||||||
placeholder="Enter your Username"
|
placeholder="Enter your Username"
|
||||||
name="username"
|
name="username"
|
||||||
|
errors={errors}
|
||||||
icon={<UserIcon />}
|
icon={<UserIcon />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -62,15 +68,24 @@ export default function SigninWithPassword() {
|
|||||||
className="mb-5 [&_input]:py-[15px]"
|
className="mb-5 [&_input]:py-[15px]"
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
name="password"
|
name="password"
|
||||||
|
errors={errors}
|
||||||
icon={
|
icon={
|
||||||
<PasswordIcon
|
<button
|
||||||
className="cursor-pointer text-dark-5 transition hover:text-primary dark:text-white/75"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPasswordFieldInputType(
|
setPasswordFieldInputType(
|
||||||
passwordFieldInputType === "password" ? "text" : "password",
|
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"
|
type="submit"
|
||||||
disabled={!!isMutating}
|
disabled={!!isMutating}
|
||||||
className={cn(
|
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",
|
isMutating && "cursor-not-allowed opacity-70 hover:translate-y-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,20 +3,104 @@ import { cn } from "@/lib/utils";
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
className?: string | null;
|
className?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grows with a flex `main` parent so the spinner sits in the middle of the content area.
|
* Full-screen route-style loader (orbs, rings, shimmer). Pass `className` to adapt
|
||||||
* Guards: `min-h-svh flex-none`. Compact UI: `flex-none min-h-*`.
|
* the shell—for example `min-h-48 flex-none` inside compact panels.
|
||||||
*/
|
*/
|
||||||
const Loading = ({ className }: IProps) => {
|
const Loading = ({ className }: IProps) => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
@keyframes app-route-loading-shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(400%); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 w-full min-w-0 flex-1 items-center justify-center",
|
"relative flex min-h-svh w-full min-w-0 flex-1 items-center justify-center overflow-hidden bg-gray dark:bg-dark",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-primary" />
|
{/* 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+105
-23
@@ -6,16 +6,15 @@ interface IProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Status = ({ status = "draft", children }: IProps) => {
|
const reds = [
|
||||||
const reds = [
|
|
||||||
"cancelled",
|
"cancelled",
|
||||||
"inactive",
|
"inactive",
|
||||||
"rejected",
|
"rejected",
|
||||||
"failed",
|
"failed",
|
||||||
"denied",
|
"denied",
|
||||||
"blocked",
|
"blocked",
|
||||||
];
|
];
|
||||||
const greens = [
|
const greens = [
|
||||||
"active",
|
"active",
|
||||||
"approved",
|
"approved",
|
||||||
"completed",
|
"completed",
|
||||||
@@ -23,31 +22,114 @@ const Status = ({ status = "draft", children }: IProps) => {
|
|||||||
"verified",
|
"verified",
|
||||||
"published",
|
"published",
|
||||||
"sent",
|
"sent",
|
||||||
];
|
];
|
||||||
const blues = ["awarded", "processing", "in-review", "scheduled", "reviewed"];
|
const blues = ["awarded", "processing", "in-review", "scheduled", "reviewed"];
|
||||||
const yellows = ["pending", "warning", "on-hold", "delayed"];
|
const yellows = ["pending", "warning", "on-hold", "delayed"];
|
||||||
const oranges = ["expired", "expiring", "limited"];
|
const oranges = ["expired", "expiring", "limited"];
|
||||||
const grays = ["draft", "disabled", "archived", "paused", "suspended"];
|
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 tone = resolveTone(status);
|
||||||
|
const { root, dot, dotRing } = toneStyles[tone];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center rounded-2xl px-3 py-2 text-center text-sm font-medium capitalize",
|
"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,
|
||||||
"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",
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{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>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ interface IProps {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
onPageChange: ReactPaginateProps["onPageChange"];
|
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 }) => {
|
const Pagination: FC<IProps> = ({ totalPages, currentPage, onPageChange }) => {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -21,9 +26,12 @@ const Pagination: FC<IProps> = ({ totalPages, currentPage, onPageChange }) => {
|
|||||||
onPageChange(e);
|
onPageChange(e);
|
||||||
}}
|
}}
|
||||||
className="mt-2 flex w-full items-center justify-center gap-2 text-sm text-gray-dark dark:text-white"
|
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]"
|
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="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]"
|
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>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user