init project
This commit is contained in:
commit
13bc112ad3
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_APP_URL=https://www.acme.ai
|
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
17
components.json
Normal file
17
components.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
53
content/introducing-acme-ai.mdx
Normal file
53
content/introducing-acme-ai.mdx
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Introducing Acme.ai
|
||||
publishedAt: "2024-08-29"
|
||||
summary: Introducing Acme.ai, a cutting-edge AI solution for modern businesses.
|
||||
author: "dillionverma"
|
||||
image: "/introducing.png"
|
||||
---
|
||||
|
||||
We're excited to unveil **Acme.ai**, an innovative AI-powered platform designed to transform your business operations and skyrocket productivity. 🚀
|
||||
|
||||
## The Challenge We're Addressing
|
||||
|
||||
In today's AI-driven world, businesses face several hurdles:
|
||||
|
||||
- Overwhelming data analysis
|
||||
- Inefficient decision-making processes
|
||||
- Difficulty in predicting market trends
|
||||
|
||||
Acme.ai tackles these challenges head-on, offering a sophisticated AI solution that simplifies complex business processes.
|
||||
|
||||
## Our Mission
|
||||
|
||||
1. **Accelerate Decision-Making**: By leveraging AI to analyze vast datasets, we help you make informed decisions faster.
|
||||
2. **Enhance Forecasting**: Our advanced predictive models provide accurate insights into future trends.
|
||||
3. **Optimize Operations**: With AI-driven recommendations, streamline your business processes effortlessly.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **AI-Powered Dashboard**: Get real-time, AI-interpreted insights at a glance
|
||||
- **Predictive Analytics**: Forecast trends and make data-driven decisions
|
||||
- **Natural Language Processing**: Interact with your data using simple language queries
|
||||
- **Automated Reporting**: Generate comprehensive reports with a single click
|
||||
- **Customizable AI Models**: Tailor the AI to your specific industry needs
|
||||
|
||||
## Why Acme.ai Stands Out
|
||||
|
||||
> "Acme.ai has revolutionized our strategic planning. It's like having a crystal ball for our business!" - John Smith, CFO of FutureTech
|
||||
|
||||
Our AI solution isn't just a tool; it's your competitive edge. Here's how we compare:
|
||||
|
||||
| Feature | Acme.ai | Traditional BI Tools |
|
||||
| ------------------------ | ------- | -------------------- |
|
||||
| AI-Powered Insights | ✅ | ❌ |
|
||||
| Predictive Capabilities | ✅ | ❌ |
|
||||
| Natural Language Queries | ✅ | ❌ |
|
||||
|
||||
## Embarking on Your AI Journey
|
||||
|
||||
Getting started with Acme.ai is seamless:
|
||||
|
||||
1. Sign up for a demo
|
||||
2. Integrate your data sources
|
||||
3. Start unlocking AI-driven insights
|
8
next.config.mjs
Normal file
8
next.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [{ hostname: "localhost" }, { hostname: "randomuser.me" }],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
50
package.json
Normal file
50
package.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "saas-template-magicui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.1.7",
|
||||
"framer-motion": "^11.3.21",
|
||||
"lucide-react": "^0.417.0",
|
||||
"next": "14.2.7",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"recharts": "^2.12.7",
|
||||
"rehype-pretty-code": "^0.13.2",
|
||||
"rehype-stringify": "^10.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unified": "^11.0.5",
|
||||
"vaul": "^0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.7",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
5267
pnpm-lock.yaml
generated
Normal file
5267
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
postcss.config.mjs
Normal file
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
BIN
public/author.jpg
Normal file
BIN
public/author.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
public/dashboard.png
Normal file
BIN
public/dashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 195 KiB |
BIN
public/introducing.png
Normal file
BIN
public/introducing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
BIN
public/og.png
Normal file
BIN
public/og.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
11
src/app/(auth)/layout.tsx
Normal file
11
src/app/(auth)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
interface MarketingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: MarketingLayoutProps) {
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-center h-screen">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
76
src/app/(auth)/login/page.tsx
Normal file
76
src/app/(auth)/login/page.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<Button variant="outline" className="w-full">
|
||||
<Icons.google className="w-4 h-4 mr-2" />
|
||||
Login with Google
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Icons.github className="w-4 h-4 mr-2" />
|
||||
Login with GitHub
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link href="#" className="ml-auto inline-block text-sm underline">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/signup" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
64
src/app/(auth)/signup/page.tsx
Normal file
64
src/app/(auth)/signup/page.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Sign Up</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first-name">First name</Label>
|
||||
<Input id="first-name" placeholder="Max" required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last-name">Last name</Label>
|
||||
<Input id="last-name" placeholder="Robinson" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Sign up with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" className="underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
135
src/app/(marketing)/blog/[slug]/page.tsx
Normal file
135
src/app/(marketing)/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import Author from "@/components/blog-author";
|
||||
import CtaSection from "@/components/sections/cta";
|
||||
import { getPost } from "@/lib/blog";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}): Promise<Metadata | undefined> {
|
||||
let post = await getPost(params.slug);
|
||||
let {
|
||||
title,
|
||||
publishedAt: publishedTime,
|
||||
summary: description,
|
||||
image,
|
||||
} = post.metadata;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: "article",
|
||||
publishedTime,
|
||||
url: `${siteConfig.url}/blog/${post.slug}`,
|
||||
images: [
|
||||
{
|
||||
url: image,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Blog({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}) {
|
||||
let post = await getPost(params.slug);
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<section id="blog">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: post.metadata.title,
|
||||
datePublished: post.metadata.publishedAt,
|
||||
dateModified: post.metadata.publishedAt,
|
||||
description: post.metadata.summary,
|
||||
image: post.metadata.image
|
||||
? `${siteConfig.url}${post.metadata.image}`
|
||||
: `${siteConfig.url}/blog/${post.slug}/opengraph-image`,
|
||||
url: `${siteConfig.url}/blog/${post.slug}`,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: siteConfig.name,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-[800px] px-4 sm:px-6 lg:px-8 space-y-4 my-12">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="mb-8 w-full h-64 bg-gray-200 animate-pulse rounded-lg"></div>
|
||||
}
|
||||
>
|
||||
{post.metadata.image && (
|
||||
<div className="mb-8">
|
||||
<Image
|
||||
width={1920}
|
||||
height={1080}
|
||||
src={post.metadata.image}
|
||||
alt={post.metadata.title}
|
||||
className="w-full h-auto rounded-lg border shadow-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Suspense>
|
||||
<div className="flex flex-col">
|
||||
<h1 className="title font-medium text-3xl tracking-tighter">
|
||||
{post.metadata.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<Suspense fallback={<p className="h-5" />}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<time
|
||||
dateTime={post.metadata.publishedAt}
|
||||
className="text-sm text-gray-500"
|
||||
>
|
||||
{formatDate(post.metadata.publishedAt)}
|
||||
</time>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Author
|
||||
twitterUsername={post.metadata.author}
|
||||
name={post.metadata.author}
|
||||
image={"/author.jpg"}
|
||||
/>
|
||||
</div>
|
||||
<article
|
||||
className="prose dark:prose-invert mx-auto max-w-full"
|
||||
dangerouslySetInnerHTML={{ __html: post.source }}
|
||||
></article>
|
||||
</div>
|
||||
<CtaSection />
|
||||
</section>
|
||||
);
|
||||
}
|
16
src/app/(marketing)/blog/layout.tsx
Normal file
16
src/app/(marketing)/blog/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import Footer from "@/components/sections/footer";
|
||||
import Header from "@/components/sections/header";
|
||||
|
||||
interface MarketingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: MarketingLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
39
src/app/(marketing)/blog/page.tsx
Normal file
39
src/app/(marketing)/blog/page.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import BlogCard from "@/components/blog-card";
|
||||
import { getBlogPosts } from "@/lib/blog";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Blog",
|
||||
description: `Latest news and updates from ${siteConfig.name}.`,
|
||||
});
|
||||
|
||||
export default async function Blog() {
|
||||
const allPosts = await getBlogPosts();
|
||||
|
||||
const articles = await Promise.all(
|
||||
allPosts.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto w-full max-w-screen-xl px-2.5 lg:px-20 mt-24">
|
||||
<div className="text-center py-16">
|
||||
<h1 className="text-3xl font-bold text-foreground sm:text-4xl">
|
||||
Articles
|
||||
</h1>
|
||||
<p className="mt-4 text-xl text-muted-foreground">
|
||||
Latest news and updates from {siteConfig.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[50vh] bg-white/50 shadow-[inset_10px_-50px_94px_0_rgb(199,199,199,0.2)] backdrop-blur-lg">
|
||||
<div className="mx-auto grid w-full max-w-screen-xl grid-cols-1 gap-8 px-2.5 py-10 lg:px-20 lg:grid-cols-3">
|
||||
{articles.map((data, idx) => (
|
||||
<BlogCard key={data.slug} data={data} priority={idx <= 1} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
95
src/app/globals.css
Normal file
95
src/app/globals.css
Normal file
@ -0,0 +1,95 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 349 100% 55.5%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 349 100% 55.5%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 349 100% 55.5%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 349 100% 55.5%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 349 100% 55.5%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 349 100% 55.5%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--chart-1: 349 100% 55.5%;
|
||||
--chart-2: 0 0 90;
|
||||
--chart-3: 0 0 83;
|
||||
--chart-4: 0 0 64;
|
||||
--chart-5: 27 87% 67%;
|
||||
font-family: Inter, sans-serif;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "salt";
|
||||
}
|
||||
|
||||
.dark {
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: InterVariable, sans-serif;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "salt";
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
46
src/app/layout.tsx
Normal file
46
src/app/layout.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { cn, constructMetadata } from "@/lib/utils";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = constructMetadata({});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
colorScheme: "light",
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "white" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background antialiased w-full mx-auto scroll-smooth"
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem={false}
|
||||
>
|
||||
{children}
|
||||
<ThemeToggle />
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
105
src/app/og/route.tsx
Normal file
105
src/app/og/route.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { ImageResponse } from "next/og";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const postTitle = searchParams.get("title") || siteConfig.description;
|
||||
const font = fetch(
|
||||
new URL("../../assets/fonts/Inter-SemiBold.ttf", import.meta.url)
|
||||
).then((res) => res.arrayBuffer());
|
||||
const fontData = await font;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#fff",
|
||||
// set background image if needed
|
||||
backgroundImage: `url(${siteConfig.url}/og.png)`,
|
||||
fontSize: 32,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
top: "125px",
|
||||
}}
|
||||
>
|
||||
<Icons.logo
|
||||
style={{
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontSize: "64px",
|
||||
fontWeight: "600",
|
||||
marginTop: "24px",
|
||||
textAlign: "center",
|
||||
width: "80%",
|
||||
letterSpacing: "-0.05em", // Added tighter tracking
|
||||
}}
|
||||
>
|
||||
{postTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
marginTop: "16px",
|
||||
color: "#808080",
|
||||
}}
|
||||
>
|
||||
{siteConfig.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={`${siteConfig.url}/dashboard.png`}
|
||||
width={900}
|
||||
style={{
|
||||
position: "relative",
|
||||
bottom: -160,
|
||||
aspectRatio: "auto",
|
||||
border: "4px solid lightgray",
|
||||
background: "lightgray",
|
||||
borderRadius: 20,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
data: fontData,
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
35
src/app/page.tsx
Normal file
35
src/app/page.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Blog from "@/components/sections/blog";
|
||||
import CTA from "@/components/sections/cta";
|
||||
import FAQ from "@/components/sections/faq";
|
||||
import Features from "@/components/sections/features";
|
||||
import Footer from "@/components/sections/footer";
|
||||
import Header from "@/components/sections/header";
|
||||
import Hero from "@/components/sections/hero";
|
||||
import HowItWorks from "@/components/sections/how-it-works";
|
||||
import Logos from "@/components/sections/logos";
|
||||
import Pricing from "@/components/sections/pricing";
|
||||
import Problem from "@/components/sections/problem";
|
||||
import Solution from "@/components/sections/solution";
|
||||
import Testimonials from "@/components/sections/testimonials";
|
||||
import TestimonialsCarousel from "@/components/sections/testimonials-carousel";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<Header />
|
||||
<Hero />
|
||||
<Logos />
|
||||
<Problem />
|
||||
<Solution />
|
||||
<HowItWorks />
|
||||
<TestimonialsCarousel />
|
||||
<Features />
|
||||
<Testimonials />
|
||||
<Pricing />
|
||||
<FAQ />
|
||||
<Blog />
|
||||
<CTA />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
21
src/app/sitemap.ts
Normal file
21
src/app/sitemap.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { getBlogPosts } from "@/lib/blog";
|
||||
import { MetadataRoute } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const allPosts = await getBlogPosts();
|
||||
const headersList = headers();
|
||||
let domain = headersList.get("host") as string;
|
||||
let protocol = "https";
|
||||
|
||||
return [
|
||||
{
|
||||
url: `${protocol}://${domain}`,
|
||||
lastModified: new Date(),
|
||||
},
|
||||
...allPosts.map((post) => ({
|
||||
url: `${protocol}://${domain}/blog/${post.slug}`,
|
||||
lastModified: new Date(),
|
||||
})),
|
||||
];
|
||||
}
|
BIN
src/assets/fonts/Inter-SemiBold.ttf
Normal file
BIN
src/assets/fonts/Inter-SemiBold.ttf
Normal file
Binary file not shown.
31
src/components/avatar-circles.tsx
Normal file
31
src/components/avatar-circles.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HTMLAttributes } from "react";
|
||||
|
||||
export interface AvatarCirclesProps extends HTMLAttributes<HTMLDivElement> {
|
||||
numPeople?: number;
|
||||
avatarUrls: string[];
|
||||
}
|
||||
|
||||
export default function AvatarCircles({
|
||||
numPeople,
|
||||
avatarUrls,
|
||||
className,
|
||||
}: AvatarCirclesProps) {
|
||||
return (
|
||||
<div className={cn("z-10 flex -space-x-4 rtl:space-x-reverse", className)}>
|
||||
{avatarUrls.map((url, index) => (
|
||||
<img
|
||||
key={index}
|
||||
className="h-10 w-10 rounded-full border-2 border-white dark:border-gray-800"
|
||||
src={url}
|
||||
width={40}
|
||||
height={40}
|
||||
alt={`Avatar ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-white bg-black text-center text-xs font-medium text-white dark:border-gray-800 dark:bg-white dark:text-black">
|
||||
+{numPeople}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
73
src/components/blog-author.tsx
Normal file
73
src/components/blog-author.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Author({
|
||||
name,
|
||||
image,
|
||||
twitterUsername,
|
||||
updatedAt,
|
||||
imageOnly,
|
||||
}: {
|
||||
name: string;
|
||||
image: string;
|
||||
twitterUsername: string;
|
||||
updatedAt?: string;
|
||||
imageOnly?: boolean;
|
||||
}) {
|
||||
if (imageOnly) {
|
||||
return (
|
||||
<Image
|
||||
src={image}
|
||||
alt={name}
|
||||
width={36}
|
||||
height={36}
|
||||
className="rounded-full transition-all group-hover:brightness-90"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (updatedAt) {
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Image
|
||||
src={image}
|
||||
alt={name}
|
||||
width={36}
|
||||
height={36}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">Written by {name}</p>
|
||||
<time
|
||||
dateTime={updatedAt}
|
||||
className="text-sm font-light text-gray-400"
|
||||
>
|
||||
Last updated {formatDate(updatedAt)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`https://twitter.com/${twitterUsername}`}
|
||||
className="group flex items-center space-x-3"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
src={image}
|
||||
alt={name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full transition-all group-hover:brightness-90"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-semibold text-gray-700">{name}</p>
|
||||
<p className="text-sm text-gray-500">@{twitterUsername}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
40
src/components/blog-card.tsx
Normal file
40
src/components/blog-card.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Post } from "@/lib/blog";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function BlogCard({
|
||||
data,
|
||||
priority,
|
||||
}: {
|
||||
data: Post;
|
||||
priority?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link href={`/blog/${data.slug}`} className="block">
|
||||
<div className="bg-background rounded-lg p-4 mb-4 border hover:shadow-sm transition-shadow duration-200">
|
||||
{data.image && (
|
||||
<Image
|
||||
className="rounded-t-lg object-cover border"
|
||||
src={data.image}
|
||||
width={1200}
|
||||
height={630}
|
||||
alt={data.title}
|
||||
priority={priority}
|
||||
/>
|
||||
)}
|
||||
{!data.image && <div className="bg-gray-200 h-[180px] mb-4 rounded" />}
|
||||
<p className="mb-2">
|
||||
<time
|
||||
dateTime={data.publishedAt}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{formatDate(data.publishedAt)}
|
||||
</time>
|
||||
</p>
|
||||
<h3 className="text-xl font-semibold mb-2">{data.title}</h3>
|
||||
<p className="text-foreground mb-4">{data.summary}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
70
src/components/drawer.tsx
Normal file
70
src/components/drawer.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { IoMenuSharp } from "react-icons/io5";
|
||||
|
||||
export default function drawerDemo() {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger>
|
||||
<IoMenuSharp className="text-2xl" />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="px-6">
|
||||
<div className="">
|
||||
<Link
|
||||
href="/"
|
||||
title="brand-logo"
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="w-auto h-[40px]" />
|
||||
<span className="font-bold text-xl">{siteConfig.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav>
|
||||
<ul className="mt-7 text-left">
|
||||
{siteConfig.header.map((item, index) => (
|
||||
<li key={index} className="my-3">
|
||||
{item.trigger ? (
|
||||
<span className="font-semibold">{item.trigger}</span>
|
||||
) : (
|
||||
<Link href={item.href || ""} className="font-semibold">
|
||||
{item.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<Link
|
||||
href="/login"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
href="/signup"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
Get Started for Free
|
||||
</Link>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
342
src/components/features-horizontal.tsx
Normal file
342
src/components/features-horizontal.tsx
Normal file
@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import { BorderBeam } from "@/components/magicui/border-beam";
|
||||
import * as Accordion from "@radix-ui/react-accordion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import React, {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AccordionItemProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
} & Accordion.AccordionItemProps;
|
||||
|
||||
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<Accordion.Item
|
||||
className={cn(
|
||||
"mt-px focus-within:relative focus-within:z-10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</Accordion.Item>
|
||||
)
|
||||
);
|
||||
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
type AccordionTriggerProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<Accordion.Header className="">
|
||||
<Accordion.Trigger
|
||||
className={cn("", className)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
)
|
||||
);
|
||||
AccordionTrigger.displayName = "AccordionTrigger";
|
||||
type AccordionContentProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
} & Accordion.AccordionContentProps;
|
||||
|
||||
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<Accordion.Content
|
||||
className={cn(
|
||||
"data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<div className="px-5 py-2">{children}</div>
|
||||
</Accordion.Content>
|
||||
)
|
||||
);
|
||||
AccordionContent.displayName = "AccordionContent";
|
||||
|
||||
type CardDataProps = {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
image?: string;
|
||||
video?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export type FeaturesProps = {
|
||||
collapseDelay?: number;
|
||||
ltr?: boolean;
|
||||
linePosition?: "left" | "right" | "top" | "bottom";
|
||||
data: CardDataProps[];
|
||||
};
|
||||
|
||||
export default function Features({
|
||||
collapseDelay = 5000,
|
||||
ltr = false,
|
||||
linePosition = "left",
|
||||
data = [],
|
||||
}: FeaturesProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(-1);
|
||||
|
||||
const carouselRef = useRef<HTMLUListElement>(null);
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, {
|
||||
once: true,
|
||||
amount: 0.5,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (isInView) {
|
||||
setCurrentIndex(0);
|
||||
} else {
|
||||
setCurrentIndex(-1);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isInView]);
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
if (carouselRef.current) {
|
||||
const card = carouselRef.current.querySelectorAll(".card")[index];
|
||||
if (card) {
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const carouselRect = carouselRef.current.getBoundingClientRect();
|
||||
const offset =
|
||||
cardRect.left -
|
||||
carouselRect.left -
|
||||
(carouselRect.width - cardRect.width) / 2;
|
||||
|
||||
carouselRef.current.scrollTo({
|
||||
left: carouselRef.current.scrollLeft + offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentIndex((prevIndex) =>
|
||||
prevIndex !== undefined ? (prevIndex + 1) % data.length : 0
|
||||
);
|
||||
}, collapseDelay);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [currentIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAutoScroll = () => {
|
||||
const nextIndex =
|
||||
(currentIndex !== undefined ? currentIndex + 1 : 0) % data.length;
|
||||
scrollToIndex(nextIndex);
|
||||
};
|
||||
|
||||
const autoScrollTimer = setInterval(handleAutoScroll, collapseDelay);
|
||||
|
||||
return () => clearInterval(autoScrollTimer);
|
||||
}, [currentIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const carousel = carouselRef.current;
|
||||
if (carousel) {
|
||||
const handleScroll = () => {
|
||||
const scrollLeft = carousel.scrollLeft;
|
||||
const cardWidth = carousel.querySelector(".card")?.clientWidth || 0;
|
||||
const newIndex = Math.min(
|
||||
Math.floor(scrollLeft / cardWidth),
|
||||
data.length - 1
|
||||
);
|
||||
setCurrentIndex(newIndex);
|
||||
};
|
||||
|
||||
carousel.addEventListener("scroll", handleScroll);
|
||||
return () => carousel.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={ref} id="features">
|
||||
<div className="container">
|
||||
<div className="max-w-6xl mx-auto ">
|
||||
<div className="">
|
||||
<div
|
||||
className={`hidden md:flex order-1 md:order-[0] ${
|
||||
ltr ? "md:order-2 md:justify-end" : "justify-start"
|
||||
}`}
|
||||
>
|
||||
<Accordion.Root
|
||||
className="grid md:grid-cols-4 gap-x-10 py-8"
|
||||
type="single"
|
||||
defaultValue={`item-${currentIndex}`}
|
||||
value={`item-${currentIndex}`}
|
||||
onValueChange={(value) =>
|
||||
setCurrentIndex(Number(value.split("-")[1]))
|
||||
}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<AccordionItem
|
||||
key={item.id}
|
||||
className="relative mb-8"
|
||||
value={`item-${index}`}
|
||||
>
|
||||
{linePosition === "left" || linePosition === "right" ? (
|
||||
<div
|
||||
className={`absolute bottom-0 top-0 h-full w-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
|
||||
linePosition === "right"
|
||||
? "left-auto right-0"
|
||||
: "left-0 right-auto"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute left-0 top-0 w-full ${
|
||||
currentIndex === index ? "h-full" : "h-0"
|
||||
} origin-top bg-primary transition-all ease-linear dark:bg-white`}
|
||||
style={{
|
||||
transitionDuration:
|
||||
currentIndex === index
|
||||
? `${collapseDelay}ms`
|
||||
: "0s",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{linePosition === "top" || linePosition === "bottom" ? (
|
||||
<div
|
||||
className={`absolute left-0 right-0 w-full h-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
|
||||
linePosition === "bottom" ? "bottom-0" : "top-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute left-0 ${
|
||||
linePosition === "bottom" ? "bottom-0" : "top-0"
|
||||
} h-full ${
|
||||
currentIndex === index ? "w-full" : "w-0"
|
||||
} origin-left bg-primary transition-all ease-linear dark:bg-white`}
|
||||
style={{
|
||||
transitionDuration:
|
||||
currentIndex === index
|
||||
? `${collapseDelay}ms`
|
||||
: "0s",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<AccordionTrigger>
|
||||
<div className="flex items-center relative flex-col">
|
||||
<div className="item-box size-16 bg-primary/10 rounded-full sm:mx-6 mx-2 shrink-0 flex items-center justify-center">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="font-bold text-xl my-3 ">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="justify-center text-center mb-4">
|
||||
{item.content}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
<div
|
||||
className={`w-auto overflow-hidden relative rounded-lg ${
|
||||
ltr && "md:order-1"
|
||||
}`}
|
||||
>
|
||||
{data[currentIndex]?.image ? (
|
||||
<motion.img
|
||||
key={currentIndex}
|
||||
src={data[currentIndex].image}
|
||||
alt="feature"
|
||||
className="aspect-auto h-full w-full object-cover relative border rounded-lg shadow-lg"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.25, ease: "easeOut" }}
|
||||
/>
|
||||
) : data[currentIndex]?.video ? (
|
||||
<video
|
||||
preload="auto"
|
||||
src={data[currentIndex].video}
|
||||
className="aspect-auto h-full w-full rounded-lg object-cover border shadow-lg"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-auto h-full w-full rounded-xl border border-neutral-300/50 bg-gray-200 p-1 min-h-[600px]"></div>
|
||||
)}
|
||||
<BorderBeam
|
||||
size={400}
|
||||
duration={12}
|
||||
delay={9}
|
||||
borderWidth={1.5}
|
||||
colorFrom="hsl(var(--primary))"
|
||||
colorTo="hsl(var(--primary)/0)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
ref={carouselRef}
|
||||
className="flex h-full snap-x flex-nowrap overflow-x-auto py-10 [-ms-overflow-style:none] [-webkit-mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [scrollbar-width:none] md:hidden [&::-webkit-scrollbar]:hidden snap-mandatory"
|
||||
style={{
|
||||
padding: "50px calc(50%)",
|
||||
}}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="card relative mr-8 grid h-full max-w-60 shrink-0 items-start justify-center py-4 last:mr-0"
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
style={{
|
||||
scrollSnapAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div className="absolute bottom-0 left-0 right-auto top-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30">
|
||||
<div
|
||||
className={`absolute left-0 top-0 h-full ${
|
||||
currentIndex === index ? "w-full" : "w-0"
|
||||
} origin-top bg-primary transition-all ease-linear dark:bg-white`}
|
||||
style={{
|
||||
transitionDuration:
|
||||
currentIndex === index ? `${collapseDelay}ms` : "0s",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">{item.title}</h2>
|
||||
<p className="mx-0 max-w-sm text-balance text-sm">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
337
src/components/features-vertical.tsx
Normal file
337
src/components/features-vertical.tsx
Normal file
@ -0,0 +1,337 @@
|
||||
"use client";
|
||||
|
||||
import * as Accordion from "@radix-ui/react-accordion";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import React, {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AccordionItemProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
} & Accordion.AccordionItemProps;
|
||||
|
||||
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<Accordion.Item
|
||||
className={cn(
|
||||
"mt-px overflow-hidden focus-within:relative focus-within:z-10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</Accordion.Item>
|
||||
)
|
||||
);
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
type AccordionTriggerProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<Accordion.Header className="flex">
|
||||
<Accordion.Trigger
|
||||
className={cn(
|
||||
"group flex flex-1 cursor-pointer items-center justify-between px-5 text-[15px] leading-none outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
)
|
||||
);
|
||||
AccordionTrigger.displayName = "AccordionTrigger";
|
||||
type AccordionContentProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
} & Accordion.AccordionContentProps;
|
||||
|
||||
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
|
||||
({ children, className, ...props }, forwardedRef) => (
|
||||
<Accordion.Content
|
||||
className={cn(
|
||||
"overflow-hidden text-[15px] font-medium data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<div className="px-5 py-2">{children}</div>
|
||||
</Accordion.Content>
|
||||
)
|
||||
);
|
||||
AccordionContent.displayName = "AccordionContent";
|
||||
|
||||
export type FeaturesDataProps = {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
image?: string;
|
||||
video?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export type FeaturesProps = {
|
||||
collapseDelay?: number;
|
||||
ltr?: boolean;
|
||||
linePosition?: "left" | "right" | "top" | "bottom";
|
||||
data: FeaturesDataProps[];
|
||||
};
|
||||
|
||||
export default function Features({
|
||||
collapseDelay = 5000,
|
||||
ltr = false,
|
||||
linePosition = "left",
|
||||
data = [],
|
||||
}: FeaturesProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(-1);
|
||||
const carouselRef = useRef<HTMLUListElement>(null);
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, {
|
||||
once: true,
|
||||
amount: 0.5,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (isInView) {
|
||||
setCurrentIndex(0);
|
||||
} else {
|
||||
setCurrentIndex(-1);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isInView]);
|
||||
|
||||
const scrollToIndex = (index: number) => {
|
||||
if (carouselRef.current) {
|
||||
const card = carouselRef.current.querySelectorAll(".card")[index];
|
||||
if (card) {
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const carouselRect = carouselRef.current.getBoundingClientRect();
|
||||
const offset =
|
||||
cardRect.left -
|
||||
carouselRect.left -
|
||||
(carouselRect.width - cardRect.width) / 2;
|
||||
|
||||
carouselRef.current.scrollTo({
|
||||
left: carouselRef.current.scrollLeft + offset,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentIndex((prevIndex) =>
|
||||
prevIndex !== undefined ? (prevIndex + 1) % data.length : 0
|
||||
);
|
||||
}, collapseDelay);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [collapseDelay, currentIndex, data.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAutoScroll = () => {
|
||||
const nextIndex =
|
||||
(currentIndex !== undefined ? currentIndex + 1 : 0) % data.length;
|
||||
scrollToIndex(nextIndex);
|
||||
};
|
||||
|
||||
const autoScrollTimer = setInterval(handleAutoScroll, collapseDelay);
|
||||
|
||||
return () => clearInterval(autoScrollTimer);
|
||||
}, [collapseDelay, currentIndex, data.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const carousel = carouselRef.current;
|
||||
if (carousel) {
|
||||
const handleScroll = () => {
|
||||
const scrollLeft = carousel.scrollLeft;
|
||||
const cardWidth = carousel.querySelector(".card")?.clientWidth || 0;
|
||||
const newIndex = Math.min(
|
||||
Math.floor(scrollLeft / cardWidth),
|
||||
data.length - 1
|
||||
);
|
||||
setCurrentIndex(newIndex);
|
||||
};
|
||||
|
||||
carousel.addEventListener("scroll", handleScroll);
|
||||
return () => carousel.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, [data.length]);
|
||||
|
||||
return (
|
||||
<section ref={ref} id="features">
|
||||
<div className="container">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mx-auto my-12 h-full grid lg:grid-cols-2 gap-10 items-center">
|
||||
<div
|
||||
className={` hidden lg:flex order-1 lg:order-[0] ${
|
||||
ltr ? "lg:order-2 lg:justify-end" : "justify-start"
|
||||
}`}
|
||||
>
|
||||
<Accordion.Root
|
||||
className=""
|
||||
type="single"
|
||||
defaultValue={`item-${currentIndex}`}
|
||||
value={`item-${currentIndex}`}
|
||||
onValueChange={(value) =>
|
||||
setCurrentIndex(Number(value.split("-")[1]))
|
||||
}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<AccordionItem
|
||||
key={item.id}
|
||||
className="relative mb-8 last:mb-0"
|
||||
value={`item-${index}`}
|
||||
>
|
||||
{linePosition === "left" || linePosition === "right" ? (
|
||||
<div
|
||||
className={`absolute bottom-0 top-0 h-full w-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
|
||||
linePosition === "right"
|
||||
? "left-auto right-0"
|
||||
: "left-0 right-auto"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute left-0 top-0 w-full ${
|
||||
currentIndex === index ? "h-full" : "h-0"
|
||||
} origin-top bg-primary transition-all ease-linear dark:bg-white`}
|
||||
style={{
|
||||
transitionDuration:
|
||||
currentIndex === index
|
||||
? `${collapseDelay}ms`
|
||||
: "0s",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{linePosition === "top" || linePosition === "bottom" ? (
|
||||
<div
|
||||
className={`absolute left-0 right-0 w-full h-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
|
||||
linePosition === "bottom" ? "bottom-0" : "top-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute left-0 ${
|
||||
linePosition === "bottom" ? "bottom-0" : "top-0"
|
||||
} h-full ${
|
||||
currentIndex === index ? "w-full" : "w-0"
|
||||
} origin-left bg-primary transition-all ease-linear dark:bg-white`}
|
||||
style={{
|
||||
transitionDuration:
|
||||
currentIndex === index
|
||||
? `${collapseDelay}ms`
|
||||
: "0s",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center relative">
|
||||
<div className="item-box w-12 h-12 bg-primary/10 rounded-full sm:mx-6 mx-2 shrink-0 flex items-center justify-center">
|
||||
{item.icon}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AccordionTrigger className="text-xl font-bold pl-0">
|
||||
{item.title}
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionTrigger className="justify-start text-left leading-4 text-[16px] pl-0">
|
||||
{item.content}
|
||||
</AccordionTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
<div
|
||||
className={`h-[350px] min-h-[200px] w-auto ${
|
||||
ltr && "lg:order-1"
|
||||
}`}
|
||||
>
|
||||
{data[currentIndex]?.image ? (
|
||||
<motion.img
|
||||
key={currentIndex}
|
||||
src={data[currentIndex].image}
|
||||
alt="feature"
|
||||
className="aspect-auto h-full w-full rounded-xl border border-neutral-300/50 object-cover object-left-top p-1 shadow-lg"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.25, ease: "easeOut" }}
|
||||
/>
|
||||
) : data[currentIndex]?.video ? (
|
||||
<video
|
||||
preload="auto"
|
||||
src={data[currentIndex].video}
|
||||
className="aspect-auto h-full w-full rounded-lg object-cover shadow-lg"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-auto h-full w-full rounded-xl border border-neutral-300/50 bg-gray-200 p-1"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul
|
||||
ref={carouselRef}
|
||||
className=" flex h-full snap-x flex-nowrap overflow-x-auto py-10 [-ms-overflow-style:none] [-webkit-mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [scrollbar-width:none] lg:hidden [&::-webkit-scrollbar]:hidden snap-mandatory"
|
||||
style={{
|
||||
padding: "50px calc(50%)",
|
||||
}}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="card relative mr-8 grid h-full max-w-60 shrink-0 items-start justify-center py-4 last:mr-0"
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
style={{
|
||||
scrollSnapAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div className="absolute bottom-0 left-0 right-auto top-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30">
|
||||
<div
|
||||
className={`absolute left-0 top-0 h-full ${
|
||||
currentIndex === index ? "w-full" : "w-0"
|
||||
} origin-top bg-primary transition-all ease-linear`}
|
||||
style={{
|
||||
transitionDuration:
|
||||
currentIndex === index ? `${collapseDelay}ms` : "0s",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold">{item.title}</h2>
|
||||
<p className="mx-0 max-w-sm text-balance text-sm">
|
||||
{item.content}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
138
src/components/icons.tsx
Normal file
138
src/components/icons.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
type IconProps = React.HTMLAttributes<SVGElement>;
|
||||
|
||||
export const Icons = {
|
||||
logo: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3" />
|
||||
</svg>
|
||||
),
|
||||
twitter: (props: IconProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
height="23"
|
||||
viewBox="0 0 1200 1227"
|
||||
width="23"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" />
|
||||
</svg>
|
||||
),
|
||||
github: (props: IconProps) => (
|
||||
<svg viewBox="0 0 438.549 438.549" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
radix: (props: IconProps) => (
|
||||
<svg viewBox="0 0 25 25" fill="none" {...props}>
|
||||
<path
|
||||
d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"
|
||||
fill="currentcolor"
|
||||
></path>
|
||||
<path d="M12 0H4V8H12V0Z" fill="currentcolor"></path>
|
||||
<path
|
||||
d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"
|
||||
fill="currentcolor"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
aria: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path d="M13.966 22.624l-1.69-4.281H8.122l3.892-9.144 5.662 13.425zM8.884 1.376H0v21.248zm15.116 0h-8.884L24 22.624Z" />
|
||||
</svg>
|
||||
),
|
||||
npm: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
yarn: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
pnpm: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
react: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
tailwind: (props: IconProps) => (
|
||||
<svg viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
google: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
apple: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
paypal: (props: IconProps) => (
|
||||
<svg role="img" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
spinner: (props: IconProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
),
|
||||
};
|
60
src/components/magicui/blur-fade.tsx
Normal file
60
src/components/magicui/blur-fade.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, useInView, Variants } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
|
||||
type MarginType = `${number}${"px" | "%"}`;
|
||||
|
||||
interface BlurFadeProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { y: number };
|
||||
visible: { y: number };
|
||||
};
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
yOffset?: number;
|
||||
inView?: boolean;
|
||||
inViewMargin?: MarginType;
|
||||
blur?: string;
|
||||
}
|
||||
export default function BlurFade({
|
||||
children,
|
||||
className,
|
||||
variant,
|
||||
duration = 0.4,
|
||||
delay = 0,
|
||||
yOffset = 6,
|
||||
inView = false,
|
||||
inViewMargin = "-50px",
|
||||
blur = "6px",
|
||||
}: BlurFadeProps) {
|
||||
const ref = useRef(null);
|
||||
const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
|
||||
const isInView = !inView || inViewResult;
|
||||
const defaultVariants: Variants = {
|
||||
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
|
||||
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
exit="hidden"
|
||||
variants={combinedVariants}
|
||||
transition={{
|
||||
delay: 0.04 + delay,
|
||||
duration,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
49
src/components/magicui/border-beam.tsx
Normal file
49
src/components/magicui/border-beam.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface BorderBeamProps {
|
||||
className?: string;
|
||||
size?: number;
|
||||
duration?: number;
|
||||
borderWidth?: number;
|
||||
anchor?: number;
|
||||
colorFrom?: string;
|
||||
colorTo?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const BorderBeam = ({
|
||||
className,
|
||||
size = 200,
|
||||
duration = 15,
|
||||
anchor = 90,
|
||||
borderWidth = 1.5,
|
||||
colorFrom = "#ffaa40",
|
||||
colorTo = "#9c40ff",
|
||||
delay = 0,
|
||||
}: BorderBeamProps) => {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--size": size,
|
||||
"--duration": duration,
|
||||
"--anchor": anchor,
|
||||
"--border-width": borderWidth,
|
||||
"--color-from": colorFrom,
|
||||
"--color-to": colorTo,
|
||||
"--delay": `-${delay}s`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]",
|
||||
|
||||
// mask styles
|
||||
"![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
|
||||
|
||||
// pseudo styles
|
||||
"after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
51
src/components/magicui/dot-pattern.tsx
Normal file
51
src/components/magicui/dot-pattern.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
interface DotPatternProps {
|
||||
width?: any;
|
||||
height?: any;
|
||||
x?: any;
|
||||
y?: any;
|
||||
cx?: any;
|
||||
cy?: any;
|
||||
cr?: any;
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
export function DotPattern({
|
||||
width = 16,
|
||||
height = 16,
|
||||
x = 0,
|
||||
y = 0,
|
||||
cx = 1,
|
||||
cy = 1,
|
||||
cr = 1,
|
||||
className,
|
||||
...props
|
||||
}: DotPatternProps) {
|
||||
const id = "pattern-circle";
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80"
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={id}
|
||||
width={width}
|
||||
height={height}
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternContentUnits="userSpaceOnUse"
|
||||
x={x}
|
||||
y={y}
|
||||
>
|
||||
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default DotPattern;
|
194
src/components/magicui/flickering-grid.tsx
Normal file
194
src/components/magicui/flickering-grid.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface FlickeringGridProps {
|
||||
squareSize?: number;
|
||||
gridGap?: number;
|
||||
flickerChance?: number;
|
||||
color?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
maxOpacity?: number;
|
||||
}
|
||||
|
||||
const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
squareSize = 4,
|
||||
gridGap = 6,
|
||||
flickerChance = 0.3,
|
||||
color = "rgb(0, 0, 0)",
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
maxOpacity = 0.3,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
|
||||
const memoizedColor = useMemo(() => {
|
||||
const toRGBA = (color: string) => {
|
||||
if (typeof window === "undefined") {
|
||||
return `rgba(0, 0, 0,`;
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = canvas.height = 1;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return "rgba(255, 0, 0,";
|
||||
|
||||
// Handle HSL colors
|
||||
if (color.startsWith("hsl")) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return `rgba(${r}, ${g}, ${b},`;
|
||||
}
|
||||
|
||||
// Handle other color formats (rgb, hex, etc.)
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return `rgba(${r}, ${g}, ${b},`;
|
||||
};
|
||||
return toRGBA(color);
|
||||
}, [color]);
|
||||
|
||||
const setupCanvas = useCallback(
|
||||
(canvas: HTMLCanvasElement) => {
|
||||
const canvasWidth = width || canvas.clientWidth;
|
||||
const canvasHeight = height || canvas.clientHeight;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = canvasWidth * dpr;
|
||||
canvas.height = canvasHeight * dpr;
|
||||
canvas.style.width = `${canvasWidth}px`;
|
||||
canvas.style.height = `${canvasHeight}px`;
|
||||
const cols = Math.floor(canvasWidth / (squareSize + gridGap));
|
||||
const rows = Math.floor(canvasHeight / (squareSize + gridGap));
|
||||
|
||||
const squares = new Float32Array(cols * rows);
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
squares[i] = Math.random() * maxOpacity;
|
||||
}
|
||||
|
||||
return {
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
cols,
|
||||
rows,
|
||||
squares,
|
||||
dpr,
|
||||
};
|
||||
},
|
||||
[squareSize, gridGap, width, height, maxOpacity]
|
||||
);
|
||||
|
||||
const updateSquares = useCallback(
|
||||
(squares: Float32Array, deltaTime: number) => {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < flickerChance * deltaTime) {
|
||||
squares[i] = Math.random() * maxOpacity;
|
||||
}
|
||||
}
|
||||
},
|
||||
[flickerChance, maxOpacity]
|
||||
);
|
||||
|
||||
const drawGrid = useCallback(
|
||||
(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cols: number,
|
||||
rows: number,
|
||||
squares: Float32Array,
|
||||
dpr: number
|
||||
) => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = "transparent";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j];
|
||||
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
||||
ctx.fillRect(
|
||||
i * (squareSize + gridGap) * dpr,
|
||||
j * (squareSize + gridGap) * dpr,
|
||||
squareSize * dpr,
|
||||
squareSize * dpr
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[memoizedColor, squareSize, gridGap]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let { width, height, cols, rows, squares, dpr } = setupCanvas(canvas);
|
||||
|
||||
let lastTime = 0;
|
||||
const animate = (time: number) => {
|
||||
if (!isInView) return;
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000;
|
||||
lastTime = time;
|
||||
|
||||
updateSquares(squares, deltaTime);
|
||||
drawGrid(ctx, width * dpr, height * dpr, cols, rows, squares, dpr);
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
({ width, height, cols, rows, squares, dpr } = setupCanvas(canvas));
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsInView(entry.isIntersecting);
|
||||
},
|
||||
{ threshold: 0 }
|
||||
);
|
||||
|
||||
observer.observe(canvas);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
if (isInView) {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`size-full pointer-events-none ${className}`}
|
||||
style={{
|
||||
width: width || "100%",
|
||||
height: height || "100%",
|
||||
}}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlickeringGrid;
|
140
src/components/magicui/hero-video.tsx
Normal file
140
src/components/magicui/hero-video.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Play, XIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
type AnimationStyle =
|
||||
| "from-bottom"
|
||||
| "from-center"
|
||||
| "from-top"
|
||||
| "from-left"
|
||||
| "from-right"
|
||||
| "fade"
|
||||
| "top-in-bottom-out"
|
||||
| "left-in-right-out";
|
||||
|
||||
interface HeroVideoProps {
|
||||
animationStyle?: AnimationStyle;
|
||||
videoSrc: string;
|
||||
thumbnailSrc: string;
|
||||
thumbnailAlt?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const animationVariants = {
|
||||
"from-bottom": {
|
||||
initial: { y: "100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 },
|
||||
},
|
||||
"from-center": {
|
||||
initial: { scale: 0.5, opacity: 0 },
|
||||
animate: { scale: 1, opacity: 1 },
|
||||
exit: { scale: 0.5, opacity: 0 },
|
||||
},
|
||||
"from-top": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "-100%", opacity: 0 },
|
||||
},
|
||||
"from-left": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "-100%", opacity: 0 },
|
||||
},
|
||||
"from-right": {
|
||||
initial: { x: "100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 },
|
||||
},
|
||||
fade: {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
},
|
||||
"top-in-bottom-out": {
|
||||
initial: { y: "-100%", opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { y: "100%", opacity: 0 },
|
||||
},
|
||||
"left-in-right-out": {
|
||||
initial: { x: "-100%", opacity: 0 },
|
||||
animate: { x: 0, opacity: 1 },
|
||||
exit: { x: "100%", opacity: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
export default function HeroVideoDialog({
|
||||
animationStyle = "from-center",
|
||||
videoSrc,
|
||||
thumbnailSrc,
|
||||
thumbnailAlt = "Video thumbnail",
|
||||
className,
|
||||
}: HeroVideoProps) {
|
||||
const [isVideoOpen, setIsVideoOpen] = useState(false);
|
||||
const selectedAnimation = animationVariants[animationStyle];
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
<div
|
||||
className="relative cursor-pointer group rounded-md p-2 ring-1 ring-slate-200/50 dark:bg-gray-900/70 dark:ring-white/10 backdrop-blur-md"
|
||||
onClick={() => setIsVideoOpen(true)}
|
||||
>
|
||||
<Image
|
||||
src={thumbnailSrc}
|
||||
alt={thumbnailAlt}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="transition-all duration-200 group-hover:brightness-[0.8] ease-out rounded-md border"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center group-hover:scale-100 scale-[0.9] transition-all duration-200 ease-out rounded-2xl">
|
||||
<div className="z-30 bg-primary/10 flex items-center justify-center rounded-full backdrop-blur-md size-28">
|
||||
<div
|
||||
className={`flex items-center justify-center bg-gradient-to-b from-primary/30 to-primary shadow-md rounded-full size-20 transition-all ease-out duration-200 relative group-hover:scale-[1.2] scale-100`}
|
||||
>
|
||||
<Play
|
||||
className="size-8 text-white fill-white group-hover:scale-105 scale-100 transition-transform duration-200 ease-out"
|
||||
style={{
|
||||
filter:
|
||||
"drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isVideoOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
onClick={() => setIsVideoOpen(false)}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
|
||||
>
|
||||
<motion.div
|
||||
{...selectedAnimation}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
|
||||
>
|
||||
<motion.button className="absolute -top-16 right-0 text-white text-xl bg-neutral-900/50 ring-1 backdrop-blur-md rounded-full p-2 dark:bg-neutral-100/50 dark:text-black">
|
||||
<XIcon className="size-5" />
|
||||
</motion.button>
|
||||
<div className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative">
|
||||
<iframe
|
||||
src={videoSrc}
|
||||
className="size-full rounded-2xl"
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
51
src/components/magicui/marquee.tsx
Normal file
51
src/components/magicui/marquee.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MarqueeProps {
|
||||
className?: string;
|
||||
reverse?: boolean;
|
||||
pauseOnHover?: boolean;
|
||||
children?: React.ReactNode;
|
||||
vertical?: boolean;
|
||||
repeat?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default function Marquee({
|
||||
className,
|
||||
reverse,
|
||||
pauseOnHover = false,
|
||||
children,
|
||||
vertical = false,
|
||||
repeat = 4,
|
||||
...props
|
||||
}: MarqueeProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
|
||||
{
|
||||
"flex-row": !vertical,
|
||||
"flex-col": vertical,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{Array(repeat)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
|
||||
"animate-marquee flex-row": !vertical,
|
||||
"animate-marquee-vertical flex-col": vertical,
|
||||
"group-hover:[animation-play-state:paused]": pauseOnHover,
|
||||
"[animation-direction:reverse]": reverse,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
58
src/components/magicui/ripple.tsx
Normal file
58
src/components/magicui/ripple.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { CSSProperties } from "react";
|
||||
|
||||
interface RippleProps {
|
||||
mainCircleSize?: number;
|
||||
mainCircleOpacity?: number;
|
||||
numCircles?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Ripple = React.memo(function Ripple({
|
||||
mainCircleSize = 210,
|
||||
mainCircleOpacity = 0.24,
|
||||
numCircles = 8,
|
||||
className,
|
||||
}: RippleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Array.from({ length: numCircles }, (_, i) => {
|
||||
const size = mainCircleSize + i * 70;
|
||||
const opacity = mainCircleOpacity - i * 0.03;
|
||||
const animationDelay = `${i * 0.06}s`;
|
||||
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
|
||||
const borderOpacity = 5 + i * 5;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`absolute animate-ripple rounded-full bg-foreground/25 shadow-xl border [--i:${i}]`}
|
||||
style={
|
||||
{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
opacity,
|
||||
animationDelay,
|
||||
borderStyle,
|
||||
borderWidth: "1px",
|
||||
borderColor: `hsl(var(--foreground), ${borderOpacity / 100})`,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Ripple.displayName = "Ripple";
|
||||
|
||||
export default Ripple;
|
110
src/components/menu.tsx
Normal file
110
src/components/menu.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function NavigationMenuDemo() {
|
||||
return (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{siteConfig.header.map((item, index) => (
|
||||
<NavigationMenuItem key={index}>
|
||||
{item.trigger ? (
|
||||
<>
|
||||
<NavigationMenuTrigger>{item.trigger}</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul
|
||||
className={`grid gap-3 p-6 ${
|
||||
item.content.main
|
||||
? "md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]"
|
||||
: "w-[400px] md:w-[500px] md:grid-cols-2 lg:w-[600px]"
|
||||
}`}
|
||||
>
|
||||
{item.content.main && (
|
||||
<li className="row-span-3">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-primary/10 from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
|
||||
href={item.content.main.href}
|
||||
>
|
||||
{item.content.main.icon}
|
||||
<div className="mb-2 mt-4 text-lg font-medium">
|
||||
{item.content.main.title}
|
||||
</div>
|
||||
<p className="text-sm leading-tight text-muted-foreground">
|
||||
{item.content.main.description}
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
)}
|
||||
{item.content.items.map((subItem, subIndex) => (
|
||||
<ListItem
|
||||
key={subIndex}
|
||||
href={subItem.href}
|
||||
title={subItem.title}
|
||||
className="hover:bg-primary/10"
|
||||
>
|
||||
{subItem.description}
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href || ""}
|
||||
target="_arya"
|
||||
legacyBehavior
|
||||
passHref
|
||||
>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
{item.label}
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
)}
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const ListItem = React.forwardRef<
|
||||
React.ElementRef<"a">,
|
||||
React.ComponentPropsWithoutRef<"a">
|
||||
>(({ className, title, children, ...props }, ref) => {
|
||||
return (
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">{title}</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
{children}
|
||||
</p>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
ListItem.displayName = "ListItem";
|
66
src/components/pie-chart.tsx
Normal file
66
src/components/pie-chart.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Pie, PieChart } from "recharts";
|
||||
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
const chartData = [
|
||||
{ browser: "chrome", visitors: 187, fill: "var(--color-chrome)" },
|
||||
{ browser: "safari", visitors: 110, fill: "var(--color-safari)" },
|
||||
{ browser: "firefox", visitors: 165, fill: "var(--color-firefox)" },
|
||||
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
chrome: {
|
||||
label: "Chrome",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
safari: {
|
||||
label: "Safari",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
firefox: {
|
||||
label: "Firefox",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
edge: {
|
||||
label: "Edge",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col border-none shadow-none">
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square max-h-[250px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie data={chartData} dataKey="visitors" nameKey="browser" />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="leading-2 text-muted-foreground text-center">
|
||||
Effective marketing and advertising materials. It is also a great
|
||||
tool.
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
76
src/components/radial-chart.tsx
Normal file
76
src/components/radial-chart.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||
import {
|
||||
Label,
|
||||
PolarGrid,
|
||||
PolarRadiusAxis,
|
||||
RadialBar,
|
||||
RadialBarChart,
|
||||
} from "recharts";
|
||||
const chartData = [
|
||||
{ browser: "safari", visitors: 1260, fill: "var(--color-safari)" },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
safari: {
|
||||
label: "Safari",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="border-none shadow-none">
|
||||
<CardContent className="flex items-center">
|
||||
<div>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square w-[100px] h-[110px]"
|
||||
>
|
||||
<RadialBarChart
|
||||
data={chartData}
|
||||
startAngle={-190}
|
||||
endAngle={70}
|
||||
innerRadius={34}
|
||||
outerRadius={50}
|
||||
>
|
||||
<PolarGrid
|
||||
gridType="circle"
|
||||
radialLines={false}
|
||||
stroke="none"
|
||||
className="first:fill-muted last:fill-background"
|
||||
polarRadius={[37, 30]}
|
||||
/>
|
||||
<RadialBar dataKey="visitors" background cornerRadius={20} />
|
||||
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
></text>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PolarRadiusAxis>
|
||||
</RadialBarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<div className="ml-5">
|
||||
<h2 className="font-semibold text-xl">Title</h2>
|
||||
<p>Effective marketing and advertising materials.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
153
src/components/safari.tsx
Normal file
153
src/components/safari.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export interface SafariProps extends SVGProps<SVGSVGElement> {
|
||||
url?: string;
|
||||
src?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export default function Safari({
|
||||
src,
|
||||
url,
|
||||
width = 1203,
|
||||
height = 753,
|
||||
...props
|
||||
}: SafariProps) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#path0)">
|
||||
<path
|
||||
d="M0 52H1202V741C1202 747.627 1196.63 753 1190 753H12C5.37258 753 0 747.627 0 741V52Z"
|
||||
className="fill-[#E5E5E5] dark:fill-[#404040]"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 12C0 5.37258 5.37258 0 12 0H1190C1196.63 0 1202 5.37258 1202 12V52H0L0 12Z"
|
||||
className="fill-[#E5E5E5] dark:fill-[#404040]"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.06738 12C1.06738 5.92487 5.99225 1 12.0674 1H1189.93C1196.01 1 1200.93 5.92487 1200.93 12V51H1.06738V12Z"
|
||||
className="fill-white dark:fill-[#262626]"
|
||||
/>
|
||||
<circle
|
||||
cx="27"
|
||||
cy="25"
|
||||
r="6"
|
||||
className="fill-[#E5E5E5] dark:fill-[#404040]"
|
||||
/>
|
||||
<circle
|
||||
cx="47"
|
||||
cy="25"
|
||||
r="6"
|
||||
className="fill-[#E5E5E5] dark:fill-[#404040]"
|
||||
/>
|
||||
<circle
|
||||
cx="67"
|
||||
cy="25"
|
||||
r="6"
|
||||
className="fill-[#E5E5E5] dark:fill-[#404040]"
|
||||
/>
|
||||
<path
|
||||
d="M286 17C286 13.6863 288.686 11 292 11H946C949.314 11 952 13.6863 952 17V35C952 38.3137 949.314 41 946 41H292C288.686 41 286 38.3137 286 35V17Z"
|
||||
fill="#F5F5F5"
|
||||
/>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M566.269 32.0852H572.426C573.277 32.0852 573.696 31.6663 573.696 30.7395V25.9851C573.696 25.1472 573.353 24.7219 572.642 24.6521V23.0842C572.642 20.6721 571.036 19.5105 569.348 19.5105C567.659 19.5105 566.053 20.6721 566.053 23.0842V24.6711C565.393 24.7727 565 25.1917 565 25.9851V30.7395C565 31.6663 565.418 32.0852 566.269 32.0852ZM567.272 22.97C567.272 21.491 568.211 20.6785 569.348 20.6785C570.478 20.6785 571.423 21.491 571.423 22.97V24.6394L567.272 24.6458V22.97Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<text
|
||||
x="580"
|
||||
y="30"
|
||||
fill="#A3A3A3"
|
||||
fontSize="12"
|
||||
fontFamily="Arial, sans-serif"
|
||||
>
|
||||
{url}
|
||||
</text>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M265.5 33.8984C265.641 33.8984 265.852 33.8516 266.047 33.7422C270.547 31.2969 272.109 30.1641 272.109 27.3203V21.4219C272.109 20.4844 271.742 20.1484 270.961 19.8125C270.094 19.4453 267.18 18.4297 266.328 18.1406C266.07 18.0547 265.766 18 265.5 18C265.234 18 264.93 18.0703 264.672 18.1406C263.82 18.3828 260.906 19.4531 260.039 19.8125C259.258 20.1406 258.891 20.4844 258.891 21.4219V27.3203C258.891 30.1641 260.461 31.2812 264.945 33.7422C265.148 33.8516 265.359 33.8984 265.5 33.8984ZM265.922 19.5781C266.945 19.9766 269.172 20.7656 270.344 21.1875C270.562 21.2656 270.617 21.3828 270.617 21.6641V27.0234C270.617 29.3125 269.469 29.9375 265.945 32.0625C265.727 32.1875 265.617 32.2344 265.508 32.2344V19.4844C265.617 19.4844 265.734 19.5156 265.922 19.5781Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M936.273 24.9766C936.5 24.9766 936.68 24.9062 936.82 24.7578L940.023 21.5312C940.195 21.3594 940.273 21.1719 940.273 20.9531C940.273 20.7422 940.188 20.5391 940.023 20.3828L936.82 17.125C936.68 16.9688 936.5 16.8906 936.273 16.8906C935.852 16.8906 935.516 17.2422 935.516 17.6719C935.516 17.8828 935.594 18.0547 935.727 18.2031L937.594 20.0312C937.227 19.9766 936.852 19.9453 936.477 19.9453C932.609 19.9453 929.516 23.0391 929.516 26.9141C929.516 30.7891 932.633 33.9062 936.5 33.9062C940.375 33.9062 943.484 30.7891 943.484 26.9141C943.484 26.4453 943.156 26.1094 942.688 26.1094C942.234 26.1094 941.93 26.4453 941.93 26.9141C941.93 29.9297 939.516 32.3516 936.5 32.3516C933.492 32.3516 931.07 29.9297 931.07 26.9141C931.07 23.875 933.469 21.4688 936.477 21.4688C936.984 21.4688 937.453 21.5078 937.867 21.5781L935.734 23.6875C935.594 23.8281 935.516 24 935.516 24.2109C935.516 24.6406 935.852 24.9766 936.273 24.9766Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M1134 33.0156C1134.49 33.0156 1134.89 32.6094 1134.89 32.1484V27.2578H1139.66C1140.13 27.2578 1140.54 26.8594 1140.54 26.3672C1140.54 25.8828 1140.13 25.4766 1139.66 25.4766H1134.89V20.5859C1134.89 20.1172 1134.49 19.7188 1134 19.7188C1133.52 19.7188 1133.11 20.1172 1133.11 20.5859V25.4766H1128.34C1127.88 25.4766 1127.46 25.8828 1127.46 26.3672C1127.46 26.8594 1127.88 27.2578 1128.34 27.2578H1133.11V32.1484C1133.11 32.6094 1133.52 33.0156 1134 33.0156Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M1161.8 31.0703H1163.23V32.375C1163.23 34.0547 1164.12 34.9219 1165.81 34.9219H1174.2C1175.89 34.9219 1176.77 34.0547 1176.77 32.3828V24.0469C1176.77 22.375 1175.89 21.5 1174.2 21.5H1172.77V20.2578C1172.77 18.5859 1171.88 17.7109 1170.19 17.7109H1161.8C1160.1 17.7109 1159.23 18.5781 1159.23 20.2578V28.5234C1159.23 30.1953 1160.1 31.0703 1161.8 31.0703ZM1161.9 29.5078C1161.18 29.5078 1160.78 29.1328 1160.78 28.3828V20.3984C1160.78 19.6406 1161.18 19.2656 1161.9 19.2656H1170.09C1170.8 19.2656 1171.2 19.6406 1171.2 20.3984V21.5H1165.81C1164.12 21.5 1163.23 22.375 1163.23 24.0469V29.5078H1161.9ZM1165.91 33.3672C1165.19 33.3672 1164.8 32.9922 1164.8 32.2422V24.1875C1164.8 23.4297 1165.19 23.0625 1165.91 23.0625H1174.1C1174.81 23.0625 1175.21 23.4297 1175.21 24.1875V32.2422C1175.21 32.9922 1174.81 33.3672 1174.1 33.3672H1165.91Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M1099.51 28.4141C1099.91 28.4141 1100.24 28.0859 1100.24 27.6953V19.8359L1100.18 18.6797L1100.66 19.25L1101.75 20.4141C1101.88 20.5547 1102.06 20.625 1102.24 20.625C1102.6 20.625 1102.9 20.3672 1102.9 20C1102.9 19.8047 1102.82 19.6641 1102.69 19.5312L1100.06 17.0078C1099.88 16.8203 1099.7 16.7578 1099.51 16.7578C1099.32 16.7578 1099.14 16.8203 1098.95 17.0078L1096.33 19.5312C1096.2 19.6641 1096.12 19.8047 1096.12 20C1096.12 20.3672 1096.41 20.625 1096.77 20.625C1096.95 20.625 1097.14 20.5547 1097.27 20.4141L1098.35 19.25L1098.84 18.6719L1098.78 19.8359V27.6953C1098.78 28.0859 1099.11 28.4141 1099.51 28.4141ZM1095 34.6562H1104C1105.7 34.6562 1106.57 33.7812 1106.57 32.1094V24.4297C1106.57 22.7578 1105.7 21.8828 1104 21.8828H1101.89V23.4375H1103.9C1104.61 23.4375 1105.02 23.8125 1105.02 24.5625V31.9688C1105.02 32.7188 1104.61 33.0938 1103.9 33.0938H1095.1C1094.38 33.0938 1093.98 32.7188 1093.98 31.9688V24.5625C1093.98 23.8125 1094.38 23.4375 1095.1 23.4375H1097.13V21.8828H1095C1093.31 21.8828 1092.43 22.75 1092.43 24.4297V32.1094C1092.43 33.7812 1093.31 34.6562 1095 34.6562Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M99.5703 33.6016H112.938C114.633 33.6016 115.516 32.7266 115.516 31.0547V21.5469C115.516 19.875 114.633 19 112.938 19H99.5703C97.8828 19 97 19.8672 97 21.5469V31.0547C97 32.7266 97.8828 33.6016 99.5703 33.6016ZM99.6719 32.0469C98.9531 32.0469 98.5547 31.6719 98.5547 30.9141V21.6875C98.5547 20.9297 98.9531 20.5547 99.6719 20.5547H103.234V32.0469H99.6719ZM112.836 20.5547C113.555 20.5547 113.953 20.9297 113.953 21.6875V30.9141C113.953 31.6719 113.555 32.0469 112.836 32.0469H104.711V20.5547H112.836ZM101.703 23.4141C101.984 23.4141 102.219 23.1719 102.219 22.9062C102.219 22.6406 101.984 22.4062 101.703 22.4062H100.102C99.8203 22.4062 99.5859 22.6406 99.5859 22.9062C99.5859 23.1719 99.8203 23.4141 100.102 23.4141H101.703ZM101.703 25.5156C101.984 25.5156 102.219 25.2812 102.219 25.0078C102.219 24.7422 101.984 24.5078 101.703 24.5078H100.102C99.8203 24.5078 99.5859 24.7422 99.5859 25.0078C99.5859 25.2812 99.8203 25.5156 100.102 25.5156H101.703ZM101.703 27.6094C101.984 27.6094 102.219 27.3828 102.219 27.1094C102.219 26.8438 101.984 26.6172 101.703 26.6172H100.102C99.8203 26.6172 99.5859 26.8438 99.5859 27.1094C99.5859 27.3828 99.8203 27.6094 100.102 27.6094H101.703Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M143.914 32.5938C144.094 32.7656 144.312 32.8594 144.562 32.8594C145.086 32.8594 145.492 32.4531 145.492 31.9375C145.492 31.6797 145.391 31.4453 145.211 31.2656L139.742 25.9219L145.211 20.5938C145.391 20.4141 145.492 20.1719 145.492 19.9219C145.492 19.4062 145.086 19 144.562 19C144.312 19 144.094 19.0938 143.922 19.2656L137.844 25.2031C137.625 25.4062 137.516 25.6562 137.516 25.9297C137.516 26.2031 137.625 26.4375 137.836 26.6484L143.914 32.5938Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<g className="mix-blend-luminosity">
|
||||
<path
|
||||
d="M168.422 32.8594C168.68 32.8594 168.891 32.7656 169.07 32.5938L175.148 26.6562C175.359 26.4375 175.469 26.2109 175.469 25.9297C175.469 25.6562 175.367 25.4141 175.148 25.2109L169.07 19.2656C168.891 19.0938 168.68 19 168.422 19C167.898 19 167.492 19.4062 167.492 19.9219C167.492 20.1719 167.602 20.4141 167.773 20.5938L173.25 25.9375L167.773 31.2656C167.594 31.4531 167.492 31.6797 167.492 31.9375C167.492 32.4531 167.898 32.8594 168.422 32.8594Z"
|
||||
fill="#A3A3A3"
|
||||
/>
|
||||
</g>
|
||||
<image
|
||||
href={src}
|
||||
width="1200"
|
||||
height="700"
|
||||
x="1"
|
||||
y="52"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
clipPath="url(#roundedBottom)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="path0">
|
||||
<rect width={width} height={height} fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="roundedBottom">
|
||||
<path
|
||||
d="M1 52H1201V741C1201 747.075 1196.08 752 1190 752H12C5.92486 752 1 747.075 1 741V52Z"
|
||||
fill="white"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
45
src/components/section.tsx
Normal file
45
src/components/section.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
interface SectionProps {
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Section({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
}: SectionProps) {
|
||||
const sectionId = title ? title.toLowerCase().replace(/\s+/g, "-") : id;
|
||||
return (
|
||||
<section id={id || sectionId}>
|
||||
<div className={className}>
|
||||
<div className="relative container mx-auto px-4 py-16 max-w-7xl">
|
||||
<div className="text-center space-y-4 pb-6 mx-auto">
|
||||
{title && (
|
||||
<h2 className="text-sm text-primary font-mono font-medium tracking-wider uppercase">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && (
|
||||
<h3 className="mx-auto mt-4 max-w-xs text-3xl font-semibold sm:max-w-none sm:text-4xl md:text-5xl">
|
||||
{subtitle}
|
||||
</h3>
|
||||
)}
|
||||
{description && (
|
||||
<p className="mt-6 text-lg leading-8 text-slate-600 max-w-2xl mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
21
src/components/sections/blog.tsx
Normal file
21
src/components/sections/blog.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import BlogCard from "@/components/blog-card";
|
||||
import Section from "@/components/section";
|
||||
import { getBlogPosts } from "@/lib/blog";
|
||||
|
||||
export default async function BlogSection() {
|
||||
const allPosts = await getBlogPosts();
|
||||
|
||||
const articles = await Promise.all(
|
||||
allPosts.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
|
||||
);
|
||||
|
||||
return (
|
||||
<Section title="Blog" subtitle="Latest Articles">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{articles.map((data, idx) => (
|
||||
<BlogCard key={data.slug} data={data} priority={idx <= 1} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
29
src/components/sections/cta.tsx
Normal file
29
src/components/sections/cta.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import Section from "@/components/section";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function CtaSection() {
|
||||
return (
|
||||
<Section
|
||||
id="cta"
|
||||
title="Ready to get started?"
|
||||
subtitle="Start your free trial today."
|
||||
className="bg-primary/10 rounded-xl py-16"
|
||||
>
|
||||
<div className="flex flex-col w-full sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4 pt-4">
|
||||
<Link
|
||||
href="/signup"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
Get started for free
|
||||
</Link>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
41
src/components/sections/faq.tsx
Normal file
41
src/components/sections/faq.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Section from "@/components/section";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<Section title="FAQ" subtitle="Frequently asked questions">
|
||||
<div className="mx-auto my-12 md:max-w-[800px]">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="flex w-full flex-col items-center justify-center space-y-2"
|
||||
>
|
||||
{siteConfig.faqs.map((faq, idx) => (
|
||||
<AccordionItem
|
||||
key={idx}
|
||||
value={faq.question}
|
||||
className="w-full border rounded-lg overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="px-4">
|
||||
{faq.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">{faq.answer}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
<h4 className="mb-12 text-center text-sm font-medium tracking-tight text-foreground/80">
|
||||
Still have questions? Email us at{" "}
|
||||
<a href={`mailto:${siteConfig.links.email}`} className="underline">
|
||||
{siteConfig.links.email}
|
||||
</a>
|
||||
</h4>
|
||||
</Section>
|
||||
);
|
||||
}
|
42
src/components/sections/features.tsx
Normal file
42
src/components/sections/features.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import Features from "@/components/features-horizontal";
|
||||
import Section from "@/components/section";
|
||||
import { BarChart3, Brain, FileText, LineChart } from "lucide-react";
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
title: "AI-Powered Dashboard",
|
||||
content: "Visualize trends and gain insights at a glance.",
|
||||
image: "/dashboard.png",
|
||||
icon: <BarChart3 className="h-6 w-6 text-primary" />,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Natural Language Processing",
|
||||
content: "Analyze text and extract sentiment effortlessly.",
|
||||
image: "/dashboard.png",
|
||||
icon: <Brain className="h-6 w-6 text-primary" />,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Predictive Analytics",
|
||||
content: "Forecast trends and make data-driven decisions.",
|
||||
image: "/dashboard.png",
|
||||
icon: <LineChart className="h-6 w-6 text-primary" />,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Automated Reporting",
|
||||
content: "Generate comprehensive reports with one click.",
|
||||
image: "/dashboard.png",
|
||||
icon: <FileText className="h-6 w-6 text-primary" />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Section title="Features" subtitle="User Flows and Navigational Structures">
|
||||
<Features collapseDelay={5000} linePosition="bottom" data={data} />
|
||||
</Section>
|
||||
);
|
||||
}
|
64
src/components/sections/footer.tsx
Normal file
64
src/components/sections/footer.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer>
|
||||
<div className="max-w-6xl mx-auto py-16 sm:px-10 px-5 pb-0">
|
||||
<a
|
||||
href="/"
|
||||
title={siteConfig.name}
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="w-auto h-[40px]" />
|
||||
<span className="font-bold text-xl">{siteConfig.name}</span>
|
||||
</a>
|
||||
|
||||
<div className="grid md:grid-cols-3 lg:grid-cols-4 sm:grid-cols-2 mt-8">
|
||||
{siteConfig.footer.map((section, index) => (
|
||||
<div key={index} className="mb-5">
|
||||
<h2 className="font-semibold">{section.title}</h2>
|
||||
<ul>
|
||||
{section.links.map((link, linkIndex) => (
|
||||
<li key={linkIndex} className="my-2">
|
||||
<Link
|
||||
href={link.href}
|
||||
className="group inline-flex cursor-pointer items-center justify-start gap-1 text-muted-foreground duration-200 hover:text-foreground hover:opacity-90"
|
||||
>
|
||||
{link.icon && link.icon}
|
||||
{link.text}
|
||||
<ChevronRight className="h-4 w-4 translate-x-0 transform opacity-0 transition-all duration-300 ease-out group-hover:translate-x-1 group-hover:opacity-100" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="max-w-6xl mx-auto border-t py-2 grid md:grid-cols-2 h-full justify-between w-full grid-cols-1 gap-1">
|
||||
<span className="text-sm tracking-tight text-foreground">
|
||||
Copyright © {new Date().getFullYear()}{" "}
|
||||
<Link href="/" className="cursor-pointer">
|
||||
{siteConfig.name}
|
||||
</Link>{" "}
|
||||
- {siteConfig.description}
|
||||
</span>
|
||||
<ul className="flex justify-start md:justify-end text-sm tracking-tight text-foreground">
|
||||
<li className="mr-3 md:mx-4">
|
||||
<Link href="#" target="_blank" rel="noopener noreferrer">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mr-3 md:mx-4">
|
||||
<Link href="#" target="_blank" rel="noopener noreferrer">
|
||||
Terms of Service
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
85
src/components/sections/header.tsx
Normal file
85
src/components/sections/header.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import Drawer from "@/components/drawer";
|
||||
import { Icons } from "@/components/icons";
|
||||
import Menu from "@/components/menu";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Header() {
|
||||
const [addBorder, setAddBorder] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 20) {
|
||||
setAddBorder(true);
|
||||
} else {
|
||||
setAddBorder(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={
|
||||
"relative sticky top-0 z-50 py-2 bg-background/60 backdrop-blur"
|
||||
}
|
||||
>
|
||||
<div className="flex justify-between items-center container">
|
||||
<Link
|
||||
href="/"
|
||||
title="brand-logo"
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="w-auto h-[40px]" />
|
||||
<span className="font-bold text-xl">{siteConfig.name}</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<div className="flex items-center ">
|
||||
<nav className="mr-10">
|
||||
<Menu />
|
||||
</nav>
|
||||
|
||||
<div className="gap-2 flex">
|
||||
<Link
|
||||
href="/login"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
href="/signup"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
Get Started for Free
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 cursor-pointer block lg:hidden">
|
||||
<Drawer />
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
className={cn(
|
||||
"absolute w-full bottom-0 transition-opacity duration-300 ease-in-out",
|
||||
addBorder ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
}
|
153
src/components/sections/hero.tsx
Normal file
153
src/components/sections/hero.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import HeroVideoDialog from "@/components/magicui/hero-video";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1];
|
||||
|
||||
function HeroPill() {
|
||||
return (
|
||||
<motion.a
|
||||
href="/blog/introducing-acme-ai"
|
||||
className="flex w-auto items-center space-x-2 rounded-full bg-primary/20 px-2 py-1 ring-1 ring-accent whitespace-pre"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease }}
|
||||
>
|
||||
<div className="w-fit rounded-full bg-accent px-2 py-0.5 text-center text-xs font-medium text-primary sm:text-sm">
|
||||
📣 Announcement
|
||||
</div>
|
||||
<p className="text-xs font-medium text-primary sm:text-sm">
|
||||
Introducing Acme.ai
|
||||
</p>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
className="ml-1"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.78141 5.33312L5.20541 1.75712L6.14808 0.814453L11.3334 5.99979L6.14808 11.1851L5.20541 10.2425L8.78141 6.66645H0.666748V5.33312H8.78141Z"
|
||||
fill="hsl(var(--primary))"
|
||||
/>
|
||||
</svg>
|
||||
</motion.a>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroTitles() {
|
||||
return (
|
||||
<div className="flex w-full max-w-2xl flex-col space-y-4 overflow-hidden pt-8">
|
||||
<motion.h1
|
||||
className="text-center text-4xl font-medium leading-tight text-foreground sm:text-5xl md:text-6xl"
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease,
|
||||
staggerChildren: 0.2,
|
||||
}}
|
||||
>
|
||||
{["Automate", "your", "workflow", "with AI"].map((text, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
className="inline-block px-1 md:px-2 text-balance font-semibold"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: index * 0.2,
|
||||
ease,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="mx-auto max-w-xl text-center text-lg leading-7 text-muted-foreground sm:text-xl sm:leading-9 text-balance"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.6,
|
||||
duration: 0.8,
|
||||
ease,
|
||||
}}
|
||||
>
|
||||
No matter what problem you have, our AI can help you solve it.
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroCTA() {
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="mx-auto mt-6 flex w-full max-w-2xl flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8, duration: 0.8, ease }}
|
||||
>
|
||||
<Link
|
||||
href="/signup"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
Get started for free
|
||||
</Link>
|
||||
</motion.div>
|
||||
<motion.p
|
||||
className="mt-5 text-sm text-muted-foreground"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.0, duration: 0.8 }}
|
||||
>
|
||||
7 day free trial. No credit card required.
|
||||
</motion.p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroImage() {
|
||||
return (
|
||||
<motion.div
|
||||
className="relative mx-auto flex w-full items-center justify-center"
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.2, duration: 1, ease }}
|
||||
>
|
||||
<HeroVideoDialog
|
||||
animationStyle="from-center"
|
||||
videoSrc="https://www.youtube.com/embed/qh3NGpYRG3I?si=4rb-zSdDkVK9qxxb"
|
||||
thumbnailSrc="/dashboard.png"
|
||||
thumbnailAlt="Hero Video"
|
||||
className="border rounded-lg shadow-lg max-w-screen-lg mt-16"
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Hero2() {
|
||||
return (
|
||||
<section id="hero">
|
||||
<div className="relative flex w-full flex-col items-center justify-start px-4 pt-32 sm:px-6 sm:pt-24 md:pt-32 lg:px-8">
|
||||
<HeroPill />
|
||||
<HeroTitles />
|
||||
<HeroCTA />
|
||||
<HeroImage />
|
||||
<div className="pointer-events-none absolute inset-x-0 -bottom-12 h-1/3 bg-gradient-to-t from-background via-background to-transparent lg:h-1/4"></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
38
src/components/sections/how-it-works.tsx
Normal file
38
src/components/sections/how-it-works.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Features from "@/components/features-vertical";
|
||||
import Section from "@/components/section";
|
||||
import { Sparkles, Upload, Zap } from "lucide-react";
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
title: "1. Upload Your Data",
|
||||
content:
|
||||
"Simply upload your data to our secure platform. We support various file formats and data types to ensure a seamless integration with your existing systems.",
|
||||
image: "/dashboard.png",
|
||||
icon: <Upload className="w-6 h-6 text-primary" />,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "2. Click Start",
|
||||
content:
|
||||
"Our advanced AI algorithms automatically process and analyze your data, extracting valuable insights and patterns that would be difficult to identify manually.",
|
||||
image: "/dashboard.png",
|
||||
icon: <Zap className="w-6 h-6 text-primary" />,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "3. Get Actionable Insights",
|
||||
content:
|
||||
"Receive clear, actionable insights and recommendations based on the AI analysis. Use these insights to make data-driven decisions and improve your business strategies.",
|
||||
image: "/dashboard.png",
|
||||
icon: <Sparkles className="w-6 h-6 text-primary" />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Section title="How it works" subtitle="Just 3 steps to get started">
|
||||
<Features data={data} />
|
||||
</Section>
|
||||
);
|
||||
}
|
41
src/components/sections/logos.tsx
Normal file
41
src/components/sections/logos.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Marquee from "@/components/magicui/marquee";
|
||||
import Image from "next/image";
|
||||
|
||||
const companies = [
|
||||
"Google",
|
||||
"Microsoft",
|
||||
"Amazon",
|
||||
"Netflix",
|
||||
"YouTube",
|
||||
"Instagram",
|
||||
"Uber",
|
||||
"Spotify",
|
||||
];
|
||||
|
||||
export default function Logos() {
|
||||
return (
|
||||
<section id="logos">
|
||||
<div className="container mx-auto px-4 md:px-8 py-12">
|
||||
<h3 className="text-center text-sm font-semibold text-gray-500">
|
||||
TRUSTED BY LEADING TEAMS
|
||||
</h3>
|
||||
<div className="relative mt-6">
|
||||
<Marquee className="max-w-full [--duration:40s]">
|
||||
{companies.map((logo, idx) => (
|
||||
<Image
|
||||
key={idx}
|
||||
width={112}
|
||||
height={40}
|
||||
src={`https://cdn.magicui.design/companies/${logo}.svg`}
|
||||
className="h-10 w-28 dark:brightness-0 dark:invert grayscale opacity-30"
|
||||
alt={logo}
|
||||
/>
|
||||
))}
|
||||
</Marquee>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 h-full w-1/3 bg-gradient-to-r from-background"></div>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 h-full w-1/3 bg-gradient-to-l from-background"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
139
src/components/sections/pricing.tsx
Normal file
139
src/components/sections/pricing.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import Section from "@/components/section";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import useWindowSize from "@/lib/hooks/use-window-size";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { Check } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { FaStar } from "react-icons/fa";
|
||||
|
||||
export default function PricingSection() {
|
||||
const [isMonthly, setIsMonthly] = useState(true);
|
||||
const { isDesktop } = useWindowSize();
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsMonthly(!isMonthly);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section title="Pricing" subtitle="Choose the plan that's right for you">
|
||||
<div className="flex justify-center mb-10">
|
||||
<span className="mr-2 font-semibold">Monthly</span>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<Label>
|
||||
<Switch checked={!isMonthly} onCheckedChange={handleToggle} />
|
||||
</Label>
|
||||
</label>
|
||||
<span className="ml-2 font-semibold">Yearly</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4">
|
||||
{siteConfig.pricing.map((plan, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ y: 50, opacity: 1 }}
|
||||
whileInView={
|
||||
isDesktop
|
||||
? {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
x:
|
||||
index === siteConfig.pricing.length - 1
|
||||
? -30
|
||||
: index === 0
|
||||
? 30
|
||||
: 0,
|
||||
scale:
|
||||
index === 0 || index === siteConfig.pricing.length - 1
|
||||
? 0.94
|
||||
: 1.0,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
duration: 1.6,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
delay: 0.4,
|
||||
opacity: { duration: 0.5 },
|
||||
}}
|
||||
className={cn(
|
||||
`rounded-2xl border-[1px] p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`,
|
||||
plan.isPopular ? "border-primary border-[2px]" : "border-border",
|
||||
index === 0 || index === siteConfig.pricing.length - 1
|
||||
? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]"
|
||||
: "z-10",
|
||||
index === 0 && "origin-right",
|
||||
index === siteConfig.pricing.length - 1 && "origin-left"
|
||||
)}
|
||||
>
|
||||
{plan.isPopular && (
|
||||
<div className="absolute top-0 right-0 bg-primary py-0.5 px-2 rounded-bl-xl rounded-tr-xl flex items-center">
|
||||
<FaStar className="text-white" />
|
||||
<span className="text-white ml-1 font-sans font-semibold">
|
||||
Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-base font-semibold text-muted-foreground">
|
||||
{plan.name}
|
||||
</p>
|
||||
<p className="mt-6 flex items-center justify-center gap-x-2">
|
||||
<span className="text-5xl font-bold tracking-tight text-foreground">
|
||||
{isMonthly ? plan.price : plan.yearlyPrice}
|
||||
</span>
|
||||
{plan.period !== "Next 3 months" && (
|
||||
<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
|
||||
/ {plan.period}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{isMonthly ? "billed monthly" : "billed annually"}
|
||||
</p>
|
||||
|
||||
<ul className="mt-5 gap-2 flex flex-col">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<Check className="mr-2 h-4 w-4 text-primary" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<hr className="w-full my-4" />
|
||||
|
||||
<Link
|
||||
href={plan.href}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
}),
|
||||
"group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter",
|
||||
"transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-white",
|
||||
plan.isPopular
|
||||
? "bg-primary text-white"
|
||||
: "bg-white text-black"
|
||||
)}
|
||||
>
|
||||
{plan.buttonText}
|
||||
</Link>
|
||||
<p className="mt-6 text-xs leading-5 text-muted-foreground">
|
||||
{plan.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
50
src/components/sections/problem.tsx
Normal file
50
src/components/sections/problem.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import BlurFade from "@/components/magicui/blur-fade";
|
||||
import Section from "@/components/section";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Brain, Shield, Zap } from "lucide-react";
|
||||
|
||||
const problems = [
|
||||
{
|
||||
title: "Data Overload",
|
||||
description:
|
||||
"Businesses struggle to make sense of vast amounts of complex data, missing out on valuable insights that could drive growth and innovation.",
|
||||
icon: Brain,
|
||||
},
|
||||
{
|
||||
title: "Slow Decision-Making",
|
||||
description:
|
||||
"Traditional data processing methods are too slow, causing businesses to lag behind market changes and miss crucial opportunities.",
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
title: "Data Security Concerns",
|
||||
description:
|
||||
"With increasing cyber threats, businesses worry about the safety of their sensitive information when adopting new technologies.",
|
||||
icon: Shield,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Section
|
||||
title="Problem"
|
||||
subtitle="Manually entering your data is a hassle."
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-12">
|
||||
{problems.map((problem, index) => (
|
||||
<BlurFade key={index} delay={0.2 + index * 0.2} inView>
|
||||
<Card className="bg-background border-none shadow-none">
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<problem.icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{problem.title}</h3>
|
||||
<p className="text-muted-foreground">{problem.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
125
src/components/sections/solution.tsx
Normal file
125
src/components/sections/solution.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import FlickeringGrid from "@/components/magicui/flickering-grid";
|
||||
import Ripple from "@/components/magicui/ripple";
|
||||
import Safari from "@/components/safari";
|
||||
import Section from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Advanced AI Algorithms",
|
||||
description:
|
||||
"Our platform utilizes cutting-edge AI algorithms to provide accurate and efficient solutions for your business needs.",
|
||||
className: "hover:bg-red-500/10 transition-all duration-500 ease-out",
|
||||
content: (
|
||||
<>
|
||||
<Safari
|
||||
src={`/dashboard.png`}
|
||||
url="https://acme.ai"
|
||||
className="-mb-32 mt-4 max-h-64 w-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-y-[-10px] transition-all duration-300"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Secure Data Handling",
|
||||
description:
|
||||
"We prioritize your data security with state-of-the-art encryption and strict privacy protocols, ensuring your information remains confidential.",
|
||||
className:
|
||||
"order-3 xl:order-none hover:bg-blue-500/10 transition-all duration-500 ease-out",
|
||||
content: (
|
||||
<Safari
|
||||
src={`/dashboard.png`}
|
||||
url="https://acme.ai"
|
||||
className="-mb-32 mt-4 max-h-64 w-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-y-[-10px] transition-all duration-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Seamless Integration",
|
||||
description:
|
||||
"Easily integrate our AI solutions into your existing workflows and systems for a smooth and efficient operation.",
|
||||
className:
|
||||
"md:row-span-2 hover:bg-orange-500/10 transition-all duration-500 ease-out",
|
||||
content: (
|
||||
<>
|
||||
<FlickeringGrid
|
||||
className="z-0 absolute inset-0 [mask:radial-gradient(circle_at_center,#fff_400px,transparent_0)]"
|
||||
squareSize={4}
|
||||
gridGap={6}
|
||||
color="#000"
|
||||
maxOpacity={0.1}
|
||||
flickerChance={0.1}
|
||||
height={800}
|
||||
width={800}
|
||||
/>
|
||||
<Safari
|
||||
src={`/dashboard.png`}
|
||||
url="https://acme.ai"
|
||||
className="-mb-48 ml-12 mt-16 h-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-x-[-10px] transition-all duration-300"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Customizable Solutions",
|
||||
description:
|
||||
"Tailor our AI services to your specific needs with flexible customization options, allowing you to get the most out of our platform.",
|
||||
className:
|
||||
"flex-row order-4 md:col-span-2 md:flex-row xl:order-none hover:bg-green-500/10 transition-all duration-500 ease-out",
|
||||
content: (
|
||||
<>
|
||||
<Ripple className="absolute -bottom-full" />
|
||||
<Safari
|
||||
src={`/dashboard.png`}
|
||||
url="https://acme.ai"
|
||||
className="-mb-32 mt-4 max-h-64 w-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-y-[-10px] transition-all duration-300"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Section
|
||||
title="Solution"
|
||||
subtitle="Empower Your Business with AI Workflows"
|
||||
description="Generic AI tools won't suffice. Our platform is purpose-built to provide exceptional AI-driven solutions for your unique business needs."
|
||||
className="bg-neutral-100 dark:bg-neutral-900"
|
||||
>
|
||||
<div className="mx-auto mt-16 grid max-w-sm grid-cols-1 gap-6 text-gray-500 md:max-w-3xl md:grid-cols-2 xl:grid-rows-2 md:grid-rows-3 xl:max-w-6xl xl:auto-rows-fr xl:grid-cols-3">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className={cn(
|
||||
"group relative items-start overflow-hidden bg-neutral-50 dark:bg-neutral-800 p-6 rounded-2xl",
|
||||
feature.className
|
||||
)}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
delay: index * 0.1,
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2 text-primary">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-foreground">{feature.description}</p>
|
||||
</div>
|
||||
{feature.content}
|
||||
<div className="absolute bottom-0 left-0 h-32 w-full bg-gradient-to-t from-neutral-50 dark:from-neutral-900 pointer-events-none"></div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
91
src/components/sections/testimonials-carousel.tsx
Normal file
91
src/components/sections/testimonials-carousel.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import BlurFade from "@/components/magicui/blur-fade";
|
||||
import Section from "@/components/section";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@/components/ui/carousel";
|
||||
import Image from "next/image";
|
||||
import { MdOutlineFormatQuote } from "react-icons/md";
|
||||
|
||||
const companies = [
|
||||
"Google",
|
||||
"Microsoft",
|
||||
"Amazon",
|
||||
"Netflix",
|
||||
"YouTube",
|
||||
"Instagram",
|
||||
"Uber",
|
||||
"Spotify",
|
||||
];
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Section
|
||||
title="Testimonial Highlight"
|
||||
subtitle="What our customers are saying"
|
||||
>
|
||||
<Carousel>
|
||||
<div className="max-w-2xl mx-auto relative">
|
||||
<CarouselContent>
|
||||
{Array.from({ length: 7 }).map((_, index) => (
|
||||
<CarouselItem key={index}>
|
||||
<div className="p-2 pb-5">
|
||||
<div className="text-center">
|
||||
<MdOutlineFormatQuote className="text-4xl text-themeDarkGray my-4 mx-auto" />
|
||||
<BlurFade delay={0.25} inView>
|
||||
<h4 className="text-1xl font-semibold max-w-lg mx-auto px-10">
|
||||
There is a lot of exciting stuff going on in the stars
|
||||
above us that make astronomy so much fun. The truth is
|
||||
the universe is a constantly changing, moving, some
|
||||
would say “living” thing because you just never know
|
||||
what you are going to see on any given night of
|
||||
stargazing.
|
||||
</h4>
|
||||
</BlurFade>
|
||||
<BlurFade delay={0.25 * 2} inView>
|
||||
<div className="mt-8">
|
||||
<Image
|
||||
width={0}
|
||||
height={40}
|
||||
key={index}
|
||||
src={`https://cdn.magicui.design/companies/${
|
||||
companies[index % companies.length]
|
||||
}.svg`}
|
||||
alt={`${companies[index % companies.length]} Logo`}
|
||||
className="mx-auto w-auto h-[40px] grayscale opacity-30"
|
||||
/>
|
||||
</div>
|
||||
</BlurFade>
|
||||
<div className="">
|
||||
<BlurFade delay={0.25 * 3} inView>
|
||||
<h4 className="text-1xl font-semibold my-2">
|
||||
Leslie Alexander
|
||||
</h4>
|
||||
</BlurFade>
|
||||
</div>
|
||||
<BlurFade delay={0.25 * 4} inView>
|
||||
<div className=" mb-3">
|
||||
<span className="text-sm text-themeDarkGray">
|
||||
UI Designer
|
||||
</span>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 h-full w-2/12 bg-gradient-to-r from-background"></div>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 h-full w-2/12 bg-gradient-to-l from-background"></div>
|
||||
</div>
|
||||
<div className="md:block hidden">
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</div>
|
||||
</Carousel>
|
||||
</Section>
|
||||
);
|
||||
}
|
313
src/components/sections/testimonials.tsx
Normal file
313
src/components/sections/testimonials.tsx
Normal file
@ -0,0 +1,313 @@
|
||||
"use client";
|
||||
|
||||
import Marquee from "@/components/magicui/marquee";
|
||||
import Section from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { Star } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export const Highlight = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"bg-primary/20 p-1 py-0.5 font-bold text-primary dark:bg-primary/20 dark:text-primary",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TestimonialCardProps {
|
||||
name: string;
|
||||
role: string;
|
||||
img?: string;
|
||||
description: React.ReactNode;
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const TestimonialCard = ({
|
||||
description,
|
||||
name,
|
||||
img,
|
||||
role,
|
||||
className,
|
||||
...props // Capture the rest of the props
|
||||
}: TestimonialCardProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 flex w-full cursor-pointer break-inside-avoid flex-col items-center justify-between gap-6 rounded-xl p-4",
|
||||
// light styles
|
||||
" border border-neutral-200 bg-white",
|
||||
// dark styles
|
||||
"dark:bg-black dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
|
||||
className
|
||||
)}
|
||||
{...props} // Spread the rest of the props here
|
||||
>
|
||||
<div className="select-none text-sm font-normal text-neutral-700 dark:text-neutral-400">
|
||||
{description}
|
||||
<div className="flex flex-row py-1">
|
||||
<Star className="size-4 text-yellow-500 fill-yellow-500" />
|
||||
<Star className="size-4 text-yellow-500 fill-yellow-500" />
|
||||
<Star className="size-4 text-yellow-500 fill-yellow-500" />
|
||||
<Star className="size-4 text-yellow-500 fill-yellow-500" />
|
||||
<Star className="size-4 text-yellow-500 fill-yellow-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full select-none items-center justify-start gap-5">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
src={img || ""}
|
||||
alt={name}
|
||||
className="h-10 w-10 rounded-full ring-1 ring-border ring-offset-4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-medium text-neutral-500">{name}</p>
|
||||
<p className="text-xs font-normal text-neutral-400">{role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: "Alex Rivera",
|
||||
role: "CTO at InnovateTech",
|
||||
img: "https://randomuser.me/api/portraits/men/91.jpg",
|
||||
description: (
|
||||
<p>
|
||||
The AI-driven analytics from #QuantumInsights have revolutionized our
|
||||
product development cycle.
|
||||
<Highlight>
|
||||
Insights are now more accurate and faster than ever.
|
||||
</Highlight>{" "}
|
||||
A game-changer for tech companies.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Samantha Lee",
|
||||
role: "Marketing Director at NextGen Solutions",
|
||||
img: "https://randomuser.me/api/portraits/women/12.jpg",
|
||||
description: (
|
||||
<p>
|
||||
Implementing #AIStream's customer prediction model has drastically
|
||||
improved our targeting strategy.
|
||||
<Highlight>Seeing a 50% increase in conversion rates!</Highlight> Highly
|
||||
recommend their solutions.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Raj Patel",
|
||||
role: "Founder & CEO at StartUp Grid",
|
||||
img: "https://randomuser.me/api/portraits/men/45.jpg",
|
||||
description: (
|
||||
<p>
|
||||
As a startup, we need to move fast and stay ahead. #CodeAI's
|
||||
automated coding assistant helps us do just that.
|
||||
<Highlight>Our development speed has doubled.</Highlight> Essential tool
|
||||
for any startup.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Emily Chen",
|
||||
role: "Product Manager at Digital Wave",
|
||||
img: "https://randomuser.me/api/portraits/women/83.jpg",
|
||||
description: (
|
||||
<p>
|
||||
#VoiceGen's AI-driven voice synthesis has made creating global
|
||||
products a breeze.
|
||||
<Highlight>Localization is now seamless and efficient.</Highlight> A
|
||||
must-have for global product teams.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Michael Brown",
|
||||
role: "Data Scientist at FinTech Innovations",
|
||||
img: "https://randomuser.me/api/portraits/men/1.jpg",
|
||||
description: (
|
||||
<p>
|
||||
Leveraging #DataCrunch's AI for our financial models has given us
|
||||
an edge in predictive accuracy.
|
||||
<Highlight>
|
||||
Our investment strategies are now powered by real-time data analytics.
|
||||
</Highlight>{" "}
|
||||
Transformative for the finance industry.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Linda Wu",
|
||||
role: "VP of Operations at LogiChain Solutions",
|
||||
img: "https://randomuser.me/api/portraits/women/5.jpg",
|
||||
description: (
|
||||
<p>
|
||||
#LogiTech's supply chain optimization tools have drastically
|
||||
reduced our operational costs.
|
||||
<Highlight>
|
||||
Efficiency and accuracy in logistics have never been better.
|
||||
</Highlight>{" "}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Carlos Gomez",
|
||||
role: "Head of R&D at EcoInnovate",
|
||||
img: "https://randomuser.me/api/portraits/men/14.jpg",
|
||||
description: (
|
||||
<p>
|
||||
By integrating #GreenTech's sustainable energy solutions,
|
||||
we've seen a significant reduction in carbon footprint.
|
||||
<Highlight>
|
||||
Leading the way in eco-friendly business practices.
|
||||
</Highlight>{" "}
|
||||
Pioneering change in the industry.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Aisha Khan",
|
||||
role: "Chief Marketing Officer at Fashion Forward",
|
||||
img: "https://randomuser.me/api/portraits/women/56.jpg",
|
||||
description: (
|
||||
<p>
|
||||
#TrendSetter's market analysis AI has transformed how we approach
|
||||
fashion trends.
|
||||
<Highlight>
|
||||
Our campaigns are now data-driven with higher customer engagement.
|
||||
</Highlight>{" "}
|
||||
Revolutionizing fashion marketing.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Tom Chen",
|
||||
role: "Director of IT at HealthTech Solutions",
|
||||
img: "https://randomuser.me/api/portraits/men/18.jpg",
|
||||
description: (
|
||||
<p>
|
||||
Implementing #MediCareAI in our patient care systems has improved
|
||||
patient outcomes significantly.
|
||||
<Highlight>
|
||||
Technology and healthcare working hand in hand for better health.
|
||||
</Highlight>{" "}
|
||||
A milestone in medical technology.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Sofia Patel",
|
||||
role: "CEO at EduTech Innovations",
|
||||
img: "https://randomuser.me/api/portraits/women/73.jpg",
|
||||
description: (
|
||||
<p>
|
||||
#LearnSmart's AI-driven personalized learning plans have doubled
|
||||
student performance metrics.
|
||||
<Highlight>
|
||||
Education tailored to every learner's needs.
|
||||
</Highlight>{" "}
|
||||
Transforming the educational landscape.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Jake Morrison",
|
||||
role: "CTO at SecureNet Tech",
|
||||
img: "https://randomuser.me/api/portraits/men/25.jpg",
|
||||
description: (
|
||||
<p>
|
||||
With #CyberShield's AI-powered security systems, our data
|
||||
protection levels are unmatched.
|
||||
<Highlight>Ensuring safety and trust in digital spaces.</Highlight>{" "}
|
||||
Redefining cybersecurity standards.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Nadia Ali",
|
||||
role: "Product Manager at Creative Solutions",
|
||||
img: "https://randomuser.me/api/portraits/women/78.jpg",
|
||||
description: (
|
||||
<p>
|
||||
#DesignPro's AI has streamlined our creative process, enhancing
|
||||
productivity and innovation.
|
||||
<Highlight>Bringing creativity and technology together.</Highlight> A
|
||||
game-changer for creative industries.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Omar Farooq",
|
||||
role: "Founder at Startup Hub",
|
||||
img: "https://randomuser.me/api/portraits/men/54.jpg",
|
||||
description: (
|
||||
<p>
|
||||
#VentureAI's insights into startup ecosystems have been invaluable
|
||||
for our growth and funding strategies.
|
||||
<Highlight>Empowering startups with data-driven decisions.</Highlight> A
|
||||
catalyst for startup success.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function Testimonials() {
|
||||
return (
|
||||
<Section
|
||||
title="Testimonials"
|
||||
subtitle="What our customers are saying"
|
||||
className="max-w-8xl"
|
||||
>
|
||||
<div className="relative mt-6 max-h-screen overflow-hidden">
|
||||
<div className="gap-4 md:columns-2 xl:columns-3 2xl:columns-4">
|
||||
{Array(Math.ceil(testimonials.length / 3))
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<Marquee
|
||||
vertical
|
||||
key={i}
|
||||
className={cn({
|
||||
"[--duration:60s]": i === 1,
|
||||
"[--duration:30s]": i === 2,
|
||||
"[--duration:70s]": i === 3,
|
||||
})}
|
||||
>
|
||||
{testimonials.slice(i * 3, (i + 1) * 3).map((card, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
delay: Math.random() * 0.8,
|
||||
duration: 1.2,
|
||||
}}
|
||||
>
|
||||
<TestimonialCard {...card} />
|
||||
</motion.div>
|
||||
))}
|
||||
</Marquee>
|
||||
))}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/4 w-full bg-gradient-to-t from-background from-20%"></div>
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-1/4 w-full bg-gradient-to-b from-background from-20%"></div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
14
src/components/tailwind-indicator.tsx
Normal file
14
src/components/tailwind-indicator.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
export function TailwindIndicator() {
|
||||
// Don't show in production
|
||||
if (process.env.NODE_ENV === "production") return null;
|
||||
return (
|
||||
<div className="fixed bottom-12 left-3 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
|
||||
<div className="block sm:hidden">xs</div>
|
||||
<div className="hidden sm:block md:hidden">sm</div>
|
||||
<div className="hidden md:block lg:hidden">md</div>
|
||||
<div className="hidden lg:block xl:hidden">lg</div>
|
||||
<div className="hidden xl:block 2xl:hidden">xl</div>
|
||||
<div className="hidden 2xl:block">2xl</div>
|
||||
</div>
|
||||
);
|
||||
}
|
8
src/components/theme-provider.tsx
Normal file
8
src/components/theme-provider.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
23
src/components/theme-toggle.tsx
Normal file
23
src/components/theme-toggle.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
// Don't show in production
|
||||
if (process.env.NODE_ENV === "production") return null;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="fixed bottom-1 left-1 z-50"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
>
|
||||
<Sun className="h-[1.5rem] w-[1.3rem] dark:hidden" />
|
||||
<Moon className="hidden h-5 w-5 dark:block" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
58
src/components/ui/accordion.tsx
Normal file
58
src/components/ui/accordion.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
59
src/components/ui/button.tsx
Normal file
59
src/components/ui/button.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
|
||||
orange:
|
||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
262
src/components/ui/carousel.tsx
Normal file
262
src/components/ui/carousel.tsx
Normal file
@ -0,0 +1,262 @@
|
||||
"use client";
|
||||
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const Carousel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
Carousel.displayName = "Carousel";
|
||||
|
||||
const CarouselContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} className="overflow-hidden">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CarouselContent.displayName = "CarouselContent";
|
||||
|
||||
const CarouselItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CarouselItem.displayName = "CarouselItem";
|
||||
|
||||
const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "left-1/2 -translate-x-16 bottom-0 translate-y-4"
|
||||
: "-top-12 right-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselPrevious.displayName = "CarouselPrevious";
|
||||
|
||||
const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-8 w-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "right-1/2 bottom-0 translate-y-4 translate-x-16"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CarouselNext.displayName = "CarouselNext";
|
||||
|
||||
export {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
type CarouselApi,
|
||||
};
|
365
src/components/ui/chart.tsx
Normal file
365
src/components/ui/chart.tsx
Normal file
@ -0,0 +1,365 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([_, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
118
src/components/ui/drawer.tsx
Normal file
118
src/components/ui/drawer.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
128
src/components/ui/navigation-menu.tsx
Normal file
128
src/components/ui/navigation-menu.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:bg-primary/10 hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-primary/10 data-[state=open]:bg-primary/10"
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
};
|
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
96
src/lib/blog.ts
Normal file
96
src/lib/blog.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { unified } from "unified";
|
||||
|
||||
export type Post = {
|
||||
title: string;
|
||||
publishedAt: string;
|
||||
summary: string;
|
||||
author: string;
|
||||
slug: string;
|
||||
image?: string;
|
||||
};
|
||||
|
||||
function parseFrontmatter(fileContent: string) {
|
||||
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
|
||||
let match = frontmatterRegex.exec(fileContent);
|
||||
let frontMatterBlock = match![1];
|
||||
let content = fileContent.replace(frontmatterRegex, "").trim();
|
||||
let frontMatterLines = frontMatterBlock.trim().split("\n");
|
||||
let metadata: Partial<Post> = {};
|
||||
|
||||
frontMatterLines.forEach((line) => {
|
||||
let [key, ...valueArr] = line.split(": ");
|
||||
let value = valueArr.join(": ").trim();
|
||||
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
|
||||
metadata[key.trim() as keyof Post] = value;
|
||||
});
|
||||
|
||||
return { data: metadata as Post, content };
|
||||
}
|
||||
|
||||
function getMDXFiles(dir: string) {
|
||||
return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
|
||||
}
|
||||
|
||||
export async function markdownToHTML(markdown: string) {
|
||||
const p = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(rehypePrettyCode, {
|
||||
// https://rehype-pretty.pages.dev/#usage
|
||||
theme: {
|
||||
light: "min-light",
|
||||
dark: "min-dark",
|
||||
},
|
||||
keepBackground: false,
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
.process(markdown);
|
||||
|
||||
return p.toString();
|
||||
}
|
||||
|
||||
export async function getPost(slug: string) {
|
||||
const filePath = path.join("content", `${slug}.mdx`);
|
||||
const source = fs.readFileSync(filePath, "utf-8");
|
||||
const { content: rawContent, data: metadata } = parseFrontmatter(source);
|
||||
const content = await markdownToHTML(rawContent);
|
||||
const defaultImage = `${siteConfig.url}/og?title=${encodeURIComponent(
|
||||
metadata.title
|
||||
)}`;
|
||||
return {
|
||||
source: content,
|
||||
metadata: {
|
||||
...metadata,
|
||||
image: metadata.image || defaultImage,
|
||||
},
|
||||
slug,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllPosts(dir: string) {
|
||||
const mdxFiles = getMDXFiles(dir);
|
||||
return Promise.all(
|
||||
mdxFiles.map(async (file) => {
|
||||
const slug = path.basename(file, path.extname(file));
|
||||
const { metadata, source } = await getPost(slug);
|
||||
return {
|
||||
...metadata,
|
||||
slug,
|
||||
source,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function getBlogPosts() {
|
||||
return getAllPosts(path.join(process.cwd(), "content"));
|
||||
}
|
251
src/lib/config.tsx
Normal file
251
src/lib/config.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { FaTwitter } from "react-icons/fa";
|
||||
import { FaYoutube } from "react-icons/fa6";
|
||||
import { RiInstagramFill } from "react-icons/ri";
|
||||
|
||||
export const BLUR_FADE_DELAY = 0.15;
|
||||
|
||||
export const siteConfig = {
|
||||
name: "acme.ai",
|
||||
description: "Automate your workflow with AI",
|
||||
url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
||||
keywords: ["SaaS", "Template", "Next.js", "React", "Tailwind CSS"],
|
||||
links: {
|
||||
email: "support@acme.ai",
|
||||
twitter: "https://twitter.com/magicuidesign",
|
||||
discord: "https://discord.gg/87p2vpsat5",
|
||||
github: "https://github.com/magicuidesign/magicui",
|
||||
instagram: "https://instagram.com/magicuidesign/",
|
||||
},
|
||||
header: [
|
||||
{
|
||||
trigger: "Features",
|
||||
content: {
|
||||
main: {
|
||||
icon: <Icons.logo className="h-6 w-6" />,
|
||||
title: "AI-Powered Automation",
|
||||
description: "Streamline your workflow with intelligent automation.",
|
||||
href: "#",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
href: "#",
|
||||
title: "Task Automation",
|
||||
description: "Automate repetitive tasks and save time.",
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Workflow Optimization",
|
||||
description: "Optimize your processes with AI-driven insights.",
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
title: "Intelligent Scheduling",
|
||||
description: "AI-powered scheduling for maximum efficiency.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: "Solutions",
|
||||
content: {
|
||||
items: [
|
||||
{
|
||||
title: "For Small Businesses",
|
||||
href: "#",
|
||||
description: "Tailored automation solutions for growing companies.",
|
||||
},
|
||||
{
|
||||
title: "Enterprise",
|
||||
href: "#",
|
||||
description: "Scalable AI automation for large organizations.",
|
||||
},
|
||||
{
|
||||
title: "Developers",
|
||||
href: "#",
|
||||
description: "API access and integration tools for developers.",
|
||||
},
|
||||
{
|
||||
title: "Healthcare",
|
||||
href: "#",
|
||||
description: "Specialized automation for healthcare workflows.",
|
||||
},
|
||||
{
|
||||
title: "Finance",
|
||||
href: "#",
|
||||
description: "AI-driven process automation for financial services.",
|
||||
},
|
||||
{
|
||||
title: "Education",
|
||||
href: "#",
|
||||
description:
|
||||
"Streamline administrative tasks in educational institutions.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
href: "/blog",
|
||||
label: "Blog",
|
||||
},
|
||||
],
|
||||
pricing: [
|
||||
{
|
||||
name: "BASIC",
|
||||
href: "#",
|
||||
price: "$19",
|
||||
period: "month",
|
||||
yearlyPrice: "$16",
|
||||
features: [
|
||||
"1 User",
|
||||
"5GB Storage",
|
||||
"Basic Support",
|
||||
"Limited API Access",
|
||||
"Standard Analytics",
|
||||
],
|
||||
description: "Perfect for individuals and small projects",
|
||||
buttonText: "Subscribe",
|
||||
isPopular: false,
|
||||
},
|
||||
{
|
||||
name: "PRO",
|
||||
href: "#",
|
||||
price: "$49",
|
||||
period: "month",
|
||||
yearlyPrice: "$40",
|
||||
features: [
|
||||
"5 Users",
|
||||
"50GB Storage",
|
||||
"Priority Support",
|
||||
"Full API Access",
|
||||
"Advanced Analytics",
|
||||
],
|
||||
description: "Ideal for growing businesses and teams",
|
||||
buttonText: "Subscribe",
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
name: "ENTERPRISE",
|
||||
href: "#",
|
||||
price: "$99",
|
||||
period: "month",
|
||||
yearlyPrice: "$82",
|
||||
features: [
|
||||
"Unlimited Users",
|
||||
"500GB Storage",
|
||||
"24/7 Premium Support",
|
||||
"Custom Integrations",
|
||||
"AI-Powered Insights",
|
||||
],
|
||||
description: "For large-scale operations and high-volume users",
|
||||
buttonText: "Subscribe",
|
||||
isPopular: false,
|
||||
},
|
||||
],
|
||||
faqs: [
|
||||
{
|
||||
question: "What is acme.ai?",
|
||||
answer: (
|
||||
<span>
|
||||
acme.ai is a platform that helps you build and manage your AI-powered
|
||||
applications. It provides tools and services to streamline the
|
||||
development and deployment of AI solutions.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How can I get started with acme.ai?",
|
||||
answer: (
|
||||
<span>
|
||||
You can get started with acme.ai by signing up for an account on our
|
||||
website, creating a new project, and following our quick-start guide.
|
||||
We also offer tutorials and documentation to help you along the way.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "What types of AI models does acme.ai support?",
|
||||
answer: (
|
||||
<span>
|
||||
acme.ai supports a wide range of AI models, including but not limited
|
||||
to natural language processing, computer vision, and predictive
|
||||
analytics. We continuously update our platform to support the latest
|
||||
AI technologies.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Is acme.ai suitable for beginners in AI development?",
|
||||
answer: (
|
||||
<span>
|
||||
Yes, acme.ai is designed to be user-friendly for both beginners and
|
||||
experienced developers. We offer intuitive interfaces, pre-built
|
||||
templates, and extensive learning resources to help users of all skill
|
||||
levels create AI-powered applications.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "What kind of support does acme.ai provide?",
|
||||
answer: (
|
||||
<span>
|
||||
acme.ai provides comprehensive support including documentation, video
|
||||
tutorials, a community forum, and dedicated customer support. We also
|
||||
offer premium support plans for enterprises with more complex needs.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
],
|
||||
footer: [
|
||||
{
|
||||
title: "Product",
|
||||
links: [
|
||||
{ href: "#", text: "Features", icon: null },
|
||||
{ href: "#", text: "Pricing", icon: null },
|
||||
{ href: "#", text: "Documentation", icon: null },
|
||||
{ href: "#", text: "API", icon: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company",
|
||||
links: [
|
||||
{ href: "#", text: "About Us", icon: null },
|
||||
{ href: "#", text: "Careers", icon: null },
|
||||
{ href: "#", text: "Blog", icon: null },
|
||||
{ href: "#", text: "Press", icon: null },
|
||||
{ href: "#", text: "Partners", icon: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Resources",
|
||||
links: [
|
||||
{ href: "#", text: "Community", icon: null },
|
||||
{ href: "#", text: "Contact", icon: null },
|
||||
{ href: "#", text: "Support", icon: null },
|
||||
{ href: "#", text: "Status", icon: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Social",
|
||||
links: [
|
||||
{
|
||||
href: "#",
|
||||
text: "Twitter",
|
||||
icon: <FaTwitter />,
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
text: "Instagram",
|
||||
icon: <RiInstagramFill />,
|
||||
},
|
||||
{
|
||||
href: "#",
|
||||
text: "Youtube",
|
||||
icon: <FaYoutube />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
38
src/lib/hooks/use-window-size.ts
Normal file
38
src/lib/hooks/use-window-size.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState<{
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
}>({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Handler to call on window resize
|
||||
function handleResize() {
|
||||
// Set window width/height to state
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []); // Empty array ensures that effect is only run on mount
|
||||
|
||||
return {
|
||||
windowSize,
|
||||
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768,
|
||||
isDesktop:
|
||||
typeof windowSize?.width === "number" && windowSize?.width >= 768,
|
||||
};
|
||||
}
|
89
src/lib/utils.ts
Normal file
89
src/lib/utils.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { Metadata } from "next";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function absoluteUrl(path: string) {
|
||||
return `${process.env.NEXT_PUBLIC_APP_URL || siteConfig.url}${path}`;
|
||||
}
|
||||
|
||||
export function constructMetadata({
|
||||
title = siteConfig.name,
|
||||
description = siteConfig.description,
|
||||
image = absoluteUrl("/og"),
|
||||
...props
|
||||
}: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
[key: string]: Metadata[keyof Metadata];
|
||||
}): Metadata {
|
||||
return {
|
||||
title: {
|
||||
template: "%s | " + siteConfig.name,
|
||||
default: siteConfig.name,
|
||||
},
|
||||
description: description || siteConfig.description,
|
||||
keywords: siteConfig.keywords,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: siteConfig.url,
|
||||
siteName: siteConfig.name,
|
||||
images: [
|
||||
{
|
||||
url: image,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
},
|
||||
icons: "/favicon.ico",
|
||||
metadataBase: new URL(siteConfig.url),
|
||||
authors: [
|
||||
{
|
||||
name: siteConfig.name,
|
||||
url: siteConfig.url,
|
||||
},
|
||||
],
|
||||
...props,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDate(date: string) {
|
||||
let currentDate = new Date().getTime();
|
||||
if (!date.includes("T")) {
|
||||
date = `${date}T00:00:00`;
|
||||
}
|
||||
let targetDate = new Date(date).getTime();
|
||||
let timeDifference = Math.abs(currentDate - targetDate);
|
||||
let daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||
|
||||
let fullDate = new Date(date).toLocaleString("en-us", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
if (daysAgo < 1) {
|
||||
return "Today";
|
||||
} else if (daysAgo < 7) {
|
||||
return `${fullDate} (${daysAgo}d ago)`;
|
||||
} else if (daysAgo < 30) {
|
||||
const weeksAgo = Math.floor(daysAgo / 7);
|
||||
return `${fullDate} (${weeksAgo}w ago)`;
|
||||
} else if (daysAgo < 365) {
|
||||
const monthsAgo = Math.floor(daysAgo / 30);
|
||||
return `${fullDate} (${monthsAgo}mo ago)`;
|
||||
} else {
|
||||
const yearsAgo = Math.floor(daysAgo / 365);
|
||||
return `${fullDate} (${yearsAgo}y ago)`;
|
||||
}
|
||||
}
|
105
tailwind.config.ts
Normal file
105
tailwind.config.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
animation: {
|
||||
marquee: "marquee var(--duration) linear infinite",
|
||||
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
|
||||
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite",
|
||||
},
|
||||
keyframes: {
|
||||
marquee: {
|
||||
from: { transform: "translateX(0)" },
|
||||
to: { transform: "translateX(calc(-100% - var(--gap)))" },
|
||||
},
|
||||
"marquee-vertical": {
|
||||
from: { transform: "translateY(0)" },
|
||||
to: { transform: "translateY(calc(-100% - var(--gap)))" },
|
||||
},
|
||||
"border-beam": {
|
||||
"100%": {
|
||||
"offset-distance": "100%",
|
||||
},
|
||||
},
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
ripple: {
|
||||
"0%, 100%": {
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
},
|
||||
"50%": {
|
||||
transform: "translate(-50%, -50%) scale(0.9)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user