mirror of
https://github.com/community-scripts/Proxmox.git
synced 2026-04-05 09:04:01 -04:00
refactor: Update landing page
This commit is contained in:
@@ -2,52 +2,64 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 224 71.4% 4.1%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
--primary: 221.2 83.2% 53.3%;
|
--primary: 220.9 39.3% 11%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 20% 98%;
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 220 14.3% 95.9%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 220 14.3% 95.9%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 220 14.3% 95.9%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 20% 98%;
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 220 13% 91%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 220 13% 91%;
|
||||||
--ring: 221.2 83.2% 53.3%;
|
--ring: 224 71.4% 4.1%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 224 71.4% 4.1%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 20% 98%;
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 224 71.4% 4.1%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 20% 98%;
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 224 71.4% 4.1%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 20% 98%;
|
||||||
--primary: 217.2 91.2% 59.8%;
|
--primary: 210 20% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 220.9 39.3% 11%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 215 27.9% 16.9%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 20% 98%;
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 215 27.9% 16.9%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 215 27.9% 16.9%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 20% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 20% 98%;
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 215 27.9% 16.9%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 215 27.9% 16.9%;
|
||||||
--ring: 224.3 76.3% 48%;
|
--ring: 216 12.2% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
|
|||||||
108
app/page.tsx
108
app/page.tsx
@@ -1,42 +1,82 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Image from "next/image";
|
import AnimatedGradientText from "@/components/magicui/animated-gradient-text";
|
||||||
import { Typewriter } from "react-simple-typewriter";
|
import Particles from "@/components/magicui/particles";
|
||||||
|
import ShineBorder from "@/components/magicui/shine-border";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ArrowRightIcon } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function CustomArrowRightIcon() {
|
||||||
|
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const [color, setColor] = useState("#000000");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColor(theme === "dark" ? "#ffffff" : "#000000");
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
export default function LandingPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="w-full">
|
||||||
<div className="relative flex h-screen w-full flex-col items-center justify-center bg-grid-black/[0.1] dark:bg-grid-white/[0.1]">
|
<Particles
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-background [mask-image:radial-gradient(ellipse_at_center,transparent_0%,black)]"></div>
|
className="absolute inset-0 -z-40"
|
||||||
<div className="flex animate-fade-up flex-col items-center justify-center">
|
quantity={100}
|
||||||
<Image src="/logo.png" alt="proxmox" width={150} height={150} />
|
ease={80}
|
||||||
<h1 className="relative z-20 bg-gradient-to-b from-[#0080C4] to-[#004c75] bg-clip-text py-4 text-center text-4xl font-bold text-transparent sm:text-left sm:text-5xl">
|
color={color}
|
||||||
Proxmox VE Helper-Scripts
|
refresh
|
||||||
</h1>
|
/>
|
||||||
<p className="bg-gradient-to-b from-neutral-200 to-neutral-500 bg-clip-text text-center text-xl leading-loose tracking-tight sm:text-left">
|
<div className="container mx-auto">
|
||||||
Proxmox VE Scripts for{" "}
|
<div className="flex flex-col items-center justify-center gap-4 py-20 lg:py-40">
|
||||||
<Typewriter
|
<div>
|
||||||
words={[
|
{/* <Button variant="secondary" size="sm" className="gap-4">
|
||||||
"Streamlining",
|
Read our launch article <MoveRight className="h-4 w-4" />
|
||||||
"Automating",
|
</Button> */}
|
||||||
"Simplifying",
|
<AnimatedGradientText>
|
||||||
"Optimizing",
|
<div
|
||||||
]}
|
className={cn(
|
||||||
loop={false}
|
`animate-gradient absolute inset-0 block size-full bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||||
cursor={true}
|
`p-px ![mask-composite:subtract]`,
|
||||||
cursorStyle="_"
|
)}
|
||||||
typeSpeed={70}
|
/>
|
||||||
deleteSpeed={50}
|
🎉 <Separator className="mx-2 h-4" orientation="vertical" />
|
||||||
delaySpeed={2000}
|
<span
|
||||||
/>
|
className={cn(
|
||||||
Your Homelab
|
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||||
</p>
|
`inline`,
|
||||||
<div className="flex gap-2 py-4">
|
)}
|
||||||
<Button asChild variant={"secondary"}>
|
>
|
||||||
<Link href="/scripts">Browse Scripts</Link>
|
Redesigned Website
|
||||||
</Button>
|
</span>
|
||||||
|
</AnimatedGradientText>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h1 className="max-w-2xl text-center text-5xl font-semibold tracking-tighter md:text-7xl">
|
||||||
|
Make managing your Homelab a breeze
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-2xl text-center text-lg leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||||
|
150+ scripts to help you manage your <b>Proxmox VE environment</b>
|
||||||
|
. Whether you're a seasoned user or a newcomer, Proxmox VE
|
||||||
|
Helper Scripts has got you covered.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<Link href="/scripts">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
variant="expandIcon"
|
||||||
|
Icon={CustomArrowRightIcon}
|
||||||
|
iconPlacement="right"
|
||||||
|
className="hover:"
|
||||||
|
>
|
||||||
|
View Scripts
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
components/magicui/animated-gradient-text.tsx
Normal file
26
components/magicui/animated-gradient-text.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function AnimatedGradientText({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
278
components/magicui/particles.tsx
Normal file
278
components/magicui/particles.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface MousePosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MousePosition(): MousePosition {
|
||||||
|
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mousePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParticlesProps {
|
||||||
|
className?: string;
|
||||||
|
quantity?: number;
|
||||||
|
staticity?: number;
|
||||||
|
ease?: number;
|
||||||
|
size?: number;
|
||||||
|
refresh?: boolean;
|
||||||
|
color?: string;
|
||||||
|
vx?: number;
|
||||||
|
vy?: number;
|
||||||
|
}
|
||||||
|
function hexToRgb(hex: string): number[] {
|
||||||
|
hex = hex.replace("#", "");
|
||||||
|
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split("")
|
||||||
|
.map((char) => char + char)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexInt = parseInt(hex, 16);
|
||||||
|
const red = (hexInt >> 16) & 255;
|
||||||
|
const green = (hexInt >> 8) & 255;
|
||||||
|
const blue = hexInt & 255;
|
||||||
|
return [red, green, blue];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Particles: React.FC<ParticlesProps> = ({
|
||||||
|
className = "",
|
||||||
|
quantity = 100,
|
||||||
|
staticity = 50,
|
||||||
|
ease = 50,
|
||||||
|
size = 0.4,
|
||||||
|
refresh = false,
|
||||||
|
color = "#ffffff",
|
||||||
|
vx = 0,
|
||||||
|
vy = 0,
|
||||||
|
}) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const context = useRef<CanvasRenderingContext2D | null>(null);
|
||||||
|
const circles = useRef<any[]>([]);
|
||||||
|
const mousePosition = MousePosition();
|
||||||
|
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
|
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
|
||||||
|
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
context.current = canvasRef.current.getContext("2d");
|
||||||
|
}
|
||||||
|
initCanvas();
|
||||||
|
animate();
|
||||||
|
window.addEventListener("resize", initCanvas);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", initCanvas);
|
||||||
|
};
|
||||||
|
}, [color]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onMouseMove();
|
||||||
|
}, [mousePosition.x, mousePosition.y]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initCanvas();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
const initCanvas = () => {
|
||||||
|
resizeCanvas();
|
||||||
|
drawParticles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = () => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const { w, h } = canvasSize.current;
|
||||||
|
const x = mousePosition.x - rect.left - w / 2;
|
||||||
|
const y = mousePosition.y - rect.top - h / 2;
|
||||||
|
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
||||||
|
if (inside) {
|
||||||
|
mouse.current.x = x;
|
||||||
|
mouse.current.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Circle = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
translateX: number;
|
||||||
|
translateY: number;
|
||||||
|
size: number;
|
||||||
|
alpha: number;
|
||||||
|
targetAlpha: number;
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
magnetism: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
||||||
|
circles.current.length = 0;
|
||||||
|
canvasSize.current.w = canvasContainerRef.current.offsetWidth;
|
||||||
|
canvasSize.current.h = canvasContainerRef.current.offsetHeight;
|
||||||
|
canvasRef.current.width = canvasSize.current.w * dpr;
|
||||||
|
canvasRef.current.height = canvasSize.current.h * dpr;
|
||||||
|
canvasRef.current.style.width = `${canvasSize.current.w}px`;
|
||||||
|
canvasRef.current.style.height = `${canvasSize.current.h}px`;
|
||||||
|
context.current.scale(dpr, dpr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const circleParams = (): Circle => {
|
||||||
|
const x = Math.floor(Math.random() * canvasSize.current.w);
|
||||||
|
const y = Math.floor(Math.random() * canvasSize.current.h);
|
||||||
|
const translateX = 0;
|
||||||
|
const translateY = 0;
|
||||||
|
const pSize = Math.floor(Math.random() * 2) + size;
|
||||||
|
const alpha = 0;
|
||||||
|
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||||
|
const dx = (Math.random() - 0.5) * 0.1;
|
||||||
|
const dy = (Math.random() - 0.5) * 0.1;
|
||||||
|
const magnetism = 0.1 + Math.random() * 4;
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
translateX,
|
||||||
|
translateY,
|
||||||
|
size: pSize,
|
||||||
|
alpha,
|
||||||
|
targetAlpha,
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
magnetism,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgb = hexToRgb(color);
|
||||||
|
|
||||||
|
const drawCircle = (circle: Circle, update = false) => {
|
||||||
|
if (context.current) {
|
||||||
|
const { x, y, translateX, translateY, size, alpha } = circle;
|
||||||
|
context.current.translate(translateX, translateY);
|
||||||
|
context.current.beginPath();
|
||||||
|
context.current.arc(x, y, size, 0, 2 * Math.PI);
|
||||||
|
context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
|
||||||
|
context.current.fill();
|
||||||
|
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
if (!update) {
|
||||||
|
circles.current.push(circle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearContext = () => {
|
||||||
|
if (context.current) {
|
||||||
|
context.current.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvasSize.current.w,
|
||||||
|
canvasSize.current.h,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawParticles = () => {
|
||||||
|
clearContext();
|
||||||
|
const particleCount = quantity;
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const circle = circleParams();
|
||||||
|
drawCircle(circle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const remapValue = (
|
||||||
|
value: number,
|
||||||
|
start1: number,
|
||||||
|
end1: number,
|
||||||
|
start2: number,
|
||||||
|
end2: number,
|
||||||
|
): number => {
|
||||||
|
const remapped =
|
||||||
|
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||||
|
return remapped > 0 ? remapped : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
clearContext();
|
||||||
|
circles.current.forEach((circle: Circle, i: number) => {
|
||||||
|
// Handle the alpha value
|
||||||
|
const edge = [
|
||||||
|
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||||
|
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||||
|
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||||
|
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||||
|
];
|
||||||
|
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||||
|
const remapClosestEdge = parseFloat(
|
||||||
|
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
||||||
|
);
|
||||||
|
if (remapClosestEdge > 1) {
|
||||||
|
circle.alpha += 0.02;
|
||||||
|
if (circle.alpha > circle.targetAlpha) {
|
||||||
|
circle.alpha = circle.targetAlpha;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||||
|
}
|
||||||
|
circle.x += circle.dx + vx;
|
||||||
|
circle.y += circle.dy + vy;
|
||||||
|
circle.translateX +=
|
||||||
|
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
|
||||||
|
ease;
|
||||||
|
circle.translateY +=
|
||||||
|
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
|
||||||
|
ease;
|
||||||
|
|
||||||
|
drawCircle(circle, true);
|
||||||
|
|
||||||
|
// circle gets out of the canvas
|
||||||
|
if (
|
||||||
|
circle.x < -circle.size ||
|
||||||
|
circle.x > canvasSize.current.w + circle.size ||
|
||||||
|
circle.y < -circle.size ||
|
||||||
|
circle.y > canvasSize.current.h + circle.size
|
||||||
|
) {
|
||||||
|
// remove the circle from the array
|
||||||
|
circles.current.splice(i, 1);
|
||||||
|
// create a new circle
|
||||||
|
const newCircle = circleParams();
|
||||||
|
drawCircle(newCircle);
|
||||||
|
// update the circle position
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} ref={canvasContainerRef} aria-hidden="true">
|
||||||
|
<canvas ref={canvasRef} className="size-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Particles;
|
||||||
61
components/magicui/shine-border.tsx
Normal file
61
components/magicui/shine-border.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type TColorProp = string | string[];
|
||||||
|
|
||||||
|
interface ShineBorderProps {
|
||||||
|
borderRadius?: number;
|
||||||
|
borderWidth?: number;
|
||||||
|
duration?: number;
|
||||||
|
color?: TColorProp;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Shine Border
|
||||||
|
* @description It is an animated background border effect component with easy to use and configurable props.
|
||||||
|
* @param borderRadius defines the radius of the border.
|
||||||
|
* @param borderWidth defines the width of the border.
|
||||||
|
* @param duration defines the animation duration to be applied on the shining border
|
||||||
|
* @param color a string or string array to define border color.
|
||||||
|
* @param className defines the class name to be applied to the component
|
||||||
|
* @param children contains react node elements.
|
||||||
|
*/
|
||||||
|
export default function ShineBorder({
|
||||||
|
borderRadius = 8,
|
||||||
|
borderWidth = 1,
|
||||||
|
duration = 14,
|
||||||
|
color = "#000000",
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: ShineBorderProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--border-radius": `${borderRadius}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--border-width": `${borderWidth}px`,
|
||||||
|
"--border-radius": `${borderRadius}px`,
|
||||||
|
"--shine-pulse-duration": `${duration}s`,
|
||||||
|
"--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||||
|
"--background-radial-gradient": `radial-gradient(transparent,transparent, ${color instanceof Array ? color.join(",") : color},transparent,transparent)`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={`before:bg-shine-size before:absolute before:inset-0 before:aspect-square before:size-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[background-image:--background-radial-gradient] before:[background-size:300%_300%] before:[mask:--mask-linear-gradient] motion-safe:before:animate-[shine-pulse_var(--shine-pulse-duration)_infinite_linear]`}
|
||||||
|
></div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -73,6 +73,21 @@ const config = {
|
|||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
to: { height: "0" },
|
to: { height: "0" },
|
||||||
},
|
},
|
||||||
|
shine: {
|
||||||
|
from: { backgroundPosition: "200% 0" },
|
||||||
|
to: { backgroundPosition: "-200% 0" },
|
||||||
|
},
|
||||||
|
"shine-pulse": {
|
||||||
|
"0%": {
|
||||||
|
"background-position": "0% 0%",
|
||||||
|
},
|
||||||
|
"50%": {
|
||||||
|
"background-position": "100% 100%",
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
"background-position": "0% 0%",
|
||||||
|
},
|
||||||
|
},
|
||||||
moveHorizontal: {
|
moveHorizontal: {
|
||||||
"0%": {
|
"0%": {
|
||||||
transform: "translateX(-50%) translateY(-10%)",
|
transform: "translateX(-50%) translateY(-10%)",
|
||||||
@@ -110,6 +125,7 @@ const config = {
|
|||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
shine: "shine 8s ease-in-out infinite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user