Spaces:
Running
Running
Florin Bobiș
commited on
Commit
•
6528c1e
1
Parent(s):
6a37227
landing
Browse files- package-lock.json +34 -0
- package.json +2 -0
- public/segment-after.jpg +0 -0
- public/segment-before.jpg +0 -0
- src/app/page.tsx +22 -35
- src/components/ui/compare.tsx +240 -0
- src/components/ui/cover.tsx +228 -0
- src/components/ui/shooting-stars.tsx +146 -0
- src/components/ui/stars-background.tsx +143 -0
- src/components/welcome-section.tsx +54 -0
package-lock.json
CHANGED
@@ -12,6 +12,7 @@
|
|
12 |
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
13 |
"@radix-ui/react-icons": "^1.3.0",
|
14 |
"@radix-ui/react-slot": "^1.1.0",
|
|
|
15 |
"@tsparticles/engine": "^3.5.0",
|
16 |
"@tsparticles/react": "^3.0.0",
|
17 |
"@tsparticles/slim": "^3.5.0",
|
@@ -23,6 +24,7 @@
|
|
23 |
"next-themes": "^0.3.0",
|
24 |
"react": "^18",
|
25 |
"react-dom": "^18",
|
|
|
26 |
"tailwind-merge": "^2.5.2",
|
27 |
"tailwindcss-animate": "^1.0.7"
|
28 |
},
|
@@ -1081,6 +1083,32 @@
|
|
1081 |
"tslib": "^2.4.0"
|
1082 |
}
|
1083 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1084 |
"node_modules/@tsparticles/basic": {
|
1085 |
"version": "3.5.0",
|
1086 |
"resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.5.0.tgz",
|
@@ -5547,6 +5575,12 @@
|
|
5547 |
"url": "https://github.com/sponsors/isaacs"
|
5548 |
}
|
5549 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
5550 |
"node_modules/source-map-js": {
|
5551 |
"version": "1.2.1",
|
5552 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
12 |
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
13 |
"@radix-ui/react-icons": "^1.3.0",
|
14 |
"@radix-ui/react-slot": "^1.1.0",
|
15 |
+
"@tabler/icons-react": "^3.17.0",
|
16 |
"@tsparticles/engine": "^3.5.0",
|
17 |
"@tsparticles/react": "^3.0.0",
|
18 |
"@tsparticles/slim": "^3.5.0",
|
|
|
24 |
"next-themes": "^0.3.0",
|
25 |
"react": "^18",
|
26 |
"react-dom": "^18",
|
27 |
+
"simplex-noise": "^4.0.3",
|
28 |
"tailwind-merge": "^2.5.2",
|
29 |
"tailwindcss-animate": "^1.0.7"
|
30 |
},
|
|
|
1083 |
"tslib": "^2.4.0"
|
1084 |
}
|
1085 |
},
|
1086 |
+
"node_modules/@tabler/icons": {
|
1087 |
+
"version": "3.17.0",
|
1088 |
+
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.17.0.tgz",
|
1089 |
+
"integrity": "sha512-sCSfAQ0w93KSnSL7tS08n73CdIKpuHP8foeLMWgDKiZaCs8ZE//N3ytazCk651ZtruTtByI3b+ZDj7nRf+hHvA==",
|
1090 |
+
"license": "MIT",
|
1091 |
+
"funding": {
|
1092 |
+
"type": "github",
|
1093 |
+
"url": "https://github.com/sponsors/codecalm"
|
1094 |
+
}
|
1095 |
+
},
|
1096 |
+
"node_modules/@tabler/icons-react": {
|
1097 |
+
"version": "3.17.0",
|
1098 |
+
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.17.0.tgz",
|
1099 |
+
"integrity": "sha512-Ndm9Htv7KpIU1PYYrzs5EMhyA3aZGcgaxUp9Q1XOxcRZ+I0X+Ub2WS5f4bkRyDdL1s0++k2T9XRgmg2pG113sw==",
|
1100 |
+
"license": "MIT",
|
1101 |
+
"dependencies": {
|
1102 |
+
"@tabler/icons": "3.17.0"
|
1103 |
+
},
|
1104 |
+
"funding": {
|
1105 |
+
"type": "github",
|
1106 |
+
"url": "https://github.com/sponsors/codecalm"
|
1107 |
+
},
|
1108 |
+
"peerDependencies": {
|
1109 |
+
"react": ">= 16"
|
1110 |
+
}
|
1111 |
+
},
|
1112 |
"node_modules/@tsparticles/basic": {
|
1113 |
"version": "3.5.0",
|
1114 |
"resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.5.0.tgz",
|
|
|
5575 |
"url": "https://github.com/sponsors/isaacs"
|
5576 |
}
|
5577 |
},
|
5578 |
+
"node_modules/simplex-noise": {
|
5579 |
+
"version": "4.0.3",
|
5580 |
+
"resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz",
|
5581 |
+
"integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==",
|
5582 |
+
"license": "MIT"
|
5583 |
+
},
|
5584 |
"node_modules/source-map-js": {
|
5585 |
"version": "1.2.1",
|
5586 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
package.json
CHANGED
@@ -13,6 +13,7 @@
|
|
13 |
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
14 |
"@radix-ui/react-icons": "^1.3.0",
|
15 |
"@radix-ui/react-slot": "^1.1.0",
|
|
|
16 |
"@tsparticles/engine": "^3.5.0",
|
17 |
"@tsparticles/react": "^3.0.0",
|
18 |
"@tsparticles/slim": "^3.5.0",
|
@@ -24,6 +25,7 @@
|
|
24 |
"next-themes": "^0.3.0",
|
25 |
"react": "^18",
|
26 |
"react-dom": "^18",
|
|
|
27 |
"tailwind-merge": "^2.5.2",
|
28 |
"tailwindcss-animate": "^1.0.7"
|
29 |
},
|
|
|
13 |
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
14 |
"@radix-ui/react-icons": "^1.3.0",
|
15 |
"@radix-ui/react-slot": "^1.1.0",
|
16 |
+
"@tabler/icons-react": "^3.17.0",
|
17 |
"@tsparticles/engine": "^3.5.0",
|
18 |
"@tsparticles/react": "^3.0.0",
|
19 |
"@tsparticles/slim": "^3.5.0",
|
|
|
25 |
"next-themes": "^0.3.0",
|
26 |
"react": "^18",
|
27 |
"react-dom": "^18",
|
28 |
+
"simplex-noise": "^4.0.3",
|
29 |
"tailwind-merge": "^2.5.2",
|
30 |
"tailwindcss-animate": "^1.0.7"
|
31 |
},
|
public/segment-after.jpg
ADDED
public/segment-before.jpg
ADDED
src/app/page.tsx
CHANGED
@@ -1,7 +1,9 @@
|
|
1 |
import Footer from "@/components/footer";
|
2 |
import Header from "@/components/header";
|
|
|
3 |
import { ContainerScroll } from "@/components/ui/container-scroll-animation";
|
4 |
-
import {
|
|
|
5 |
import Image from "next/image";
|
6 |
|
7 |
export default function Home() {
|
@@ -9,42 +11,12 @@ export default function Home() {
|
|
9 |
<div className="flex min-h-screen w-full flex-col">
|
10 |
<Header />
|
11 |
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col">
|
12 |
-
<
|
13 |
-
|
14 |
-
ATOM
|
15 |
-
</h1>
|
16 |
-
<div className="w-[40rem] h-40 relative">
|
17 |
-
{/* Gradients */}
|
18 |
-
<div className="absolute inset-x-20 top-0 bg-gradient-to-r from-transparent via-indigo-500 to-transparent h-[2px] w-3/4 blur-sm" />
|
19 |
-
<div className="absolute inset-x-20 top-0 bg-gradient-to-r from-transparent via-indigo-500 to-transparent h-px w-3/4" />
|
20 |
-
<div className="absolute inset-x-60 top-0 bg-gradient-to-r from-transparent via-sky-500 to-transparent h-[5px] w-1/4 blur-sm" />
|
21 |
-
<div className="absolute inset-x-60 top-0 bg-gradient-to-r from-transparent via-sky-500 to-transparent h-px w-1/4" />
|
22 |
-
|
23 |
-
{/* Core component */}
|
24 |
-
<SparklesCore
|
25 |
-
background="transparent"
|
26 |
-
minSize={0.4}
|
27 |
-
maxSize={1}
|
28 |
-
particleDensity={1200}
|
29 |
-
className="w-full h-full"
|
30 |
-
particleColor="#FFFFFF"
|
31 |
-
/>
|
32 |
-
|
33 |
-
<h3 className="mt-[-9rem] pb-1 md:text-xl text-sm lg:text-3xl bg-clip-text text-transparent bg-gradient-to-b from-gray-800 to-gray-500 dark:from-neutral-200 dark:to-neutral-600 text-center font-sans font-bold relative z-20">
|
34 |
-
Your one-stop shop
|
35 |
-
<br />
|
36 |
-
for gravity-free design...
|
37 |
-
</h3>
|
38 |
-
|
39 |
-
{/* Radial Gradient to prevent sharp edges */}
|
40 |
-
<div className="absolute inset-0 w-full h-full bg-black [mask-image:radial-gradient(350px_200px_at_top,transparent_20%,white)]"></div>
|
41 |
-
</div>
|
42 |
-
</div>
|
43 |
-
<div className="flex flex-col overflow-hidden bg-black">
|
44 |
<ContainerScroll
|
45 |
titleComponent={
|
46 |
<>
|
47 |
-
<h1 className="text-4xl font-semibold text-
|
48 |
Discover the power of <br />
|
49 |
<span className="text-4xl md:text-[6rem] font-bold mt-1 leading-none">
|
50 |
Microgravity
|
@@ -62,7 +34,22 @@ export default function Home() {
|
|
62 |
draggable={false}
|
63 |
/>
|
64 |
</ContainerScroll>
|
65 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
</main>
|
67 |
<Footer />
|
68 |
</div>
|
|
|
1 |
import Footer from "@/components/footer";
|
2 |
import Header from "@/components/header";
|
3 |
+
import { Compare } from "@/components/ui/compare";
|
4 |
import { ContainerScroll } from "@/components/ui/container-scroll-animation";
|
5 |
+
import { Cover } from "@/components/ui/cover";
|
6 |
+
import WelcomeSection from "@/components/welcome-section";
|
7 |
import Image from "next/image";
|
8 |
|
9 |
export default function Home() {
|
|
|
11 |
<div className="flex min-h-screen w-full flex-col">
|
12 |
<Header />
|
13 |
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col">
|
14 |
+
<WelcomeSection />
|
15 |
+
<section className="flex flex-col overflow-hidden bg-background dark:bg-black">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
<ContainerScroll
|
17 |
titleComponent={
|
18 |
<>
|
19 |
+
<h1 className="p-2 text-4xl font-semibold bg-clip-text text-transparent text-center bg-gradient-to-b from-neutral-900 to-neutral-700 dark:from-neutral-600 dark:to-white">
|
20 |
Discover the power of <br />
|
21 |
<span className="text-4xl md:text-[6rem] font-bold mt-1 leading-none">
|
22 |
Microgravity
|
|
|
34 |
draggable={false}
|
35 |
/>
|
36 |
</ContainerScroll>
|
37 |
+
</section>
|
38 |
+
<section className="flex flex-col overflow-hidden bg-background dark:bg-black h-[calc(100vh_-_theme(spacing.16))] items-center justify-center">
|
39 |
+
<h1 className="text-4xl md:text-4xl lg:text-6xl font-semibold max-w-7xl mx-auto text-center mt-6 relative z-20 py-6 bg-clip-text text-transparent bg-gradient-to-b from-neutral-800 via-neutral-700 to-neutral-700 dark:from-neutral-800 dark:via-white dark:to-white">
|
40 |
+
Get amazing insights <br /> at <Cover>warp speed</Cover>
|
41 |
+
</h1>
|
42 |
+
<div className="p-4 border rounded-3xl dark:bg-neutral-900 bg-neutral-100 border-neutral-200 dark:border-neutral-800 px-4">
|
43 |
+
<Compare
|
44 |
+
firstImage="/segment-before.jpg"
|
45 |
+
secondImage="/segment-after.jpg"
|
46 |
+
firstImageClassName="object-cover object-left-top"
|
47 |
+
secondImageClassname="object-cover object-left-top"
|
48 |
+
className="h-[250px] w-[200px] md:h-[500px] md:w-[500px]"
|
49 |
+
slideMode="hover"
|
50 |
+
/>
|
51 |
+
</div>
|
52 |
+
</section>
|
53 |
</main>
|
54 |
<Footer />
|
55 |
</div>
|
src/components/ui/compare.tsx
ADDED
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
3 |
+
import { SparklesCore } from "@/components/ui/sparkles";
|
4 |
+
import { AnimatePresence, motion } from "framer-motion";
|
5 |
+
import { cn } from "@/lib/utils";
|
6 |
+
import { IconDotsVertical } from "@tabler/icons-react";
|
7 |
+
|
8 |
+
interface CompareProps {
|
9 |
+
firstImage?: string;
|
10 |
+
secondImage?: string;
|
11 |
+
className?: string;
|
12 |
+
firstImageClassName?: string;
|
13 |
+
secondImageClassname?: string;
|
14 |
+
initialSliderPercentage?: number;
|
15 |
+
slideMode?: "hover" | "drag";
|
16 |
+
showHandlebar?: boolean;
|
17 |
+
autoplay?: boolean;
|
18 |
+
autoplayDuration?: number;
|
19 |
+
}
|
20 |
+
export const Compare = ({
|
21 |
+
firstImage = "",
|
22 |
+
secondImage = "",
|
23 |
+
className,
|
24 |
+
firstImageClassName,
|
25 |
+
secondImageClassname,
|
26 |
+
initialSliderPercentage = 50,
|
27 |
+
slideMode = "hover",
|
28 |
+
showHandlebar = true,
|
29 |
+
autoplay = false,
|
30 |
+
autoplayDuration = 5000,
|
31 |
+
}: CompareProps) => {
|
32 |
+
const [sliderXPercent, setSliderXPercent] = useState(initialSliderPercentage);
|
33 |
+
const [isDragging, setIsDragging] = useState(false);
|
34 |
+
|
35 |
+
const sliderRef = useRef<HTMLDivElement>(null);
|
36 |
+
|
37 |
+
const [isMouseOver, setIsMouseOver] = useState(false);
|
38 |
+
|
39 |
+
const autoplayRef = useRef<NodeJS.Timeout | null>(null);
|
40 |
+
|
41 |
+
const startAutoplay = useCallback(() => {
|
42 |
+
if (!autoplay) return;
|
43 |
+
|
44 |
+
const startTime = Date.now();
|
45 |
+
const animate = () => {
|
46 |
+
const elapsedTime = Date.now() - startTime;
|
47 |
+
const progress =
|
48 |
+
(elapsedTime % (autoplayDuration * 2)) / autoplayDuration;
|
49 |
+
const percentage = progress <= 1 ? progress * 100 : (2 - progress) * 100;
|
50 |
+
|
51 |
+
setSliderXPercent(percentage);
|
52 |
+
autoplayRef.current = setTimeout(animate, 16); // ~60fps
|
53 |
+
};
|
54 |
+
|
55 |
+
animate();
|
56 |
+
}, [autoplay, autoplayDuration]);
|
57 |
+
|
58 |
+
const stopAutoplay = useCallback(() => {
|
59 |
+
if (autoplayRef.current) {
|
60 |
+
clearTimeout(autoplayRef.current);
|
61 |
+
autoplayRef.current = null;
|
62 |
+
}
|
63 |
+
}, []);
|
64 |
+
|
65 |
+
useEffect(() => {
|
66 |
+
startAutoplay();
|
67 |
+
return () => stopAutoplay();
|
68 |
+
}, [startAutoplay, stopAutoplay]);
|
69 |
+
|
70 |
+
function mouseEnterHandler() {
|
71 |
+
setIsMouseOver(true);
|
72 |
+
stopAutoplay();
|
73 |
+
}
|
74 |
+
|
75 |
+
function mouseLeaveHandler() {
|
76 |
+
setIsMouseOver(false);
|
77 |
+
if (slideMode === "hover") {
|
78 |
+
setSliderXPercent(initialSliderPercentage);
|
79 |
+
}
|
80 |
+
if (slideMode === "drag") {
|
81 |
+
setIsDragging(false);
|
82 |
+
}
|
83 |
+
startAutoplay();
|
84 |
+
}
|
85 |
+
|
86 |
+
const handleStart = useCallback(
|
87 |
+
(clientX: number) => {
|
88 |
+
if (slideMode === "drag") {
|
89 |
+
setIsDragging(true);
|
90 |
+
}
|
91 |
+
},
|
92 |
+
[slideMode]
|
93 |
+
);
|
94 |
+
|
95 |
+
const handleEnd = useCallback(() => {
|
96 |
+
if (slideMode === "drag") {
|
97 |
+
setIsDragging(false);
|
98 |
+
}
|
99 |
+
}, [slideMode]);
|
100 |
+
|
101 |
+
const handleMove = useCallback(
|
102 |
+
(clientX: number) => {
|
103 |
+
if (!sliderRef.current) return;
|
104 |
+
if (slideMode === "hover" || (slideMode === "drag" && isDragging)) {
|
105 |
+
const rect = sliderRef.current.getBoundingClientRect();
|
106 |
+
const x = clientX - rect.left;
|
107 |
+
const percent = (x / rect.width) * 100;
|
108 |
+
requestAnimationFrame(() => {
|
109 |
+
setSliderXPercent(Math.max(0, Math.min(100, percent)));
|
110 |
+
});
|
111 |
+
}
|
112 |
+
},
|
113 |
+
[slideMode, isDragging]
|
114 |
+
);
|
115 |
+
|
116 |
+
const handleMouseDown = useCallback(
|
117 |
+
(e: React.MouseEvent) => handleStart(e.clientX),
|
118 |
+
[handleStart]
|
119 |
+
);
|
120 |
+
const handleMouseUp = useCallback(() => handleEnd(), [handleEnd]);
|
121 |
+
const handleMouseMove = useCallback(
|
122 |
+
(e: React.MouseEvent) => handleMove(e.clientX),
|
123 |
+
[handleMove]
|
124 |
+
);
|
125 |
+
|
126 |
+
const handleTouchStart = useCallback(
|
127 |
+
(e: React.TouchEvent) => {
|
128 |
+
if (!autoplay) {
|
129 |
+
handleStart(e.touches[0].clientX);
|
130 |
+
}
|
131 |
+
},
|
132 |
+
[handleStart, autoplay]
|
133 |
+
);
|
134 |
+
|
135 |
+
const handleTouchEnd = useCallback(() => {
|
136 |
+
if (!autoplay) {
|
137 |
+
handleEnd();
|
138 |
+
}
|
139 |
+
}, [handleEnd, autoplay]);
|
140 |
+
|
141 |
+
const handleTouchMove = useCallback(
|
142 |
+
(e: React.TouchEvent) => {
|
143 |
+
if (!autoplay) {
|
144 |
+
handleMove(e.touches[0].clientX);
|
145 |
+
}
|
146 |
+
},
|
147 |
+
[handleMove, autoplay]
|
148 |
+
);
|
149 |
+
|
150 |
+
return (
|
151 |
+
<div
|
152 |
+
ref={sliderRef}
|
153 |
+
className={cn("w-[400px] h-[400px] overflow-hidden", className)}
|
154 |
+
style={{
|
155 |
+
position: "relative",
|
156 |
+
cursor: slideMode === "drag" ? "grab" : "col-resize",
|
157 |
+
}}
|
158 |
+
onMouseMove={handleMouseMove}
|
159 |
+
onMouseLeave={mouseLeaveHandler}
|
160 |
+
onMouseEnter={mouseEnterHandler}
|
161 |
+
onMouseDown={handleMouseDown}
|
162 |
+
onMouseUp={handleMouseUp}
|
163 |
+
onTouchStart={handleTouchStart}
|
164 |
+
onTouchEnd={handleTouchEnd}
|
165 |
+
onTouchMove={handleTouchMove}
|
166 |
+
>
|
167 |
+
<AnimatePresence initial={false}>
|
168 |
+
<motion.div
|
169 |
+
className="h-full w-px absolute top-0 m-auto z-30 bg-gradient-to-b from-transparent from-[5%] to-[95%] via-indigo-500 to-transparent"
|
170 |
+
style={{
|
171 |
+
left: `${sliderXPercent}%`,
|
172 |
+
top: "0",
|
173 |
+
zIndex: 40,
|
174 |
+
}}
|
175 |
+
transition={{ duration: 0 }}
|
176 |
+
>
|
177 |
+
<div className="w-36 h-full [mask-image:radial-gradient(100px_at_left,white,transparent)] absolute top-1/2 -translate-y-1/2 left-0 bg-gradient-to-r from-indigo-400 via-transparent to-transparent z-20 opacity-50" />
|
178 |
+
<div className="w-10 h-1/2 [mask-image:radial-gradient(50px_at_left,white,transparent)] absolute top-1/2 -translate-y-1/2 left-0 bg-gradient-to-r from-cyan-400 via-transparent to-transparent z-10 opacity-100" />
|
179 |
+
<div className="w-10 h-3/4 top-1/2 -translate-y-1/2 absolute -right-10 [mask-image:radial-gradient(100px_at_left,white,transparent)]">
|
180 |
+
<MemoizedSparklesCore
|
181 |
+
background="transparent"
|
182 |
+
minSize={0.4}
|
183 |
+
maxSize={1}
|
184 |
+
particleDensity={1200}
|
185 |
+
className="w-full h-full"
|
186 |
+
particleColor="#FFFFFF"
|
187 |
+
/>
|
188 |
+
</div>
|
189 |
+
{showHandlebar && (
|
190 |
+
<div className="h-5 w-5 rounded-md top-1/2 -translate-y-1/2 bg-white z-30 -right-2.5 absolute flex items-center justify-center shadow-[0px_-1px_0px_0px_#FFFFFF40]">
|
191 |
+
<IconDotsVertical className="h-4 w-4 text-black" />
|
192 |
+
</div>
|
193 |
+
)}
|
194 |
+
</motion.div>
|
195 |
+
</AnimatePresence>
|
196 |
+
<div className="overflow-hidden w-full h-full relative z-20 pointer-events-none">
|
197 |
+
<AnimatePresence initial={false}>
|
198 |
+
{firstImage ? (
|
199 |
+
<motion.div
|
200 |
+
className={cn(
|
201 |
+
"absolute inset-0 z-20 rounded-2xl flex-shrink-0 w-full h-full select-none overflow-hidden",
|
202 |
+
firstImageClassName
|
203 |
+
)}
|
204 |
+
style={{
|
205 |
+
clipPath: `inset(0 ${100 - sliderXPercent}% 0 0)`,
|
206 |
+
}}
|
207 |
+
transition={{ duration: 0 }}
|
208 |
+
>
|
209 |
+
<img
|
210 |
+
alt="first image"
|
211 |
+
src={firstImage}
|
212 |
+
className={cn(
|
213 |
+
"absolute inset-0 z-20 rounded-2xl flex-shrink-0 w-full h-full select-none",
|
214 |
+
firstImageClassName
|
215 |
+
)}
|
216 |
+
draggable={false}
|
217 |
+
/>
|
218 |
+
</motion.div>
|
219 |
+
) : null}
|
220 |
+
</AnimatePresence>
|
221 |
+
</div>
|
222 |
+
|
223 |
+
<AnimatePresence initial={false}>
|
224 |
+
{secondImage ? (
|
225 |
+
<motion.img
|
226 |
+
className={cn(
|
227 |
+
"absolute top-0 left-0 z-[19] rounded-2xl w-full h-full select-none",
|
228 |
+
secondImageClassname
|
229 |
+
)}
|
230 |
+
alt="second image"
|
231 |
+
src={secondImage}
|
232 |
+
draggable={false}
|
233 |
+
/>
|
234 |
+
) : null}
|
235 |
+
</AnimatePresence>
|
236 |
+
</div>
|
237 |
+
);
|
238 |
+
};
|
239 |
+
|
240 |
+
const MemoizedSparklesCore = React.memo(SparklesCore);
|
src/components/ui/cover.tsx
ADDED
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import React, { useEffect, useId, useState } from "react";
|
3 |
+
import { AnimatePresence, motion } from "framer-motion";
|
4 |
+
import { useRef } from "react";
|
5 |
+
import { cn } from "@/lib/utils";
|
6 |
+
import { SparklesCore } from "@/components/ui/sparkles";
|
7 |
+
|
8 |
+
export const Cover = ({
|
9 |
+
children,
|
10 |
+
className,
|
11 |
+
}: {
|
12 |
+
children?: React.ReactNode;
|
13 |
+
className?: string;
|
14 |
+
}) => {
|
15 |
+
const [hovered, setHovered] = useState(false);
|
16 |
+
|
17 |
+
const ref = useRef<HTMLDivElement>(null);
|
18 |
+
|
19 |
+
const [containerWidth, setContainerWidth] = useState(0);
|
20 |
+
const [beamPositions, setBeamPositions] = useState<number[]>([]);
|
21 |
+
|
22 |
+
useEffect(() => {
|
23 |
+
if (ref.current) {
|
24 |
+
setContainerWidth(ref.current?.clientWidth ?? 0);
|
25 |
+
|
26 |
+
const height = ref.current?.clientHeight ?? 0;
|
27 |
+
const numberOfBeams = Math.floor(height / 10); // Adjust the divisor to control the spacing
|
28 |
+
const positions = Array.from(
|
29 |
+
{ length: numberOfBeams },
|
30 |
+
(_, i) => (i + 1) * (height / (numberOfBeams + 1))
|
31 |
+
);
|
32 |
+
setBeamPositions(positions);
|
33 |
+
}
|
34 |
+
}, [ref.current]);
|
35 |
+
|
36 |
+
return (
|
37 |
+
<div
|
38 |
+
onMouseEnter={() => setHovered(true)}
|
39 |
+
onMouseLeave={() => setHovered(false)}
|
40 |
+
ref={ref}
|
41 |
+
className="relative hover:bg-neutral-900 group/cover inline-block dark:bg-neutral-900 bg-neutral-100 px-2 py-2 transition duration-200 rounded-sm"
|
42 |
+
>
|
43 |
+
<AnimatePresence>
|
44 |
+
{hovered && (
|
45 |
+
<motion.div
|
46 |
+
initial={{ opacity: 0 }}
|
47 |
+
animate={{ opacity: 1 }}
|
48 |
+
exit={{ opacity: 0 }}
|
49 |
+
transition={{
|
50 |
+
opacity: {
|
51 |
+
duration: 0.2,
|
52 |
+
},
|
53 |
+
}}
|
54 |
+
className="h-full w-full overflow-hidden absolute inset-0"
|
55 |
+
>
|
56 |
+
<motion.div
|
57 |
+
animate={{
|
58 |
+
translateX: ["-50%", "0%"],
|
59 |
+
}}
|
60 |
+
transition={{
|
61 |
+
translateX: {
|
62 |
+
duration: 10,
|
63 |
+
ease: "linear",
|
64 |
+
repeat: Infinity,
|
65 |
+
},
|
66 |
+
}}
|
67 |
+
className="w-[200%] h-full flex"
|
68 |
+
>
|
69 |
+
<SparklesCore
|
70 |
+
background="transparent"
|
71 |
+
minSize={0.4}
|
72 |
+
maxSize={1}
|
73 |
+
particleDensity={500}
|
74 |
+
className="w-full h-full"
|
75 |
+
particleColor="#FFFFFF"
|
76 |
+
/>
|
77 |
+
<SparklesCore
|
78 |
+
background="transparent"
|
79 |
+
minSize={0.4}
|
80 |
+
maxSize={1}
|
81 |
+
particleDensity={500}
|
82 |
+
className="w-full h-full"
|
83 |
+
particleColor="#FFFFFF"
|
84 |
+
/>
|
85 |
+
</motion.div>
|
86 |
+
</motion.div>
|
87 |
+
)}
|
88 |
+
</AnimatePresence>
|
89 |
+
{beamPositions.map((position, index) => (
|
90 |
+
<Beam
|
91 |
+
key={index}
|
92 |
+
hovered={hovered}
|
93 |
+
duration={Math.random() * 2 + 1}
|
94 |
+
delay={Math.random() * 2 + 1}
|
95 |
+
width={containerWidth}
|
96 |
+
style={{
|
97 |
+
top: `${position}px`,
|
98 |
+
}}
|
99 |
+
/>
|
100 |
+
))}
|
101 |
+
<motion.span
|
102 |
+
key={String(hovered)}
|
103 |
+
animate={{
|
104 |
+
scale: hovered ? 0.8 : 1,
|
105 |
+
x: hovered ? [0, -30, 30, -30, 30, 0] : 0,
|
106 |
+
y: hovered ? [0, 30, -30, 30, -30, 0] : 0,
|
107 |
+
}}
|
108 |
+
exit={{
|
109 |
+
filter: "none",
|
110 |
+
scale: 1,
|
111 |
+
x: 0,
|
112 |
+
y: 0,
|
113 |
+
}}
|
114 |
+
transition={{
|
115 |
+
duration: 0.2,
|
116 |
+
x: {
|
117 |
+
duration: 0.2,
|
118 |
+
repeat: Infinity,
|
119 |
+
repeatType: "loop",
|
120 |
+
},
|
121 |
+
y: {
|
122 |
+
duration: 0.2,
|
123 |
+
repeat: Infinity,
|
124 |
+
repeatType: "loop",
|
125 |
+
},
|
126 |
+
scale: {
|
127 |
+
duration: 0.2,
|
128 |
+
},
|
129 |
+
filter: {
|
130 |
+
duration: 0.2,
|
131 |
+
},
|
132 |
+
}}
|
133 |
+
className={cn(
|
134 |
+
"dark:text-white inline-block text-neutral-900 relative z-20 group-hover/cover:text-white transition duration-200",
|
135 |
+
className
|
136 |
+
)}
|
137 |
+
>
|
138 |
+
{children}
|
139 |
+
</motion.span>
|
140 |
+
<CircleIcon className="absolute -right-[2px] -top-[2px]" />
|
141 |
+
<CircleIcon className="absolute -bottom-[2px] -right-[2px]" delay={0.4} />
|
142 |
+
<CircleIcon className="absolute -left-[2px] -top-[2px]" delay={0.8} />
|
143 |
+
<CircleIcon className="absolute -bottom-[2px] -left-[2px]" delay={1.6} />
|
144 |
+
</div>
|
145 |
+
);
|
146 |
+
};
|
147 |
+
|
148 |
+
export const Beam = ({
|
149 |
+
className,
|
150 |
+
delay,
|
151 |
+
duration,
|
152 |
+
hovered,
|
153 |
+
width = 600,
|
154 |
+
...svgProps
|
155 |
+
}: {
|
156 |
+
className?: string;
|
157 |
+
delay?: number;
|
158 |
+
duration?: number;
|
159 |
+
hovered?: boolean;
|
160 |
+
width?: number;
|
161 |
+
} & React.ComponentProps<typeof motion.svg>) => {
|
162 |
+
const id = useId();
|
163 |
+
|
164 |
+
return (
|
165 |
+
<motion.svg
|
166 |
+
width={width ?? "600"}
|
167 |
+
height="1"
|
168 |
+
viewBox={`0 0 ${width ?? "600"} 1`}
|
169 |
+
fill="none"
|
170 |
+
xmlns="http://www.w3.org/2000/svg"
|
171 |
+
className={cn("absolute inset-x-0 w-full", className)}
|
172 |
+
{...svgProps}
|
173 |
+
>
|
174 |
+
<motion.path
|
175 |
+
d={`M0 0.5H${width ?? "600"}`}
|
176 |
+
stroke={`url(#svgGradient-${id})`}
|
177 |
+
/>
|
178 |
+
|
179 |
+
<defs>
|
180 |
+
<motion.linearGradient
|
181 |
+
id={`svgGradient-${id}`}
|
182 |
+
key={String(hovered)}
|
183 |
+
gradientUnits="userSpaceOnUse"
|
184 |
+
initial={{
|
185 |
+
x1: "0%",
|
186 |
+
x2: hovered ? "-10%" : "-5%",
|
187 |
+
y1: 0,
|
188 |
+
y2: 0,
|
189 |
+
}}
|
190 |
+
animate={{
|
191 |
+
x1: "110%",
|
192 |
+
x2: hovered ? "100%" : "105%",
|
193 |
+
y1: 0,
|
194 |
+
y2: 0,
|
195 |
+
}}
|
196 |
+
transition={{
|
197 |
+
duration: hovered ? 0.5 : duration ?? 2,
|
198 |
+
ease: "linear",
|
199 |
+
repeat: Infinity,
|
200 |
+
delay: hovered ? Math.random() * (1 - 0.2) + 0.2 : 0,
|
201 |
+
repeatDelay: hovered ? Math.random() * (2 - 1) + 1 : delay ?? 1,
|
202 |
+
}}
|
203 |
+
>
|
204 |
+
<stop stopColor="#2EB9DF" stopOpacity="0" />
|
205 |
+
<stop stopColor="#3b82f6" />
|
206 |
+
<stop offset="1" stopColor="#3b82f6" stopOpacity="0" />
|
207 |
+
</motion.linearGradient>
|
208 |
+
</defs>
|
209 |
+
</motion.svg>
|
210 |
+
);
|
211 |
+
};
|
212 |
+
|
213 |
+
export const CircleIcon = ({
|
214 |
+
className,
|
215 |
+
delay,
|
216 |
+
}: {
|
217 |
+
className?: string;
|
218 |
+
delay?: number;
|
219 |
+
}) => {
|
220 |
+
return (
|
221 |
+
<div
|
222 |
+
className={cn(
|
223 |
+
`pointer-events-none animate-pulse group-hover/cover:hidden group-hover/cover:opacity-100 group h-2 w-2 rounded-full bg-neutral-600 dark:bg-white opacity-20 group-hover/cover:bg-white`,
|
224 |
+
className
|
225 |
+
)}
|
226 |
+
></div>
|
227 |
+
);
|
228 |
+
};
|
src/components/ui/shooting-stars.tsx
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import { cn } from "@/lib/utils";
|
3 |
+
import React, { useEffect, useState, useRef } from "react";
|
4 |
+
|
5 |
+
interface ShootingStar {
|
6 |
+
id: number;
|
7 |
+
x: number;
|
8 |
+
y: number;
|
9 |
+
angle: number;
|
10 |
+
scale: number;
|
11 |
+
speed: number;
|
12 |
+
distance: number;
|
13 |
+
}
|
14 |
+
|
15 |
+
interface ShootingStarsProps {
|
16 |
+
minSpeed?: number;
|
17 |
+
maxSpeed?: number;
|
18 |
+
minDelay?: number;
|
19 |
+
maxDelay?: number;
|
20 |
+
starColor?: string;
|
21 |
+
trailColor?: string;
|
22 |
+
starWidth?: number;
|
23 |
+
starHeight?: number;
|
24 |
+
className?: string;
|
25 |
+
}
|
26 |
+
|
27 |
+
const getRandomStartPoint = () => {
|
28 |
+
const side = Math.floor(Math.random() * 4);
|
29 |
+
const offset = Math.random() * window.innerWidth;
|
30 |
+
|
31 |
+
switch (side) {
|
32 |
+
case 0:
|
33 |
+
return { x: offset, y: 0, angle: 45 };
|
34 |
+
case 1:
|
35 |
+
return { x: window.innerWidth, y: offset, angle: 135 };
|
36 |
+
case 2:
|
37 |
+
return { x: offset, y: window.innerHeight, angle: 225 };
|
38 |
+
case 3:
|
39 |
+
return { x: 0, y: offset, angle: 315 };
|
40 |
+
default:
|
41 |
+
return { x: 0, y: 0, angle: 45 };
|
42 |
+
}
|
43 |
+
};
|
44 |
+
export const ShootingStars: React.FC<ShootingStarsProps> = ({
|
45 |
+
minSpeed = 10,
|
46 |
+
maxSpeed = 30,
|
47 |
+
minDelay = 1200,
|
48 |
+
maxDelay = 4200,
|
49 |
+
starColor = "#9E00FF",
|
50 |
+
trailColor = "#2EB9DF",
|
51 |
+
starWidth = 10,
|
52 |
+
starHeight = 1,
|
53 |
+
className,
|
54 |
+
}) => {
|
55 |
+
const [star, setStar] = useState<ShootingStar | null>(null);
|
56 |
+
const svgRef = useRef<SVGSVGElement>(null);
|
57 |
+
|
58 |
+
useEffect(() => {
|
59 |
+
const createStar = () => {
|
60 |
+
const { x, y, angle } = getRandomStartPoint();
|
61 |
+
const newStar: ShootingStar = {
|
62 |
+
id: Date.now(),
|
63 |
+
x,
|
64 |
+
y,
|
65 |
+
angle,
|
66 |
+
scale: 1,
|
67 |
+
speed: Math.random() * (maxSpeed - minSpeed) + minSpeed,
|
68 |
+
distance: 0,
|
69 |
+
};
|
70 |
+
setStar(newStar);
|
71 |
+
|
72 |
+
const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay;
|
73 |
+
setTimeout(createStar, randomDelay);
|
74 |
+
};
|
75 |
+
|
76 |
+
createStar();
|
77 |
+
|
78 |
+
return () => {};
|
79 |
+
}, [minSpeed, maxSpeed, minDelay, maxDelay]);
|
80 |
+
|
81 |
+
useEffect(() => {
|
82 |
+
const moveStar = () => {
|
83 |
+
if (star) {
|
84 |
+
setStar((prevStar) => {
|
85 |
+
if (!prevStar) return null;
|
86 |
+
const newX =
|
87 |
+
prevStar.x +
|
88 |
+
prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180);
|
89 |
+
const newY =
|
90 |
+
prevStar.y +
|
91 |
+
prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180);
|
92 |
+
const newDistance = prevStar.distance + prevStar.speed;
|
93 |
+
const newScale = 1 + newDistance / 100;
|
94 |
+
if (
|
95 |
+
newX < -20 ||
|
96 |
+
newX > window.innerWidth + 20 ||
|
97 |
+
newY < -20 ||
|
98 |
+
newY > window.innerHeight + 20
|
99 |
+
) {
|
100 |
+
return null;
|
101 |
+
}
|
102 |
+
return {
|
103 |
+
...prevStar,
|
104 |
+
x: newX,
|
105 |
+
y: newY,
|
106 |
+
distance: newDistance,
|
107 |
+
scale: newScale,
|
108 |
+
};
|
109 |
+
});
|
110 |
+
}
|
111 |
+
};
|
112 |
+
|
113 |
+
const animationFrame = requestAnimationFrame(moveStar);
|
114 |
+
return () => cancelAnimationFrame(animationFrame);
|
115 |
+
}, [star]);
|
116 |
+
|
117 |
+
return (
|
118 |
+
<svg
|
119 |
+
ref={svgRef}
|
120 |
+
className={cn("w-full h-full absolute inset-0", className)}
|
121 |
+
>
|
122 |
+
{star && (
|
123 |
+
<rect
|
124 |
+
key={star.id}
|
125 |
+
x={star.x}
|
126 |
+
y={star.y}
|
127 |
+
width={starWidth * star.scale}
|
128 |
+
height={starHeight}
|
129 |
+
fill="url(#gradient)"
|
130 |
+
transform={`rotate(${star.angle}, ${
|
131 |
+
star.x + (starWidth * star.scale) / 2
|
132 |
+
}, ${star.y + starHeight / 2})`}
|
133 |
+
/>
|
134 |
+
)}
|
135 |
+
<defs>
|
136 |
+
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
137 |
+
<stop offset="0%" style={{ stopColor: trailColor, stopOpacity: 0 }} />
|
138 |
+
<stop
|
139 |
+
offset="100%"
|
140 |
+
style={{ stopColor: starColor, stopOpacity: 1 }}
|
141 |
+
/>
|
142 |
+
</linearGradient>
|
143 |
+
</defs>
|
144 |
+
</svg>
|
145 |
+
);
|
146 |
+
};
|
src/components/ui/stars-background.tsx
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import { cn } from "@/lib/utils";
|
3 |
+
import React, {
|
4 |
+
useState,
|
5 |
+
useEffect,
|
6 |
+
useRef,
|
7 |
+
RefObject,
|
8 |
+
useCallback,
|
9 |
+
} from "react";
|
10 |
+
|
11 |
+
interface StarProps {
|
12 |
+
x: number;
|
13 |
+
y: number;
|
14 |
+
radius: number;
|
15 |
+
opacity: number;
|
16 |
+
twinkleSpeed: number | null;
|
17 |
+
}
|
18 |
+
|
19 |
+
interface StarBackgroundProps {
|
20 |
+
starDensity?: number;
|
21 |
+
allStarsTwinkle?: boolean;
|
22 |
+
twinkleProbability?: number;
|
23 |
+
minTwinkleSpeed?: number;
|
24 |
+
maxTwinkleSpeed?: number;
|
25 |
+
className?: string;
|
26 |
+
}
|
27 |
+
|
28 |
+
export const StarsBackground: React.FC<StarBackgroundProps> = ({
|
29 |
+
starDensity = 0.00015,
|
30 |
+
allStarsTwinkle = true,
|
31 |
+
twinkleProbability = 0.7,
|
32 |
+
minTwinkleSpeed = 0.5,
|
33 |
+
maxTwinkleSpeed = 1,
|
34 |
+
className,
|
35 |
+
}) => {
|
36 |
+
const [stars, setStars] = useState<StarProps[]>([]);
|
37 |
+
const canvasRef: RefObject<HTMLCanvasElement> =
|
38 |
+
useRef<HTMLCanvasElement>(null);
|
39 |
+
|
40 |
+
const generateStars = useCallback(
|
41 |
+
(width: number, height: number): StarProps[] => {
|
42 |
+
const area = width * height;
|
43 |
+
const numStars = Math.floor(area * starDensity);
|
44 |
+
return Array.from({ length: numStars }, () => {
|
45 |
+
const shouldTwinkle =
|
46 |
+
allStarsTwinkle || Math.random() < twinkleProbability;
|
47 |
+
return {
|
48 |
+
x: Math.random() * width,
|
49 |
+
y: Math.random() * height,
|
50 |
+
radius: Math.random() * 0.05 + 0.5,
|
51 |
+
opacity: Math.random() * 0.5 + 0.5,
|
52 |
+
twinkleSpeed: shouldTwinkle
|
53 |
+
? minTwinkleSpeed +
|
54 |
+
Math.random() * (maxTwinkleSpeed - minTwinkleSpeed)
|
55 |
+
: null,
|
56 |
+
};
|
57 |
+
});
|
58 |
+
},
|
59 |
+
[
|
60 |
+
starDensity,
|
61 |
+
allStarsTwinkle,
|
62 |
+
twinkleProbability,
|
63 |
+
minTwinkleSpeed,
|
64 |
+
maxTwinkleSpeed,
|
65 |
+
]
|
66 |
+
);
|
67 |
+
|
68 |
+
useEffect(() => {
|
69 |
+
const updateStars = () => {
|
70 |
+
if (canvasRef.current) {
|
71 |
+
const canvas = canvasRef.current;
|
72 |
+
const ctx = canvas.getContext("2d");
|
73 |
+
if (!ctx) return;
|
74 |
+
|
75 |
+
const { width, height } = canvas.getBoundingClientRect();
|
76 |
+
canvas.width = width;
|
77 |
+
canvas.height = height;
|
78 |
+
setStars(generateStars(width, height));
|
79 |
+
}
|
80 |
+
};
|
81 |
+
|
82 |
+
updateStars();
|
83 |
+
|
84 |
+
const resizeObserver = new ResizeObserver(updateStars);
|
85 |
+
if (canvasRef.current) {
|
86 |
+
resizeObserver.observe(canvasRef.current);
|
87 |
+
}
|
88 |
+
|
89 |
+
return () => {
|
90 |
+
if (canvasRef.current) {
|
91 |
+
resizeObserver.unobserve(canvasRef.current);
|
92 |
+
}
|
93 |
+
};
|
94 |
+
}, [
|
95 |
+
starDensity,
|
96 |
+
allStarsTwinkle,
|
97 |
+
twinkleProbability,
|
98 |
+
minTwinkleSpeed,
|
99 |
+
maxTwinkleSpeed,
|
100 |
+
generateStars,
|
101 |
+
]);
|
102 |
+
|
103 |
+
useEffect(() => {
|
104 |
+
const canvas = canvasRef.current;
|
105 |
+
if (!canvas) return;
|
106 |
+
|
107 |
+
const ctx = canvas.getContext("2d");
|
108 |
+
if (!ctx) return;
|
109 |
+
|
110 |
+
let animationFrameId: number;
|
111 |
+
|
112 |
+
const render = () => {
|
113 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
114 |
+
stars.forEach((star) => {
|
115 |
+
ctx.beginPath();
|
116 |
+
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
|
117 |
+
ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
|
118 |
+
ctx.fill();
|
119 |
+
|
120 |
+
if (star.twinkleSpeed !== null) {
|
121 |
+
star.opacity =
|
122 |
+
0.5 +
|
123 |
+
Math.abs(Math.sin((Date.now() * 0.001) / star.twinkleSpeed) * 0.5);
|
124 |
+
}
|
125 |
+
});
|
126 |
+
|
127 |
+
animationFrameId = requestAnimationFrame(render);
|
128 |
+
};
|
129 |
+
|
130 |
+
render();
|
131 |
+
|
132 |
+
return () => {
|
133 |
+
cancelAnimationFrame(animationFrameId);
|
134 |
+
};
|
135 |
+
}, [stars]);
|
136 |
+
|
137 |
+
return (
|
138 |
+
<canvas
|
139 |
+
ref={canvasRef}
|
140 |
+
className={cn("h-full w-full absolute inset-0", className)}
|
141 |
+
/>
|
142 |
+
);
|
143 |
+
};
|
src/components/welcome-section.tsx
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { useTheme } from "next-themes";
|
4 |
+
import { ShootingStars } from "./ui/shooting-stars";
|
5 |
+
import { SparklesCore } from "./ui/sparkles";
|
6 |
+
import { StarsBackground } from "./ui/stars-background";
|
7 |
+
|
8 |
+
const WelcomeSection = () => {
|
9 |
+
const { theme } = useTheme();
|
10 |
+
return (
|
11 |
+
<section className="h-screen w-full bg-background dark:bg-black flex flex-col items-center justify-center overflow-hidden rounded-md">
|
12 |
+
<h1
|
13 |
+
className="md:text-7xl text-3xl lg:text-9xl font-bold text-center text-white relative z-20
|
14 |
+
bg-clip-text text-transparent bg-gradient-to-b from-neutral-900 to-neutral-700 dark:from-neutral-600 dark:to-white"
|
15 |
+
>
|
16 |
+
ATOM
|
17 |
+
</h1>
|
18 |
+
<div className="w-[40rem] h-40 relative">
|
19 |
+
{/* Gradients */}
|
20 |
+
<div className="absolute inset-x-20 top-0 bg-gradient-to-r from-transparent via-indigo-500 to-transparent h-[2px] w-3/4 blur-sm" />
|
21 |
+
<div className="absolute inset-x-20 top-0 bg-gradient-to-r from-transparent via-indigo-500 to-transparent h-px w-3/4" />
|
22 |
+
<div className="absolute inset-x-60 top-0 bg-gradient-to-r from-transparent via-sky-500 to-transparent h-[5px] w-1/4 blur-sm" />
|
23 |
+
<div className="absolute inset-x-60 top-0 bg-gradient-to-r from-transparent via-sky-500 to-transparent h-px w-1/4" />
|
24 |
+
|
25 |
+
{/* Core component */}
|
26 |
+
<SparklesCore
|
27 |
+
background="transparent"
|
28 |
+
minSize={0.4}
|
29 |
+
maxSize={1}
|
30 |
+
particleDensity={1200}
|
31 |
+
className="w-full h-full"
|
32 |
+
particleColor="#FFFFFF"
|
33 |
+
/>
|
34 |
+
|
35 |
+
{theme == "dark" &&
|
36 |
+
<h3 className="mt-[-9rem] pb-1 md:text-xl text-sm lg:text-3xl bg-clip-text text-transparent bg-gradient-to-b from-gray-800 to-gray-500 dark:from-neutral-200 dark:to-neutral-600 text-center font-sans font-bold relative z-20">
|
37 |
+
Your one-stop shop
|
38 |
+
<br />
|
39 |
+
for gravity-free design...
|
40 |
+
</h3>
|
41 |
+
}
|
42 |
+
|
43 |
+
{theme === "dark" && (
|
44 |
+
/* Radial Gradient to prevent sharp edges */
|
45 |
+
<div className="absolute inset-0 w-full h-full bg-black [mask-image:radial-gradient(350px_200px_at_top,transparent_20%,white)]"></div>
|
46 |
+
)}
|
47 |
+
</div>
|
48 |
+
<ShootingStars minSpeed={3} maxSpeed={12} />
|
49 |
+
<StarsBackground starDensity={0.0005} />
|
50 |
+
</section>
|
51 |
+
);
|
52 |
+
};
|
53 |
+
|
54 |
+
export default WelcomeSection;
|