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