mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-22 22:02:00 +00:00
feat(home-page): redesign home page with interactive components
Added new interactive components like animated grids, pulsating buttons, and ripple effects to enhance the user experience. Updated the layout to include sections for features, architecture, and file sharing. Improved the overall design with modern animations and typography.
This commit is contained in:
21
apps/docs/components.json
Normal file
21
apps/docs/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 🔌 API Endpoints
|
||||
tag: v2.0.0-beta
|
||||
---
|
||||
|
||||
## 📚 Accessing the API Documentation
|
||||
|
@@ -9,13 +9,17 @@
|
||||
"postinstall": "fumadocs-mdx"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"fumadocs-core": "15.2.7",
|
||||
"fumadocs-mdx": "11.6.0",
|
||||
"fumadocs-ui": "15.2.7",
|
||||
"lucide-react": "^0.488.0",
|
||||
"motion": "^12.9.1",
|
||||
"next": "15.3.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.3",
|
||||
@@ -27,6 +31,7 @@
|
||||
"eslint-config-next": "15.3.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tw-animate-css": "^1.2.8",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
93
apps/docs/pnpm-lock.yaml
generated
93
apps/docs/pnpm-lock.yaml
generated
@@ -8,6 +8,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
fumadocs-core:
|
||||
specifier: 15.2.7
|
||||
version: 15.2.7(@types/react@19.1.2)(next@15.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -20,6 +26,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.488.0
|
||||
version: 0.488.0(react@19.1.0)
|
||||
motion:
|
||||
specifier: ^12.9.1
|
||||
version: 12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next:
|
||||
specifier: 15.3.0
|
||||
version: 15.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -29,6 +38,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
tailwind-merge:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
devDependencies:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.3
|
||||
@@ -57,6 +69,9 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.4
|
||||
tw-animate-css:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@@ -1648,6 +1663,20 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
framer-motion@12.9.1:
|
||||
resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
@@ -2295,6 +2324,26 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
motion-dom@12.9.1:
|
||||
resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==}
|
||||
|
||||
motion-utils@12.8.3:
|
||||
resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==}
|
||||
|
||||
motion@12.9.1:
|
||||
resolution: {integrity: sha512-amdtlwafU+XLPcrfSrOQ/S2sqiSw+UTywH+X/Yoqaz0qYEocqJKh8bs6M09CdRmkjZuKx2YM+BHodXjsqoTEag==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -2796,6 +2845,9 @@ packages:
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tw-animate-css@1.2.8:
|
||||
resolution: {integrity: sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4324,8 +4376,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@8.57.1)(typescript@5.8.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@@ -4344,7 +4396,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
@@ -4355,22 +4407,22 @@ snapshots:
|
||||
tinyglobby: 0.2.12
|
||||
unrs-resolver: 1.5.0
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@8.57.1)(typescript@5.8.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1):
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@@ -4381,7 +4433,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -4614,6 +4666,15 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
framer-motion@12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
motion-dom: 12.9.1
|
||||
motion-utils: 12.8.3
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fumadocs-core@15.2.7(@types/react@19.1.2)(next@15.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
@@ -5595,6 +5656,20 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
motion-dom@12.9.1:
|
||||
dependencies:
|
||||
motion-utils: 12.8.3
|
||||
|
||||
motion-utils@12.8.3: {}
|
||||
|
||||
motion@12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
framer-motion: 12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
@@ -6223,6 +6298,8 @@ snapshots:
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.2.8: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
import type { ReactNode } from "react";
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||
import { baseOptions } from "@/app/layout.config";
|
||||
import { Particles } from "@/components/magicui/particles";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
|
||||
return (
|
||||
<HomeLayout {...baseOptions}>
|
||||
<Particles className="absolute w-full" />
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
|
@@ -1,19 +1,365 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
type LucideIcon,
|
||||
MousePointer,
|
||||
UploadIcon,
|
||||
Share2Icon,
|
||||
GithubIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
BatteryChargingIcon,
|
||||
KeyboardIcon,
|
||||
LayoutIcon,
|
||||
PersonStandingIcon,
|
||||
RocketIcon,
|
||||
SearchIcon,
|
||||
TimerIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { ThreeDMarquee } from "@/components/ui/3d-marquee";
|
||||
import { AnimatedGridPattern } from "@/components/magicui/animated-grid-pattern";
|
||||
import { TypingAnimation } from "@/components/magicui/typing-animation";
|
||||
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
||||
import { PulsatingButton } from "@/components/magicui/pulsating-button";
|
||||
import { RippleButton } from "@/components/magicui/ripple-button";
|
||||
import { WordRotate } from "@/components/magicui/word-rotate";
|
||||
|
||||
const images = [
|
||||
"https://assets.aceternity.com/cloudinary_bkp/3d-card.png",
|
||||
"https://assets.aceternity.com/animated-modal.png",
|
||||
"https://assets.aceternity.com/animated-testimonials.webp",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Tooltip_luwy44.png",
|
||||
"https://assets.aceternity.com/github-globe.png",
|
||||
"https://assets.aceternity.com/glare-card.png",
|
||||
"https://assets.aceternity.com/layout-grid.png",
|
||||
"https://assets.aceternity.com/flip-text.png",
|
||||
"https://assets.aceternity.com/hero-highlight.png",
|
||||
"https://assets.aceternity.com/carousel.webp",
|
||||
"https://assets.aceternity.com/placeholders-and-vanish-input.png",
|
||||
"https://assets.aceternity.com/shooting-stars-and-stars-background.png",
|
||||
"https://assets.aceternity.com/signup-form.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/stars_sxle3d.png",
|
||||
"https://assets.aceternity.com/spotlight-new.webp",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Spotlight_ar5jpr.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Parallax_Scroll_pzlatw_anfkh7.png",
|
||||
"https://assets.aceternity.com/tabs.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Tracing_Beam_npujte.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/typewriter-effect.png",
|
||||
"https://assets.aceternity.com/glowing-effect.webp",
|
||||
"https://assets.aceternity.com/hover-border-gradient.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Infinite_Moving_Cards_evhzur.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Lamp_hlq3ln.png",
|
||||
"https://assets.aceternity.com/macbook-scroll.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Meteors_fye3ys.png",
|
||||
"https://assets.aceternity.com/cloudinary_bkp/Moving_Border_yn78lv.png",
|
||||
"https://assets.aceternity.com/multi-step-loader.png",
|
||||
"https://assets.aceternity.com/vortex.png",
|
||||
"https://assets.aceternity.com/wobble-card.png",
|
||||
"https://assets.aceternity.com/world-map.webp",
|
||||
];
|
||||
|
||||
const docsLink = "/docs/2.0.0-beta";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="flex flex-1 flex-col justify-center text-center">
|
||||
<h1 className="mb-4 text-2xl font-bold">Hello World</h1>
|
||||
<p className="text-fd-muted-foreground">
|
||||
You can open{' '}
|
||||
<Link
|
||||
href="/docs"
|
||||
className="text-fd-foreground font-semibold underline"
|
||||
>
|
||||
/docs
|
||||
</Link>{' '}
|
||||
and see the documentation.
|
||||
</p>
|
||||
</main>
|
||||
<>
|
||||
<main className="relative z-[2] w-full px-4 py-6 sm:px-6 lg:px-8">
|
||||
<div className="relative mx-auto max-w-screen-xl bg-background">
|
||||
<Hero />
|
||||
<LogoShowcase />
|
||||
<Feedback />
|
||||
<Introduction />
|
||||
<Architecture />
|
||||
<FileSection />
|
||||
<Highlights />
|
||||
<End />
|
||||
</div>
|
||||
</main>
|
||||
<FullWidthFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<section className="relative z-[2] flex flex-col border-x border-t px-6 pt-12 pb-10 md:px-12 md:pt-16 max-md:text-center">
|
||||
<h1 className="mb-8 text-5xl font-bold">🌴 Palmr.</h1>
|
||||
<h1 className="hidden text-4xl font-medium max-w-[600px] md:block mb-4">
|
||||
Modern & efficient file sharing
|
||||
</h1>
|
||||
<p className="mb-8 text-fd-muted-foreground md:max-w-[80%] md:text-xl">
|
||||
Palmr is a fast and secure platform for sharing files, built with
|
||||
performance and privacy in mind.
|
||||
</p>
|
||||
<div className="hidden h-[10rem] lg:flex items-center justify-center absolute right-0 top-10">
|
||||
<TextHoverEffect text="Palmr." />
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-6 max-md:mx-auto mb-4">
|
||||
<PulsatingButton>
|
||||
<Link href={docsLink}>Get Started</Link>
|
||||
</PulsatingButton>
|
||||
|
||||
<RippleButton>
|
||||
<a
|
||||
href="https://github.com/kyantech/Palmr"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<GithubIcon size={18} />
|
||||
GitHub
|
||||
</a>
|
||||
</RippleButton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoShowcase() {
|
||||
return (
|
||||
<div className="z-[2] border-x bg-background">
|
||||
<ThreeDMarquee images={images} className="rounded-none" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Feedback() {
|
||||
return (
|
||||
<section className="relative flex flex-col items-center overflow-hidden border-x border-t px-6 py-8 md:py-16">
|
||||
<p className="text-xl font-medium flex items-center justify-center gap-2">
|
||||
A modern way to share files
|
||||
<WordRotate
|
||||
duration={4000}
|
||||
words={[
|
||||
"efficiently",
|
||||
"securely",
|
||||
"privately",
|
||||
"reliably",
|
||||
"seamlessly",
|
||||
]}
|
||||
className="min-w-[100px] inline-block"
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function Introduction() {
|
||||
return (
|
||||
<section className="grid grid-cols-1 border-t border-x md:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 border-r p-8 md:p-12">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex items-center gap-3 text-muted-foreground border border-foreground w-fit p-3 rounded-lg">
|
||||
<UploadIcon className="size-6 text-foreground" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold">Upload.</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Send your files quickly and safely.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 border-r p-8 md:p-12">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex items-center gap-3 text-muted-foreground border border-foreground w-fit p-3 rounded-lg">
|
||||
<Share2Icon className="size-6 text-foreground" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold">Share.</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground">Easily share with anyone.</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Architecture() {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 border-x border-t px-8 py-16 md:py-24 lg:flex-row md:px-12">
|
||||
<div className="flex-1 shrink-0 text-start">
|
||||
<p className="mb-4 w-fit bg-fd-primary px-2 py-1 text-md font-bold font-mono text-fd-primary-foreground">
|
||||
Carefully Built
|
||||
</p>
|
||||
<h2 className="mb-4 text-xl font-semibold sm:text-2xl">
|
||||
A complete solution for file sharing.
|
||||
</h2>
|
||||
<p className="mb-6 text-fd-muted-foreground">
|
||||
From the upload to the link generation, everything is designed to be
|
||||
fast, reliable, and privacy-friendly.
|
||||
<br />
|
||||
<br />
|
||||
Every feature was crafted to deliver the best possible experience.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FileSection() {
|
||||
return (
|
||||
<section
|
||||
className="relative overflow-hidden border-x border-t px-8 py-16 sm:py-24"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at center, var(--color-fd-secondary), var(--color-fd-background) 40%)",
|
||||
}}
|
||||
>
|
||||
<h2 className="text-center text-2xl font-semibold sm:text-3xl">
|
||||
File Sharing
|
||||
<TypingAnimation className="text-center text-2xl font-semibold sm:text-3xl">
|
||||
Free & Open Source
|
||||
</TypingAnimation>
|
||||
</h2>
|
||||
<AnimatedGridPattern className="opacity-5" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Highlights() {
|
||||
const features = [
|
||||
{
|
||||
icon: TimerIcon,
|
||||
title: "Fast & Efficient",
|
||||
text: "Optimized upload and download speeds.",
|
||||
},
|
||||
{
|
||||
icon: LayoutIcon,
|
||||
title: "Intuitive UI",
|
||||
text: "Clean, modern, and easy to use.",
|
||||
},
|
||||
{
|
||||
icon: RocketIcon,
|
||||
title: "Modern Stack",
|
||||
text: "Powered by Next.js, Fastify, MinIO, Postgres and the latest tech.",
|
||||
},
|
||||
{
|
||||
icon: SearchIcon,
|
||||
title: "Smart Search",
|
||||
text: "Find shared files quickly.",
|
||||
},
|
||||
{
|
||||
icon: KeyboardIcon,
|
||||
title: "Open API",
|
||||
text: "REST API endpoinds available for any integrations.",
|
||||
},
|
||||
{
|
||||
icon: PersonStandingIcon,
|
||||
title: "Customizable",
|
||||
text: "Full control over all the system and configurations.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-1 border-r md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="col-span-full flex items-start justify-center border-l border-t p-8 pb-2 text-center">
|
||||
<h2 className="bg-fd-primary px-1 text-2xl font-semibold text-fd-primary-foreground">
|
||||
Highlights
|
||||
</h2>
|
||||
<MousePointer className="-ml-1 mt-8" />
|
||||
</div>
|
||||
{features.map(({ icon, title, text }, i) => (
|
||||
<Highlight key={i} icon={icon} heading={title}>
|
||||
{text}
|
||||
</Highlight>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Highlight({
|
||||
icon: Icon,
|
||||
heading,
|
||||
children,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
heading: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="border-l border-t px-6 py-12">
|
||||
<div className="mb-4 flex items-center gap-2 text-fd-muted-foreground">
|
||||
<Icon className="size-6" />
|
||||
<h2 className="text-sm font-medium">{heading}</h2>
|
||||
</div>
|
||||
<span className="font-medium">{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function End() {
|
||||
return (
|
||||
<section className="flex w-full flex-1">
|
||||
<div className="w-full flex flex-col gap-8 overflow-hidden border px-8 py-14">
|
||||
<h2 className="text-3xl font-extrabold font-mono uppercase ">
|
||||
Start Using Now. 🌴
|
||||
</h2>
|
||||
<ul className="mt-2 flex flex-col gap-6">
|
||||
<ListItem icon={TimerIcon} title="Fast Setup">
|
||||
Get up and running in minutes.
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon={BatteryChargingIcon}
|
||||
title="All under your own control"
|
||||
>
|
||||
Take full control of your file sharing infrastructure.
|
||||
</ListItem>
|
||||
</ul>
|
||||
<div className="flex flex-wrap gap-2 border-t pt-14 pb-0 justify-end">
|
||||
<RippleButton>
|
||||
<Link href={docsLink}>Documentation</Link>
|
||||
</RippleButton>
|
||||
<RippleButton>
|
||||
<a
|
||||
href="https://github.com/kyantech/Palmr"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<GithubIcon size={18} />
|
||||
GitHub
|
||||
</a>
|
||||
</RippleButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ListItem({
|
||||
icon: Icon,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<li>
|
||||
<span className="flex items-center gap-3 font-medium">
|
||||
<Icon className="size-8" />
|
||||
{title}
|
||||
</span>
|
||||
<span className="mt-2 block text-sm text-fd-muted-foreground">
|
||||
{children}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function FullWidthFooter() {
|
||||
return (
|
||||
<footer className="w-full flex items-center justify-center p-6 border-t font-light container max-w-7xl">
|
||||
<div className="flex items-center gap-1 text-sm max-w-7xl">
|
||||
<span>Powered by</span>
|
||||
<Link
|
||||
href="http://kyantech.com.br"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="flex items-center hover:text-green-700 text-green-500 transition-colors font-light"
|
||||
>
|
||||
Kyantech Solutions ©
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
@@ -1,4 +1,16 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createFromSource(source);
|
||||
|
||||
export const { GET } = createFromSource(source, (page) => {
|
||||
// Log the page URL for debugging
|
||||
console.log('Page URL:', page.url);
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
url: page.url,
|
||||
id: page.url,
|
||||
structuredData: page.data.structuredData,
|
||||
tag: page.url.startsWith('/docs/2.0.0-beta') ? 'v2.0.0-beta' : 'v1.1.7-beta'
|
||||
};
|
||||
});
|
||||
|
@@ -1,6 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/neutral.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
/* @import 'fumadocs-ui/css/black.css'; */
|
||||
|
||||
@source '../../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
@@ -18,3 +21,176 @@ h4 {
|
||||
.prose h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-ripple: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite;
|
||||
@keyframes ripple {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
--animate-pulse: pulse var(--duration) ease-out infinite
|
||||
;
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
boxShadow: 0 0 0 0 var(--pulse-color);
|
||||
}
|
||||
50% {
|
||||
boxShadow: 0 0 0 8px var(--pulse-color);
|
||||
}
|
||||
}
|
||||
--animate-rippling: rippling var(--duration) ease-out;
|
||||
@keyframes rippling {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--animate-pulse: pulse var(--duration) ease-out infinite;
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 var(--pulse-color);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px var(--pulse-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@theme inline {
|
||||
--animate-rippling: rippling var(--duration) ease-out;
|
||||
|
||||
@keyframes rippling {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,20 +1,45 @@
|
||||
import { Banner } from 'fumadocs-ui/components/banner';
|
||||
import './global.css';
|
||||
import { RootProvider } from 'fumadocs-ui/provider';
|
||||
import { Inter } from 'next/font/google';
|
||||
import type { ReactNode } from 'react';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { Banner } from "fumadocs-ui/components/banner";
|
||||
import "./global.css";
|
||||
import { RootProvider } from "fumadocs-ui/provider";
|
||||
import { Inter } from "next/font/google";
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "fumadocs-core/link";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={inter.className} suppressHydrationWarning>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<Banner variant="rainbow" id='banner-v-2' changeLayout={false}><Link href='/docs/2.0.0-beta' >Palmr. v2.0.0-beta has released!</Link></Banner>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
<Banner variant="rainbow" id="banner-v-2" changeLayout={false}>
|
||||
<Link href="/docs/2.0.0-beta">Palmr. v2.0.0-beta has released!</Link>
|
||||
</Banner>
|
||||
<RootProvider
|
||||
search={{
|
||||
options: {
|
||||
defaultTag: "2.0.0-beta",
|
||||
tags: [
|
||||
{
|
||||
name: "v1.1.7 Beta",
|
||||
value: "1.1.7-beta",
|
||||
},
|
||||
{
|
||||
name: "v2.0.0 Beta ✨",
|
||||
value: "2.0.0-beta",
|
||||
props: {
|
||||
style: {
|
||||
border: "1px solid rgba(0,165,80,0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
154
apps/docs/src/components/magicui/animated-grid-pattern.tsx
Normal file
154
apps/docs/src/components/magicui/animated-grid-pattern.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface AnimatedGridPatternProps
|
||||
extends ComponentPropsWithoutRef<"svg"> {
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
strokeDasharray?: any;
|
||||
numSquares?: number;
|
||||
maxOpacity?: number;
|
||||
duration?: number;
|
||||
repeatDelay?: number;
|
||||
}
|
||||
|
||||
export function AnimatedGridPattern({
|
||||
width = 40,
|
||||
height = 40,
|
||||
x = -1,
|
||||
y = -1,
|
||||
strokeDasharray = 0,
|
||||
numSquares = 50,
|
||||
className,
|
||||
maxOpacity = 0.5,
|
||||
duration = 4,
|
||||
repeatDelay = 0.5,
|
||||
...props
|
||||
}: AnimatedGridPatternProps) {
|
||||
const id = useId();
|
||||
const containerRef = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [squares, setSquares] = useState(() => generateSquares(numSquares));
|
||||
|
||||
function getPos() {
|
||||
return [
|
||||
Math.floor((Math.random() * dimensions.width) / width),
|
||||
Math.floor((Math.random() * dimensions.height) / height),
|
||||
];
|
||||
}
|
||||
|
||||
// Adjust the generateSquares function to return objects with an id, x, and y
|
||||
function generateSquares(count: number) {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
pos: getPos(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Function to update a single square's position
|
||||
const updateSquarePosition = (id: number) => {
|
||||
setSquares((currentSquares) =>
|
||||
currentSquares.map((sq) =>
|
||||
sq.id === id
|
||||
? {
|
||||
...sq,
|
||||
pos: getPos(),
|
||||
}
|
||||
: sq,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// Update squares to animate in
|
||||
useEffect(() => {
|
||||
if (dimensions.width && dimensions.height) {
|
||||
setSquares(generateSquares(numSquares));
|
||||
}
|
||||
}, [dimensions, numSquares]);
|
||||
|
||||
// Resize observer to update container dimensions
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
setDimensions({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
resizeObserver.unobserve(containerRef.current);
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={containerRef}
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={id}
|
||||
width={width}
|
||||
height={height}
|
||||
patternUnits="userSpaceOnUse"
|
||||
x={x}
|
||||
y={y}
|
||||
>
|
||||
<path
|
||||
d={`M.5 ${height}V.5H${width}`}
|
||||
fill="none"
|
||||
strokeDasharray={strokeDasharray}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#${id})`} />
|
||||
<svg x={x} y={y} className="overflow-visible">
|
||||
{squares.map(({ pos: [x, y], id }, index) => (
|
||||
<motion.rect
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: maxOpacity }}
|
||||
transition={{
|
||||
duration,
|
||||
repeat: 1,
|
||||
delay: index * 0.1,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
onAnimationComplete={() => updateSquarePosition(id)}
|
||||
key={`${x}-${y}-${index}`}
|
||||
width={width - 1}
|
||||
height={height - 1}
|
||||
x={x * width + 1}
|
||||
y={y * height + 1}
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</svg>
|
||||
);
|
||||
}
|
313
apps/docs/src/components/magicui/particles.tsx
Normal file
313
apps/docs/src/components/magicui/particles.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
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 extends ComponentPropsWithoutRef<"div"> {
|
||||
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];
|
||||
}
|
||||
|
||||
type Circle = {
|
||||
x: number;
|
||||
y: number;
|
||||
translateX: number;
|
||||
translateY: number;
|
||||
size: number;
|
||||
alpha: number;
|
||||
targetAlpha: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
magnetism: number;
|
||||
};
|
||||
|
||||
export const Particles: React.FC<ParticlesProps> = ({
|
||||
className = "",
|
||||
quantity = 100,
|
||||
staticity = 50,
|
||||
ease = 50,
|
||||
size = 0.4,
|
||||
refresh = false,
|
||||
color = "#ffffff",
|
||||
vx = 0,
|
||||
vy = 0,
|
||||
...props
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||
const context = useRef<CanvasRenderingContext2D | null>(null);
|
||||
const circles = useRef<Circle[]>([]);
|
||||
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;
|
||||
const rafID = useRef<number | null>(null);
|
||||
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
context.current = canvasRef.current.getContext("2d");
|
||||
}
|
||||
initCanvas();
|
||||
animate();
|
||||
|
||||
const handleResize = () => {
|
||||
if (resizeTimeout.current) {
|
||||
clearTimeout(resizeTimeout.current);
|
||||
}
|
||||
resizeTimeout.current = setTimeout(() => {
|
||||
initCanvas();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
if (rafID.current != null) {
|
||||
window.cancelAnimationFrame(rafID.current);
|
||||
}
|
||||
if (resizeTimeout.current) {
|
||||
clearTimeout(resizeTimeout.current);
|
||||
}
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
||||
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);
|
||||
|
||||
// Clear existing particles and create new ones with exact quantity
|
||||
circles.current = [];
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const circle = circleParams();
|
||||
drawCircle(circle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
rafID.current = window.requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("pointer-events-none", className)}
|
||||
ref={canvasContainerRef}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<canvas ref={canvasRef} className="size-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
46
apps/docs/src/components/magicui/pulsating-button.tsx
Normal file
46
apps/docs/src/components/magicui/pulsating-button.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PulsatingButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
pulseColor?: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
export const PulsatingButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
PulsatingButtonProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
pulseColor = "#808080",
|
||||
duration = "1.5s",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-center justify-center rounded-lg bg-primary px-4 py-2 text-center text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--pulse-color": pulseColor,
|
||||
"--duration": duration,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative z-10">{children}</div>
|
||||
<div className="absolute left-1/2 top-1/2 size-full -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-lg bg-inherit" />
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
PulsatingButton.displayName = "PulsatingButton";
|
91
apps/docs/src/components/magicui/ripple-button.tsx
Normal file
91
apps/docs/src/components/magicui/ripple-button.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { MouseEvent, useEffect, useState } from "react";
|
||||
|
||||
interface RippleButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
rippleColor?: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
export const RippleButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
RippleButtonProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
rippleColor = "#ffffff",
|
||||
duration = "600ms",
|
||||
onClick,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [buttonRipples, setButtonRipples] = useState<
|
||||
Array<{ x: number; y: number; size: number; key: number }>
|
||||
>([]);
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
createRipple(event);
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
const createRipple = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
const button = event.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
const size = Math.max(rect.width, rect.height);
|
||||
const x = event.clientX - rect.left - size / 2;
|
||||
const y = event.clientY - rect.top - size / 2;
|
||||
|
||||
const newRipple = { x, y, size, key: Date.now() };
|
||||
setButtonRipples((prevRipples) => [...prevRipples, newRipple]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonRipples.length > 0) {
|
||||
const lastRipple = buttonRipples[buttonRipples.length - 1];
|
||||
const timeout = setTimeout(() => {
|
||||
setButtonRipples((prevRipples) =>
|
||||
prevRipples.filter((ripple) => ripple.key !== lastRipple.key),
|
||||
);
|
||||
}, parseInt(duration));
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [buttonRipples, duration]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"relative flex cursor-pointer items-center justify-center overflow-hidden rounded-lg border-2 bg-background px-4 py-2 text-center text-primary",
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative z-10">{children}</div>
|
||||
<span className="pointer-events-none absolute inset-0">
|
||||
{buttonRipples.map((ripple) => (
|
||||
<span
|
||||
className="absolute animate-rippling rounded-full bg-background opacity-30"
|
||||
key={ripple.key}
|
||||
style={{
|
||||
width: `${ripple.size}px`,
|
||||
height: `${ripple.size}px`,
|
||||
top: `${ripple.y}px`,
|
||||
left: `${ripple.x}px`,
|
||||
backgroundColor: rippleColor,
|
||||
transform: `scale(0)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
RippleButton.displayName = "RippleButton";
|
90
apps/docs/src/components/magicui/typing-animation.tsx
Normal file
90
apps/docs/src/components/magicui/typing-animation.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, MotionProps } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface TypingAnimationProps extends MotionProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
as?: React.ElementType;
|
||||
startOnView?: boolean;
|
||||
}
|
||||
|
||||
export function TypingAnimation({
|
||||
children,
|
||||
className,
|
||||
duration = 100,
|
||||
delay = 0,
|
||||
as: Component = "div",
|
||||
startOnView = false,
|
||||
...props
|
||||
}: TypingAnimationProps) {
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
});
|
||||
|
||||
const [displayedText, setDisplayedText] = useState<string>("");
|
||||
const [started, setStarted] = useState(false);
|
||||
const elementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!startOnView) {
|
||||
const startTimeout = setTimeout(() => {
|
||||
setStarted(true);
|
||||
}, delay);
|
||||
return () => clearTimeout(startTimeout);
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
setStarted(true);
|
||||
}, delay);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
if (elementRef.current) {
|
||||
observer.observe(elementRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [delay, startOnView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!started) return;
|
||||
|
||||
let i = 0;
|
||||
const typingEffect = setInterval(() => {
|
||||
if (i < children.length) {
|
||||
setDisplayedText(children.substring(0, i + 1));
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(typingEffect);
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return () => {
|
||||
clearInterval(typingEffect);
|
||||
};
|
||||
}, [children, duration, started]);
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn(
|
||||
"text-4xl font-bold leading-[5rem] tracking-[-0.02em]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{displayedText}
|
||||
</MotionComponent>
|
||||
);
|
||||
}
|
50
apps/docs/src/components/magicui/word-rotate.tsx
Normal file
50
apps/docs/src/components/magicui/word-rotate.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, MotionProps } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface WordRotateProps {
|
||||
words: string[];
|
||||
duration?: number;
|
||||
motionProps?: MotionProps;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WordRotate({
|
||||
words,
|
||||
duration = 2500,
|
||||
motionProps = {
|
||||
initial: { opacity: 0, y: -50 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: 50 },
|
||||
transition: { duration: 0.25, ease: "easeOut" },
|
||||
},
|
||||
className,
|
||||
}: WordRotateProps) {
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setIndex((prevIndex) => (prevIndex + 1) % words.length);
|
||||
}, duration);
|
||||
|
||||
// Clean up interval on unmount
|
||||
return () => clearInterval(interval);
|
||||
}, [words, duration]);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden py-2">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.h1
|
||||
key={words[index]}
|
||||
className={cn(className)}
|
||||
{...motionProps}
|
||||
>
|
||||
{words[index]}
|
||||
</motion.h1>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
142
apps/docs/src/components/ui/3d-marquee.tsx
Normal file
142
apps/docs/src/components/ui/3d-marquee.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
export const ThreeDMarquee = ({
|
||||
images,
|
||||
className,
|
||||
}: {
|
||||
images: string[];
|
||||
className?: string;
|
||||
}) => {
|
||||
// Split the images array into 4 equal parts
|
||||
const chunkSize = Math.ceil(images.length / 4);
|
||||
const chunks = Array.from({ length: 4 }, (_, colIndex) => {
|
||||
const start = colIndex * chunkSize;
|
||||
return images.slice(start, start + chunkSize);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto block h-[600px] overflow-hidden rounded-2xl max-sm:h-100",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<div className="size-[1720px] shrink-0 scale-50 sm:scale-75 lg:scale-100">
|
||||
<div
|
||||
style={{
|
||||
transform: "rotateX(55deg) rotateY(0deg) rotateZ(-45deg)",
|
||||
}}
|
||||
className="relative top-96 right-[50%] grid size-full origin-top-left grid-cols-4 gap-8 transform-3d"
|
||||
>
|
||||
{chunks.map((subarray, colIndex) => (
|
||||
<motion.div
|
||||
animate={{ y: colIndex % 2 === 0 ? 100 : -100 }}
|
||||
transition={{
|
||||
duration: colIndex % 2 === 0 ? 10 : 15,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
key={colIndex + "marquee"}
|
||||
className="flex flex-col items-start gap-8"
|
||||
>
|
||||
<GridLineVertical className="-left-4" offset="80px" />
|
||||
{subarray.map((image, imageIndex) => (
|
||||
<div className="relative" key={imageIndex + image}>
|
||||
<GridLineHorizontal className="-top-4" offset="20px" />
|
||||
<motion.img
|
||||
whileHover={{
|
||||
y: -10,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
key={imageIndex + image}
|
||||
src={image}
|
||||
alt={`Image ${imageIndex + 1}`}
|
||||
className="aspect-[970/700] rounded-lg object-cover ring ring-gray-950/5 hover:shadow-2xl"
|
||||
width={970}
|
||||
height={700}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineHorizontal = ({
|
||||
className,
|
||||
offset,
|
||||
}: {
|
||||
className?: string;
|
||||
offset?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "1px",
|
||||
"--width": "5px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "200px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.2)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute left-[calc(var(--offset)/2*-1)] h-[var(--height)] w-[calc(100%+var(--offset))]",
|
||||
"bg-[linear-gradient(to_right,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_left,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_right,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_right,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className,
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridLineVertical = ({
|
||||
className,
|
||||
offset,
|
||||
}: {
|
||||
className?: string;
|
||||
offset?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--background": "#ffffff",
|
||||
"--color": "rgba(0, 0, 0, 0.2)",
|
||||
"--height": "5px",
|
||||
"--width": "1px",
|
||||
"--fade-stop": "90%",
|
||||
"--offset": offset || "150px", //-100px if you want to keep the line inside
|
||||
"--color-dark": "rgba(255, 255, 255, 0.2)",
|
||||
maskComposite: "exclude",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute top-[calc(var(--offset)/2*-1)] h-[calc(100%+var(--offset))] w-[var(--width)]",
|
||||
"bg-[linear-gradient(to_bottom,var(--color),var(--color)_50%,transparent_0,transparent)]",
|
||||
"[background-size:var(--width)_var(--height)]",
|
||||
"[mask:linear-gradient(to_top,var(--background)_var(--fade-stop),transparent),_linear-gradient(to_bottom,var(--background)_var(--fade-stop),transparent),_linear-gradient(black,black)]",
|
||||
"[mask-composite:exclude]",
|
||||
"z-30",
|
||||
"dark:bg-[linear-gradient(to_bottom,var(--color-dark),var(--color-dark)_50%,transparent_0,transparent)]",
|
||||
className,
|
||||
)}
|
||||
></div>
|
||||
);
|
||||
};
|
134
apps/docs/src/components/ui/text-hover-effect.tsx
Normal file
134
apps/docs/src/components/ui/text-hover-effect.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export const TextHoverEffect = ({
|
||||
text,
|
||||
duration,
|
||||
}: {
|
||||
text: string;
|
||||
duration?: number;
|
||||
automatic?: boolean;
|
||||
}) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [maskPosition, setMaskPosition] = useState({ cx: "50%", cy: "50%" });
|
||||
|
||||
useEffect(() => {
|
||||
if (svgRef.current && cursor.x !== null && cursor.y !== null) {
|
||||
const svgRect = svgRef.current.getBoundingClientRect();
|
||||
const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
|
||||
const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
|
||||
setMaskPosition({
|
||||
cx: `${cxPercentage}%`,
|
||||
cy: `${cyPercentage}%`,
|
||||
});
|
||||
}
|
||||
}, [cursor]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 300 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}
|
||||
className="select-none"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="textGradient"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
r="25%"
|
||||
>
|
||||
{hovered && (
|
||||
<>
|
||||
<stop offset="0%" stopColor="#eab308" />
|
||||
<stop offset="25%" stopColor="#ef4444" />
|
||||
<stop offset="50%" stopColor="#3b82f6" />
|
||||
<stop offset="75%" stopColor="#06b6d4" />
|
||||
<stop offset="100%" stopColor="#8b5cf6" />
|
||||
</>
|
||||
)}
|
||||
</linearGradient>
|
||||
|
||||
<motion.radialGradient
|
||||
id="revealMask"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
r="20%"
|
||||
initial={{ cx: "50%", cy: "50%" }}
|
||||
animate={maskPosition}
|
||||
transition={{ duration: duration ?? 0, ease: "easeOut" }}
|
||||
|
||||
// example for a smoother animation below
|
||||
|
||||
// transition={{
|
||||
// type: "spring",
|
||||
// stiffness: 300,
|
||||
// damping: 50,
|
||||
// }}
|
||||
>
|
||||
<stop offset="0%" stopColor="white" />
|
||||
<stop offset="100%" stopColor="black" />
|
||||
</motion.radialGradient>
|
||||
<mask id="textMask">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="url(#revealMask)"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
strokeWidth="0.3"
|
||||
className="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
|
||||
style={{ opacity: hovered ? 0.7 : 0 }}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
<motion.text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
strokeWidth="0.3"
|
||||
className="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
|
||||
initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
|
||||
animate={{
|
||||
strokeDashoffset: 0,
|
||||
strokeDasharray: 1000,
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</motion.text>
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
stroke="url(#textGradient)"
|
||||
strokeWidth="0.3"
|
||||
mask="url(#textMask)"
|
||||
className="fill-transparent font-[helvetica] text-7xl font-bold"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
};
|
6
apps/docs/src/lib/utils.ts
Normal file
6
apps/docs/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
Reference in New Issue
Block a user