mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat(ui): revamp sign-in/sign-up with animated bg and release highlights
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const PARTICLE_DENSITY = 14000;
|
||||
const PARTICLE_MIN_COUNT = 40;
|
||||
const PARTICLE_MIN_RADIUS = 1.4;
|
||||
const PARTICLE_RADIUS_RANGE = 1.8;
|
||||
const PARTICLE_SPEED = 0.35;
|
||||
const CONNECTION_DISTANCE = 140;
|
||||
const CONNECTION_DISTANCE_SQ = CONNECTION_DISTANCE * CONNECTION_DISTANCE;
|
||||
const LINE_WIDTH = 0.8;
|
||||
const MOUSE_RADIUS = 160;
|
||||
const MOUSE_RADIUS_SQ = MOUSE_RADIUS * MOUSE_RADIUS;
|
||||
const MOUSE_FORCE = 2.2;
|
||||
const PARTICLE_ALPHA = 0.75;
|
||||
const CONNECTION_MAX_ALPHA = 0.35;
|
||||
const ALPHA_TIER_COUNT = 3;
|
||||
const MAX_DPR = 2;
|
||||
const ACCENT_FALLBACK = "rgb(110, 231, 183)";
|
||||
const ACCENT_VAR = "--bg-button-primary";
|
||||
|
||||
export const AnimatedDotsBackground = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d", { alpha: true });
|
||||
if (!ctx) return;
|
||||
|
||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const ac = new AbortController();
|
||||
const { signal } = ac;
|
||||
|
||||
let animationId = 0;
|
||||
let resizeFrameId = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let accentColor = ACCENT_FALLBACK;
|
||||
let count = 0;
|
||||
let xs = new Float32Array(0);
|
||||
let ys = new Float32Array(0);
|
||||
let vxs = new Float32Array(0);
|
||||
let vys = new Float32Array(0);
|
||||
let rs = new Float32Array(0);
|
||||
|
||||
const tierBuckets: Float32Array[] = [];
|
||||
const tierLengths = new Int32Array(ALPHA_TIER_COUNT);
|
||||
|
||||
const mouse = { x: -9999, y: -9999, active: false };
|
||||
|
||||
const readAccent = () => {
|
||||
const value = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(ACCENT_VAR)
|
||||
.trim();
|
||||
accentColor = value || ACCENT_FALLBACK;
|
||||
};
|
||||
|
||||
const ensureTierCapacity = (n: number) => {
|
||||
const maxPairs = (n * (n - 1)) / 2;
|
||||
const floatsNeeded = maxPairs * 4;
|
||||
for (let t = 0; t < ALPHA_TIER_COUNT; t++) {
|
||||
if (!tierBuckets[t] || tierBuckets[t].length < floatsNeeded) {
|
||||
tierBuckets[t] = new Float32Array(floatsNeeded);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const initParticles = () => {
|
||||
count = Math.max(
|
||||
PARTICLE_MIN_COUNT,
|
||||
Math.floor((width * height) / PARTICLE_DENSITY),
|
||||
);
|
||||
xs = new Float32Array(count);
|
||||
ys = new Float32Array(count);
|
||||
vxs = new Float32Array(count);
|
||||
vys = new Float32Array(count);
|
||||
rs = new Float32Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
xs[i] = Math.random() * width;
|
||||
ys[i] = Math.random() * height;
|
||||
vxs[i] = (Math.random() - 0.5) * PARTICLE_SPEED;
|
||||
vys[i] = (Math.random() - 0.5) * PARTICLE_SPEED;
|
||||
rs[i] = Math.random() * PARTICLE_RADIUS_RANGE + PARTICLE_MIN_RADIUS;
|
||||
}
|
||||
ensureTierCapacity(count);
|
||||
};
|
||||
|
||||
const resize = () => {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
|
||||
width = canvas.clientWidth;
|
||||
height = canvas.clientHeight;
|
||||
canvas.width = Math.floor(width * dpr);
|
||||
canvas.height = Math.floor(height * dpr);
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
initParticles();
|
||||
};
|
||||
|
||||
const scheduleResize = () => {
|
||||
if (resizeFrameId) return;
|
||||
resizeFrameId = requestAnimationFrame(() => {
|
||||
resizeFrameId = 0;
|
||||
resize();
|
||||
});
|
||||
};
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
mouse.active = true;
|
||||
};
|
||||
const onLeave = () => {
|
||||
mouse.active = false;
|
||||
mouse.x = -9999;
|
||||
mouse.y = -9999;
|
||||
};
|
||||
|
||||
const drawFrame = () => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let x = xs[i] + vxs[i];
|
||||
let y = ys[i] + vys[i];
|
||||
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
vxs[i] = -vxs[i];
|
||||
} else if (x > width) {
|
||||
x = width;
|
||||
vxs[i] = -vxs[i];
|
||||
}
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
vys[i] = -vys[i];
|
||||
} else if (y > height) {
|
||||
y = height;
|
||||
vys[i] = -vys[i];
|
||||
}
|
||||
|
||||
if (mouse.active) {
|
||||
const dx = x - mouse.x;
|
||||
const dy = y - mouse.y;
|
||||
const dsq = dx * dx + dy * dy;
|
||||
if (dsq < MOUSE_RADIUS_SQ && dsq > 0) {
|
||||
const dist = Math.sqrt(dsq);
|
||||
const force = (MOUSE_RADIUS - dist) / MOUSE_RADIUS;
|
||||
const inv = 1 / dist;
|
||||
x += dx * inv * force * MOUSE_FORCE;
|
||||
y += dy * inv * force * MOUSE_FORCE;
|
||||
}
|
||||
}
|
||||
|
||||
xs[i] = x;
|
||||
ys[i] = y;
|
||||
}
|
||||
|
||||
for (let t = 0; t < ALPHA_TIER_COUNT; t++) tierLengths[t] = 0;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const xi = xs[i];
|
||||
const yi = ys[i];
|
||||
for (let j = i + 1; j < count; j++) {
|
||||
const dx = xi - xs[j];
|
||||
const dy = yi - ys[j];
|
||||
const dsq = dx * dx + dy * dy;
|
||||
if (dsq >= CONNECTION_DISTANCE_SQ) continue;
|
||||
|
||||
const ratio = 1 - Math.sqrt(dsq) / CONNECTION_DISTANCE;
|
||||
let tier = (ratio * ALPHA_TIER_COUNT) | 0;
|
||||
if (tier >= ALPHA_TIER_COUNT) tier = ALPHA_TIER_COUNT - 1;
|
||||
|
||||
const bucket = tierBuckets[tier];
|
||||
const off = tierLengths[tier];
|
||||
bucket[off] = xi;
|
||||
bucket[off + 1] = yi;
|
||||
bucket[off + 2] = xs[j];
|
||||
bucket[off + 3] = ys[j];
|
||||
tierLengths[tier] = off + 4;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.lineWidth = LINE_WIDTH;
|
||||
ctx.strokeStyle = accentColor;
|
||||
|
||||
for (let t = 0; t < ALPHA_TIER_COUNT; t++) {
|
||||
const len = tierLengths[t];
|
||||
if (len === 0) continue;
|
||||
ctx.globalAlpha = CONNECTION_MAX_ALPHA * ((t + 0.5) / ALPHA_TIER_COUNT);
|
||||
ctx.beginPath();
|
||||
const bucket = tierBuckets[t];
|
||||
for (let k = 0; k < len; k += 4) {
|
||||
ctx.moveTo(bucket[k], bucket[k + 1]);
|
||||
ctx.lineTo(bucket[k + 2], bucket[k + 3]);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.fillStyle = accentColor;
|
||||
ctx.globalAlpha = PARTICLE_ALPHA;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < count; i++) {
|
||||
ctx.moveTo(xs[i] + rs[i], ys[i]);
|
||||
ctx.arc(xs[i], ys[i], rs[i], 0, Math.PI * 2);
|
||||
}
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
};
|
||||
|
||||
const loop = () => {
|
||||
drawFrame();
|
||||
animationId = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (animationId) return;
|
||||
animationId = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (!animationId) return;
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = 0;
|
||||
};
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (document.hidden) stop();
|
||||
else if (!reducedMotion.matches) start();
|
||||
};
|
||||
|
||||
const onMotionChange = () => {
|
||||
if (reducedMotion.matches) {
|
||||
stop();
|
||||
drawFrame();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
};
|
||||
|
||||
const themeObserver = new MutationObserver(readAccent);
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
readAccent();
|
||||
resize();
|
||||
if (reducedMotion.matches) drawFrame();
|
||||
else start();
|
||||
|
||||
window.addEventListener("resize", scheduleResize, { signal });
|
||||
window.addEventListener("mousemove", onMove, { signal, passive: true });
|
||||
window.addEventListener("mouseleave", onLeave, { signal });
|
||||
document.addEventListener("visibilitychange", onVisibilityChange, {
|
||||
signal,
|
||||
});
|
||||
reducedMotion.addEventListener("change", onMotionChange, { signal });
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
if (resizeFrameId) cancelAnimationFrame(resizeFrameId);
|
||||
themeObserver.disconnect();
|
||||
ac.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { cn } from "@/lib";
|
||||
|
||||
interface AuthCardProps {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const CARD_BASE =
|
||||
"rounded-large border-divider shadow-small dark:bg-background/80 relative flex w-full flex-col border bg-white/85 backdrop-blur-xl";
|
||||
|
||||
export const AuthCard = ({ className, children }: AuthCardProps) => (
|
||||
<div className={cn(CARD_BASE, className)}>{children}</div>
|
||||
);
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { AnimatedDotsBackground } from "@/components/auth/oss/animated-dots-background";
|
||||
import { AuthCard } from "@/components/auth/oss/auth-card";
|
||||
import { AuthReleaseHighlights } from "@/components/auth/oss/auth-release-highlights";
|
||||
import { ProwlerExtended } from "@/components/icons";
|
||||
import { ThemeSwitch } from "@/components/ThemeSwitch";
|
||||
|
||||
@@ -8,36 +11,32 @@ interface AuthLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AuthLayout = ({ title, children }: AuthLayoutProps) => {
|
||||
return (
|
||||
<div className="relative flex min-h-screen w-full overflow-x-hidden overflow-y-auto">
|
||||
<div className="relative flex w-full flex-col items-center justify-center px-4 py-32">
|
||||
{/* Background Pattern */}
|
||||
<div
|
||||
className="absolute inset-0 mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)] bg-size-[16px_16px]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(var(--bg-button-primary) 1px, transparent 1px)",
|
||||
}}
|
||||
></div>
|
||||
export const AuthLayout = ({ title, children }: AuthLayoutProps) => (
|
||||
<main className="relative min-h-screen w-full overflow-hidden">
|
||||
<AnimatedDotsBackground />
|
||||
|
||||
{/* Prowler Logo */}
|
||||
<div className="relative z-10 mb-8 flex w-full max-w-[300px]">
|
||||
<ProwlerExtended width={300} className="h-auto w-full" />
|
||||
</div>
|
||||
|
||||
{/* Auth Form Container */}
|
||||
<div className="rounded-large border-divider shadow-small dark:bg-background/85 relative z-10 flex w-full max-w-sm flex-col gap-4 border bg-white/90 px-8 py-10 md:max-w-md">
|
||||
{/* Header with Title and Theme Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="pb-2 text-xl font-medium">{title}</p>
|
||||
<ThemeSwitch aria-label="Toggle theme" />
|
||||
<div className="relative z-10 grid min-h-screen grid-cols-1 lg:grid-cols-2">
|
||||
<div className="flex items-center justify-center px-6 py-10 sm:px-10">
|
||||
<div className="flex w-full max-w-sm flex-col">
|
||||
<div className="mb-6 flex w-full justify-center">
|
||||
<ProwlerExtended
|
||||
width={300}
|
||||
className="h-auto w-[220px] max-w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{children}
|
||||
<AuthCard className="gap-3 px-6 py-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="pb-1 text-lg font-medium">{title}</p>
|
||||
<ThemeSwitch aria-label="Toggle theme" />
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</AuthCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuthReleaseHighlights />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import { AuthCard } from "@/components/auth/oss/auth-card";
|
||||
import { ProwlerShort } from "@/components/icons";
|
||||
import { Button } from "@/components/shadcn";
|
||||
|
||||
const RELEASE = {
|
||||
version: "5.24.0",
|
||||
url: "https://github.com/prowler-cloud/prowler/releases/tag/5.24.0",
|
||||
repoUrl: "https://github.com/prowler-cloud/prowler",
|
||||
} as const;
|
||||
|
||||
const HIGHLIGHTS: readonly string[] = [
|
||||
"Redesigned resources side drawer",
|
||||
"New AWS Bedrock & IAM hardening checks",
|
||||
"New Microsoft 365 Conditional Access coverage",
|
||||
];
|
||||
|
||||
export const AuthReleaseHighlights = () => (
|
||||
<aside
|
||||
aria-label={`Prowler v${RELEASE.version} highlights`}
|
||||
className="hidden items-center justify-center px-6 py-10 sm:px-10 lg:flex"
|
||||
>
|
||||
<div className="w-full max-w-md">
|
||||
<AuthCard className="gap-6 px-7 py-8">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute top-6 right-6 flex h-9 w-9 items-center justify-center rounded-md bg-emerald-400/15"
|
||||
>
|
||||
<ProwlerShort width={18} className="text-emerald-400" />
|
||||
</div>
|
||||
|
||||
<div className="pr-14">
|
||||
<p className="text-xs font-semibold text-emerald-400">
|
||||
Prowler v{RELEASE.version}
|
||||
</p>
|
||||
<h2 className="mt-1 text-2xl leading-tight font-semibold">
|
||||
Fresh off the branch
|
||||
</h2>
|
||||
<p className="text-default-500 mt-2 text-sm">
|
||||
A quick look at what we just shipped in v{RELEASE.version}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col gap-3">
|
||||
{HIGHLIGHTS.map((label) => (
|
||||
<li key={label} className="flex items-start gap-3">
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
icon="mdi:check-circle"
|
||||
width={20}
|
||||
className="mt-0.5 shrink-0 text-emerald-400"
|
||||
/>
|
||||
<span className="text-sm">{label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Button asChild size="sm">
|
||||
<a href={RELEASE.url} target="_blank" rel="noopener noreferrer">
|
||||
See full release notes
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a href={RELEASE.repoUrl} target="_blank" rel="noopener noreferrer">
|
||||
<Icon aria-hidden="true" icon="mdi:github" width={16} />
|
||||
GitHub
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</AuthCard>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
Reference in New Issue
Block a user