feat: add UI components for card stack, world map and evervault card with theme support
This commit is contained in:
48
src/components/ui/card-stack.tsx
Normal file
48
src/components/ui/card-stack.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type Card = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CardStack = ({
|
||||
items,
|
||||
offset,
|
||||
scaleFactor,
|
||||
}: {
|
||||
items: Card[];
|
||||
offset?: number;
|
||||
scaleFactor?: number;
|
||||
}) => {
|
||||
const CARD_OFFSET = offset || 10;
|
||||
const HORIZONTAL_OFFSET = 336; // Adjusted for 1/8 overlap
|
||||
const SCALE_FACTOR = scaleFactor || 0.06;
|
||||
|
||||
return (
|
||||
<div className="relative h-[20rem] w-full flex items-center justify-center">
|
||||
{items.map((card, index) => (
|
||||
<motion.div
|
||||
key={card.id}
|
||||
className="absolute dark:bg-black bg-white h-[16rem] w-[24rem] rounded-3xl p-4 shadow-xl border border-neutral-200 dark:border-white/[0.1] shadow-black/[0.1] dark:shadow-white/[0.05] flex flex-col justify-between"
|
||||
style={{
|
||||
transformOrigin: "top center",
|
||||
}}
|
||||
animate={{
|
||||
top: index * -CARD_OFFSET,
|
||||
left: index * HORIZONTAL_OFFSET,
|
||||
scale: 1 - index * SCALE_FACTOR,
|
||||
zIndex: items.length - index,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col p-4">
|
||||
<h3 className="text-base/7 font-semibold text-gray-900 dark:text-white">{card.name}</h3>
|
||||
<p className="mt-1 flex-auto text-base/7 text-gray-600 dark:text-neutral-300">{card.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
106
src/components/ui/evervault-card.tsx
Normal file
106
src/components/ui/evervault-card.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
import { useMotionValue } from "motion/react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useMotionTemplate, motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const EvervaultCard = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
let mouseX = useMotionValue(0);
|
||||
let mouseY = useMotionValue(0);
|
||||
|
||||
const [randomString, setRandomString] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let str = generateRandomString(1500);
|
||||
setRandomString(str);
|
||||
}, []);
|
||||
|
||||
function onMouseMove({ currentTarget, clientX, clientY }: any) {
|
||||
let { left, top } = currentTarget.getBoundingClientRect();
|
||||
mouseX.set(clientX - left);
|
||||
mouseY.set(clientY - top);
|
||||
|
||||
const str = generateRandomString(1500);
|
||||
setRandomString(str);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"p-0.5 bg-transparent aspect-square flex items-center justify-center w-full h-full relative",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onMouseMove={onMouseMove}
|
||||
className="group/card rounded-3xl w-full relative overflow-hidden bg-transparent flex items-center justify-center h-full"
|
||||
>
|
||||
<CardPattern
|
||||
mouseX={mouseX}
|
||||
mouseY={mouseY}
|
||||
randomString={randomString}
|
||||
/>
|
||||
<div className="relative z-10 flex items-center justify-center">
|
||||
<div className="relative p-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function CardPattern({ mouseX, mouseY, randomString }: any) {
|
||||
let maskImage = useMotionTemplate`radial-gradient(250px at ${mouseX}px ${mouseY}px, white, transparent)`;
|
||||
let style = { maskImage, WebkitMaskImage: maskImage };
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none">
|
||||
<div className="absolute inset-0 rounded-2xl [mask-image:linear-gradient(white,transparent)] group-hover/card:opacity-50"></div>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-green-500 to-blue-700 opacity-0 group-hover/card:opacity-100 backdrop-blur-xl transition duration-500"
|
||||
style={style}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl opacity-0 mix-blend-overlay group-hover/card:opacity-100"
|
||||
style={style}
|
||||
>
|
||||
<p className="absolute inset-x-0 text-xs h-full break-words whitespace-pre-wrap text-white font-mono font-bold transition duration-500">
|
||||
{randomString}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
export const generateRandomString = (length: number) => {
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const Icon = ({ className, ...rest }: any) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
170
src/components/ui/world-map.tsx
Normal file
170
src/components/ui/world-map.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import DottedMap from "dotted-map";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
interface MapProps {
|
||||
dots?: Array<{
|
||||
start: { lat: number; lng: number; label?: string };
|
||||
end: { lat: number; lng: number; label?: string };
|
||||
}>;
|
||||
lineColor?: string;
|
||||
}
|
||||
|
||||
export default function WorldMap({
|
||||
dots = [],
|
||||
lineColor = "#06b6d4",
|
||||
}: MapProps) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const map = new DottedMap({ height: 100, grid: "diagonal" });
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const svgMap = map.getSVG({
|
||||
radius: 0.22,
|
||||
color: theme === "dark" ? "#FFFFFF40" : "#00000040",
|
||||
shape: "circle",
|
||||
backgroundColor: theme === "dark" ? "black" : "white",
|
||||
});
|
||||
|
||||
const projectPoint = (lat: number, lng: number) => {
|
||||
const x = (lng + 180) * (800 / 360);
|
||||
const y = (90 - lat) * (400 / 180);
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const createCurvedPath = (
|
||||
start: { x: number; y: number },
|
||||
end: { x: number; y: number }
|
||||
) => {
|
||||
const midX = (start.x + end.x) / 2;
|
||||
const midY = Math.min(start.y, end.y) - 50;
|
||||
return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full aspect-[2/1] dark:bg-black bg-white rounded-lg relative font-sans">
|
||||
<img
|
||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`}
|
||||
className="h-full w-full [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_90%,transparent)] pointer-events-none select-none"
|
||||
alt="world map"
|
||||
height="495"
|
||||
width="1056"
|
||||
draggable={false}
|
||||
/>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox="0 0 800 400"
|
||||
className="w-full h-full absolute inset-0 pointer-events-none select-none"
|
||||
>
|
||||
{dots.map((dot, i) => {
|
||||
const startPoint = projectPoint(dot.start.lat, dot.start.lng);
|
||||
const endPoint = projectPoint(dot.end.lat, dot.end.lng);
|
||||
return (
|
||||
<g key={`path-group-${i}`}>
|
||||
<motion.path
|
||||
d={createCurvedPath(startPoint, endPoint)}
|
||||
fill="none"
|
||||
stroke="url(#path-gradient)"
|
||||
strokeWidth="1"
|
||||
initial={{
|
||||
pathLength: 0,
|
||||
}}
|
||||
animate={{
|
||||
pathLength: 1,
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
delay: 0.5 * i,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
key={`start-upper-${i}`}
|
||||
></motion.path>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
<defs>
|
||||
<linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="white" stopOpacity="0" />
|
||||
<stop offset="5%" stopColor={lineColor} stopOpacity="1" />
|
||||
<stop offset="95%" stopColor={lineColor} stopOpacity="1" />
|
||||
<stop offset="100%" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{dots.map((dot, i) => (
|
||||
<g key={`points-group-${i}`}>
|
||||
<g key={`start-${i}`}>
|
||||
<circle
|
||||
cx={projectPoint(dot.start.lat, dot.start.lng).x}
|
||||
cy={projectPoint(dot.start.lat, dot.start.lng).y}
|
||||
r="2"
|
||||
fill={lineColor}
|
||||
/>
|
||||
<circle
|
||||
cx={projectPoint(dot.start.lat, dot.start.lng).x}
|
||||
cy={projectPoint(dot.start.lat, dot.start.lng).y}
|
||||
r="2"
|
||||
fill={lineColor}
|
||||
opacity="0.5"
|
||||
>
|
||||
<animate
|
||||
attributeName="r"
|
||||
from="2"
|
||||
to="8"
|
||||
dur="1.5s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
from="0.5"
|
||||
to="0"
|
||||
dur="1.5s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
<g key={`end-${i}`}>
|
||||
<circle
|
||||
cx={projectPoint(dot.end.lat, dot.end.lng).x}
|
||||
cy={projectPoint(dot.end.lat, dot.end.lng).y}
|
||||
r="2"
|
||||
fill={lineColor}
|
||||
/>
|
||||
<circle
|
||||
cx={projectPoint(dot.end.lat, dot.end.lng).x}
|
||||
cy={projectPoint(dot.end.lat, dot.end.lng).y}
|
||||
r="2"
|
||||
fill={lineColor}
|
||||
opacity="0.5"
|
||||
>
|
||||
<animate
|
||||
attributeName="r"
|
||||
from="2"
|
||||
to="8"
|
||||
dur="1.5s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
from="0.5"
|
||||
to="0"
|
||||
dur="1.5s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user