init project
This commit is contained in:
parent
c73a45047d
commit
b759dbbdba
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
|
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/how-dev-ai.mdx
Normal file
53
content/how-dev-ai.mdx
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
title: How Dev AI?
|
||||
publishedAt: "2024-11-01"
|
||||
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
|
53
content/introducing-dev-ai.mdx
Normal file
53
content/introducing-dev-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
|
53
content/why-dev-ai.mdx
Normal file
53
content/why-dev-ai.mdx
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Why Dev AI?
|
||||
publishedAt: "2024-11-01"
|
||||
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
|
9
next.config.mjs
Normal file
9
next.config.mjs
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [{ hostname: "localhost" }, { hostname: "randomuser.me" }],
|
||||
},
|
||||
transpilePackages: ["geist"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
7807
package-lock.json
generated
Normal file
7807
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "devtool-magicui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@splinetool/react-spline": "^4.0.0",
|
||||
"@splinetool/runtime": "^1.9.37",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.21",
|
||||
"geist": "^1.3.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"next": "15.0.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"rehype-pretty-code": "^0.14.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"shiki": "^1.22.0",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unified": "^11.0.5",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"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": "15.0.3",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
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/cube.png
Normal file
BIN
public/cube.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 176 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: 7.2 KiB |
127
src/app/(marketing)/blog/[slug]/page.tsx
Normal file
127
src/app/(marketing)/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import Author from "@/components/blog-author";
|
||||
import { CTA } 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(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const params = await props.params;
|
||||
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 Page(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const 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-muted 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"
|
||||
/>
|
||||
</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">
|
||||
{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>
|
||||
<CTA />
|
||||
</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 |
125
src/app/globals.css
Normal file
125
src/app/globals.css
Normal file
@ -0,0 +1,125 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 210 20% 98%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 210 20% 98%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 210 20% 98%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 261 75.8% 75.7%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 215 20% 95%;
|
||||
--secondary-foreground: 215 25% 10%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 215 20% 95%;
|
||||
--accent-foreground: 215 25% 10%;
|
||||
--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: 210 76% 61%;
|
||||
--chart-2: 215 58% 39%;
|
||||
--chart-3: 220 37% 24%;
|
||||
--chart-4: 225 74% 66%;
|
||||
--chart-5: 230 87% 67%;
|
||||
|
||||
--color-1: 0 100% 63%;
|
||||
--color-2: 270 100% 63%;
|
||||
--color-3: 210 100% 63%;
|
||||
--color-4: 195 100% 63%;
|
||||
--color-5: 90 100% 63%;
|
||||
|
||||
--header-height: 3.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 225 15% 6%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 225 15% 6%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 225 15% 6%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 261 75.8% 75.7%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 225 10% 18%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 225 10% 12%;
|
||||
--muted-foreground: 225 10% 70%;
|
||||
--accent: 225 10% 18%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 85.7% 97.3%;
|
||||
--border: 225 10% 18%;
|
||||
--input: 225 10% 18%;
|
||||
--ring: 225 15% 80%;
|
||||
--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: 261 75.8% 75.7%;
|
||||
--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: 261 75.8% 75.7%;
|
||||
--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;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:horizontal {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
/* Fix for scrollbar corner overlap */
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
}
|
||||
}
|
51
src/app/layout.tsx
Normal file
51
src/app/layout.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn, constructMetadata } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = constructMetadata({
|
||||
title: `${siteConfig.name} | ${siteConfig.description}`,
|
||||
});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
colorScheme: "dark",
|
||||
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
|
||||
className={`${GeistSans.variable} ${GeistMono.variable}`}
|
||||
>
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background antialiased w-full mx-auto scroll-smooth font-sans"
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem={false}
|
||||
>
|
||||
{children}
|
||||
<ThemeToggle />
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
103
src/app/og/route.tsx
Normal file
103
src/app/og/route.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
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={{
|
||||
color: "#fff",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontSize: "64px",
|
||||
fontWeight: "600",
|
||||
marginTop: "24px",
|
||||
textAlign: "center",
|
||||
color: "#fff",
|
||||
width: "60%",
|
||||
letterSpacing: "-0.05em", // Added tighter tracking
|
||||
}}
|
||||
>
|
||||
{postTitle}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
marginTop: "16px",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{siteConfig.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={`${siteConfig.url}/cube.png`}
|
||||
width={500}
|
||||
style={{
|
||||
position: "relative",
|
||||
bottom: -100,
|
||||
aspectRatio: "auto",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
data: fontData,
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
33
src/app/page.tsx
Normal file
33
src/app/page.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Blog } from "@/components/sections/blog";
|
||||
import { Community } from "@/components/sections/community";
|
||||
import { CTA } from "@/components/sections/cta";
|
||||
import { Examples } from "@/components/sections/examples";
|
||||
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 { Logos } from "@/components/sections/logos";
|
||||
import { Pricing } from "@/components/sections/pricing";
|
||||
import { Statistics } from "@/components/sections/statistics";
|
||||
import { Testimonials } from "@/components/sections/testimonials";
|
||||
import { UseCases } from "@/components/sections/use-cases";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<Header />
|
||||
<Hero />
|
||||
<Logos />
|
||||
<Examples />
|
||||
<UseCases />
|
||||
<Features />
|
||||
<Statistics />
|
||||
<Testimonials />
|
||||
<Pricing />
|
||||
<Community />
|
||||
<Blog />
|
||||
<CTA />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
15
src/app/sitemap.ts
Normal file
15
src/app/sitemap.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const headersList = await headers();
|
||||
let domain = headersList.get("host") as string;
|
||||
let protocol = "https";
|
||||
|
||||
return [
|
||||
{
|
||||
url: `${protocol}://${domain}`,
|
||||
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.
128
src/components/aurora-text.tsx
Normal file
128
src/components/aurora-text.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AuroraText({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"relative overflow-hidden inline-flex bg-background",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div className="aurora absolute inset-0 pointer-events-none mix-blend-lighten dark:mix-blend-darken">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aurora__item absolute w-[60vw] h-[60vw]"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--color-${i + 1}))`,
|
||||
filter: "blur(1rem)",
|
||||
animation: `aurora-border 6s ease-in-out infinite, aurora-${
|
||||
i + 1
|
||||
} 12s ease-in-out infinite alternate`,
|
||||
mixBlendMode: "overlay",
|
||||
...getInitialPosition(i),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<style jsx>{`
|
||||
@keyframes aurora-border {
|
||||
0%,
|
||||
100% {
|
||||
border-radius: 37% 29% 27% 27% / 28% 25% 41% 37%;
|
||||
}
|
||||
25% {
|
||||
border-radius: 47% 29% 39% 49% / 61% 19% 66% 26%;
|
||||
}
|
||||
50% {
|
||||
border-radius: 57% 23% 47% 72% / 63% 17% 66% 33%;
|
||||
}
|
||||
75% {
|
||||
border-radius: 28% 49% 29% 100% / 93% 20% 64% 25%;
|
||||
}
|
||||
}
|
||||
@keyframes aurora-1 {
|
||||
0%,
|
||||
100% {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
50% {
|
||||
top: 50%;
|
||||
right: 25%;
|
||||
}
|
||||
75% {
|
||||
top: 25%;
|
||||
right: 50%;
|
||||
}
|
||||
}
|
||||
@keyframes aurora-2 {
|
||||
0%,
|
||||
100% {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
60% {
|
||||
top: 75%;
|
||||
left: 25%;
|
||||
}
|
||||
85% {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
@keyframes aurora-3 {
|
||||
0%,
|
||||
100% {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
40% {
|
||||
bottom: 50%;
|
||||
left: 25%;
|
||||
}
|
||||
65% {
|
||||
bottom: 25%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
@keyframes aurora-4 {
|
||||
0%,
|
||||
100% {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
50% {
|
||||
bottom: 25%;
|
||||
right: 40%;
|
||||
}
|
||||
90% {
|
||||
bottom: 50%;
|
||||
right: 25%;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function getInitialPosition(index: number): React.CSSProperties {
|
||||
const positions = [
|
||||
{ top: "-50%" },
|
||||
{ right: 0, top: 0 },
|
||||
{ left: 0, bottom: 0 },
|
||||
{ right: 0, bottom: "-50%" },
|
||||
];
|
||||
return positions[index] || {};
|
||||
}
|
70
src/components/blog-author.tsx
Normal file
70
src/components/blog-author.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
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-muted-foreground">Written by {name}</p>
|
||||
<time dateTime={updatedAt} className="text-sm font-light">
|
||||
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-medium text-foreground">{name}</p>
|
||||
<p className="text-sm text-muted-foreground">@{twitterUsername}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
41
src/components/blog-card.tsx
Normal file
41
src/components/blog-card.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
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="bg-background transition-colors hover:bg-secondary/20 p-4 last:border-b-0 lg:border-r last:lg:border-r-0 border-b lg:border-b-0"
|
||||
>
|
||||
{data.image && (
|
||||
<Image
|
||||
className="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="my-2">
|
||||
<time
|
||||
dateTime={data.publishedAt}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
{formatDate(data.publishedAt)}
|
||||
</time>
|
||||
</p>
|
||||
<h3 className="text-xl font-medium mb-2">{data.title}</h3>
|
||||
<p className="text-muted-foreground">{data.summary}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
49
src/components/feature-selector.tsx
Normal file
49
src/components/feature-selector.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface FeatureOption {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface FeatureSelectorProps {
|
||||
features: FeatureOption[];
|
||||
}
|
||||
|
||||
export const FeatureSelector: React.FC<FeatureSelectorProps> = ({
|
||||
features,
|
||||
}) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 relative">
|
||||
<div className="md:col-span-2 border-b md:border-b-0 bg-background md:border-r border-border sticky top-[var(--header-height)]">
|
||||
<div className="flex md:flex-col feature-btn-container overflow-x-auto p-4 pb-2">
|
||||
{features.map((option, index) => (
|
||||
<button
|
||||
key={option.id}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
className={`flex-shrink-0 w-64 md:w-full text-left p-4 mb-2 mr-2 last:mr-0 md:mr-0 rounded border border-border ${
|
||||
selectedIndex === index ? "bg-accent/70" : "hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-medium tracking-tight">{option.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{option.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 md:col-span-3">
|
||||
<div
|
||||
className="bg-background font-mono text-sm [&>pre]:!bg-transparent [&>pre]:p-4 [&_code]:break-all md:max-h-[45vh] overflow-scroll"
|
||||
dangerouslySetInnerHTML={{ __html: features[selectedIndex].code }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
144
src/components/icons.tsx
Normal file
144
src/components/icons.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import { DiscordLogoIcon } from "@radix-ui/react-icons";
|
||||
|
||||
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}
|
||||
>
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" x2="20" y1="19" y2="19"></line>
|
||||
</svg>
|
||||
),
|
||||
discord: DiscordLogoIcon,
|
||||
twitter: (props: IconProps) => (
|
||||
<svg
|
||||
height="23"
|
||||
viewBox="0 0 1200 1227"
|
||||
width="23"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
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>
|
||||
),
|
||||
};
|
49
src/components/mobile-drawer.tsx
Normal file
49
src/components/mobile-drawer.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
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 function MobileDrawer() {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger>
|
||||
<IoMenuSharp className="text-2xl" />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="px-6">
|
||||
<Link
|
||||
href="/"
|
||||
title="brand-logo"
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="w-auto h-[40px]" />
|
||||
<DrawerTitle>{siteConfig.name}</DrawerTitle>
|
||||
</Link>
|
||||
<DrawerDescription>{siteConfig.description}</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<Link
|
||||
href="#"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"text-white rounded-full group"
|
||||
)}
|
||||
>
|
||||
{siteConfig.cta}
|
||||
</Link>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
96
src/components/section.tsx
Normal file
96
src/components/section.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import FlickeringGrid from "@/components/ui/flickering-grid";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
|
||||
interface SectionProps {
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
align?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
const Section = forwardRef<HTMLElement, SectionProps>(
|
||||
(
|
||||
{ id, title, subtitle, description, children, className, align },
|
||||
forwardedRef
|
||||
) => {
|
||||
const internalRef = useRef<HTMLElement>(null);
|
||||
const ref = forwardedRef || internalRef;
|
||||
const sectionId = title ? title.toLowerCase().replace(/\s+/g, "-") : id;
|
||||
const alignmentClass =
|
||||
align === "left"
|
||||
? "text-left"
|
||||
: align === "right"
|
||||
? "text-right"
|
||||
: "text-center";
|
||||
|
||||
return (
|
||||
<section id={id} ref={ref}>
|
||||
<div className={cn("relative mx-auto container", className)}>
|
||||
{(title || subtitle || description) && (
|
||||
<div
|
||||
className={cn(
|
||||
alignmentClass,
|
||||
"relative mx-auto border-x border-t overflow-hidden p-2 py-8 md:p-12"
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<h2 className="text-sm text-muted-foreground text-balance font-semibold tracking-tigh uppercase">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{subtitle && (
|
||||
<h3
|
||||
className={cn(
|
||||
"mx-0 mt-4 max-w-lg text-5xl text-balance font-bold sm:max-w-none sm:text-4xl md:text-5xl lg:text-6xl leading-[1.2] tracking-tighter text-foreground lowercase",
|
||||
align === "center"
|
||||
? "mx-auto"
|
||||
: align === "right"
|
||||
? "ml-auto"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{subtitle}
|
||||
</h3>
|
||||
)}
|
||||
{description && (
|
||||
<p
|
||||
className={cn(
|
||||
"mt-6 text-lg leading-8 text-muted-foreground text-balance max-w-2xl",
|
||||
align === "center"
|
||||
? "mx-auto"
|
||||
: align === "right"
|
||||
? "ml-auto"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-full w-full bg-gradient-to-t from-background dark:from-background -z-10 from-50%" />
|
||||
<FlickeringGrid
|
||||
squareSize={4}
|
||||
gridGap={4}
|
||||
color="#6B7280"
|
||||
maxOpacity={0.2}
|
||||
flickerChance={0.1}
|
||||
className="-z-20 absolute inset-0 size-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Section.displayName = "Section";
|
||||
|
||||
export { 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 async function Blog() {
|
||||
const allPosts = await getBlogPosts();
|
||||
|
||||
const articles = await Promise.all(
|
||||
allPosts.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
|
||||
);
|
||||
|
||||
return (
|
||||
<Section id="blog" title="Blog">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 border border-b-0">
|
||||
{articles.map((data, idx) => (
|
||||
<BlogCard key={data.slug} data={data} priority={idx <= 1} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
73
src/components/sections/community.tsx
Normal file
73
src/components/sections/community.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Section } from "@/components/section";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Ripple } from "@/components/ui/ripple";
|
||||
|
||||
const contributors = [
|
||||
{
|
||||
name: "Alice Johnson",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D",
|
||||
},
|
||||
{
|
||||
name: "Bob Brown",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
name: "Charlie Davis",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
name: "Diana Evans",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
name: "Ethan Ford",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
];
|
||||
|
||||
export function Community() {
|
||||
return (
|
||||
<Section id="community" title="Community">
|
||||
<div className="border-x border-t overflow-hidden relative">
|
||||
<Ripple />
|
||||
<div className="p-6 text-center py-12">
|
||||
<p className="text-muted-foreground mb-6 text-balance max-w-prose mx-auto font-medium">
|
||||
We're grateful for the amazing open-source community that helps
|
||||
make our project better every day.
|
||||
</p>
|
||||
<div className="flex justify-center -space-x-6 mb-8">
|
||||
{contributors.map((contributor, index) => (
|
||||
<div key={index}>
|
||||
<Avatar className="size-12 relative border-2 border-background bg-muted">
|
||||
<AvatarImage
|
||||
src={contributor.avatar}
|
||||
alt={contributor.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="text-lg font-semibold">
|
||||
{contributor.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button variant="secondary" className="flex items-center gap-2">
|
||||
<Icons.github className="h-5 w-5" />
|
||||
Become a contributor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
18
src/components/sections/cta.tsx
Normal file
18
src/components/sections/cta.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Section } from "@/components/section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function CTA() {
|
||||
return (
|
||||
<Section id="cta">
|
||||
<div className="border overflow-hidden relative text-center py-16 mx-auto">
|
||||
<p className="max-w-3xl text-foreground mb-6 text-balance mx-auto font-medium text-3xl">
|
||||
Ready to build your next AI agent?
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button className="flex items-center gap-2">Get Started</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
207
src/components/sections/examples.tsx
Normal file
207
src/components/sections/examples.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import { FeatureSelector } from "@/components/feature-selector";
|
||||
import { Section } from "@/components/section";
|
||||
import { codeToHtml } from "shiki";
|
||||
|
||||
interface FeatureOption {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const featureOptions: FeatureOption[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Simple Agent Workflow",
|
||||
description: "Create a basic AI agent workflow with multiple agents.",
|
||||
code: `import { Swarm, Agent } from 'ai-agent-sdk';
|
||||
|
||||
const client = new Swarm();
|
||||
|
||||
const transferToAgentB = (): Agent => {
|
||||
return agentB;
|
||||
};
|
||||
|
||||
const agentA = new Agent({
|
||||
name: "Agent A",
|
||||
instructions: "You are a helpful agent.",
|
||||
functions: [transferToAgentB],
|
||||
});
|
||||
|
||||
const agentB = new Agent({
|
||||
name: "Agent B",
|
||||
instructions: "Only speak in Haikus.",
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
const response = await client.run({
|
||||
agent: agentA,
|
||||
messages: [{ role: "user", content: "I want to talk to agent B." }],
|
||||
});
|
||||
console.log('Response:', response);
|
||||
};
|
||||
|
||||
run();`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Multi-Agent Collaboration",
|
||||
description:
|
||||
"Set up multiple AI agents to work together on a complex task.",
|
||||
code: `import { Agent, MultiAgentSystem } from 'ai-agent-sdk';
|
||||
|
||||
const researchAgent = new Agent('Researcher');
|
||||
const analysisAgent = new Agent('Analyst');
|
||||
const reportAgent = new Agent('Reporter');
|
||||
|
||||
const system = new MultiAgentSystem('MarketResearch');
|
||||
|
||||
system.addAgent(researchAgent, {
|
||||
task: 'collectData',
|
||||
output: 'rawData'
|
||||
});
|
||||
|
||||
system.addAgent(analysisAgent, {
|
||||
task: 'analyzeData',
|
||||
input: 'rawData',
|
||||
output: 'analysisResults'
|
||||
});
|
||||
|
||||
system.addAgent(reportAgent, {
|
||||
task: 'generateReport',
|
||||
input: 'analysisResults',
|
||||
output: 'finalReport'
|
||||
});
|
||||
|
||||
const runResearch = async () => {
|
||||
const finalReport = await system.run();
|
||||
console.log('Research completed:', finalReport);
|
||||
};
|
||||
|
||||
runResearch();`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Tool Integration",
|
||||
description: "Integrate external tools and APIs into an AI agent workflow.",
|
||||
code: `import { Agent, Tool } from 'ai-agent-sdk';
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
|
||||
const agent = new Agent('ResearchAssistant');
|
||||
|
||||
const openaiTool = new Tool('OpenAI', {
|
||||
action: async (prompt: string) => {
|
||||
const configuration = new Configuration({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
const response = await openai.createCompletion({
|
||||
model: "text-davinci-002",
|
||||
prompt,
|
||||
});
|
||||
return response.data.choices[0].text;
|
||||
}
|
||||
});
|
||||
|
||||
const searchTool = new Tool('GoogleSearch', {
|
||||
action: async (query: string) => {
|
||||
const url = new URL('https://www.googleapis.com/customsearch/v1');
|
||||
url.searchParams.append('key', process.env.GOOGLE_API_KEY);
|
||||
url.searchParams.append('cx', process.env.GOOGLE_SEARCH_ENGINE_ID);
|
||||
url.searchParams.append('q', query);
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data.items.slice(0, 5);
|
||||
}
|
||||
});
|
||||
|
||||
agent.addTool(openaiTool);
|
||||
agent.addTool(searchTool);
|
||||
|
||||
const performResearch = async (topic: string) => {
|
||||
const researchResult = await agent.performResearch(topic);
|
||||
console.log('Research results:', researchResult);
|
||||
};
|
||||
|
||||
performResearch('AI advancements in 2023');`,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Customizable Agent Behavior",
|
||||
description:
|
||||
"Design a specialized AI agent with custom decision-making logic.",
|
||||
code: `import { Agent, KnowledgeBase } from 'ai-agent-sdk';
|
||||
|
||||
class CustomerSupportAgent extends Agent {
|
||||
private knowledgeBase: KnowledgeBase;
|
||||
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
this.knowledgeBase = new KnowledgeBase('support-docs.json');
|
||||
}
|
||||
|
||||
async decideAction(input: string): Promise<string> {
|
||||
if (this.isSimpleQuery(input)) {
|
||||
return this.provideDirectAnswer(input);
|
||||
} else if (this.needsEscalation(input)) {
|
||||
return this.escalateToHuman(input);
|
||||
} else {
|
||||
return this.generateDetailedResponse(input);
|
||||
}
|
||||
}
|
||||
|
||||
private isSimpleQuery(input: string): boolean {
|
||||
// Custom logic to determine if the query is simple
|
||||
return input.split(' ').length < 5;
|
||||
}
|
||||
|
||||
private needsEscalation(input: string): boolean {
|
||||
// Custom logic to decide if human intervention is needed
|
||||
return input.toLowerCase().includes('urgent') || input.toLowerCase().includes('complaint');
|
||||
}
|
||||
|
||||
private async provideDirectAnswer(input: string): Promise<string> {
|
||||
return this.knowledgeBase.getQuickAnswer(input);
|
||||
}
|
||||
|
||||
private async escalateToHuman(input: string): Promise<string> {
|
||||
// Logic to forward the query to a human support agent
|
||||
return "Your query has been escalated to our human support team. They will contact you shortly.";
|
||||
}
|
||||
|
||||
private async generateDetailedResponse(input: string): Promise<string> {
|
||||
// Use AI to generate a detailed response
|
||||
return this.generateResponse(input);
|
||||
}
|
||||
}
|
||||
|
||||
const handleCustomerQuery = async (query: string) => {
|
||||
const supportAgent = new CustomerSupportAgent('HelpDesk');
|
||||
const response = await supportAgent.handleQuery(query);
|
||||
console.log('Agent response:', response);
|
||||
};
|
||||
|
||||
handleCustomerQuery("How do I reset my password?");`,
|
||||
},
|
||||
];
|
||||
|
||||
export async function Examples() {
|
||||
const features = await Promise.all(
|
||||
featureOptions.map(async (feature) => ({
|
||||
...feature,
|
||||
code: await codeToHtml(feature.code, {
|
||||
lang: "typescript",
|
||||
theme: "github-dark",
|
||||
}),
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<Section id="examples">
|
||||
<div className="border-x border-t">
|
||||
<FeatureSelector features={features} />
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
45
src/components/sections/features.tsx
Normal file
45
src/components/sections/features.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Section } from "@/components/section";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Features() {
|
||||
const services = siteConfig.features;
|
||||
return (
|
||||
<Section id="features" title="Features">
|
||||
<div className="border-x border-t">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
{services.map(({ name, description, icon: Icon }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex flex-col gap-y-2 items-center justify-center py-8 px-4 border-b transition-colors hover:bg-secondary/20",
|
||||
"last:border-b-0",
|
||||
"md:[&:nth-child(2n+1)]:border-r md:[&:nth-child(n+5)]:border-b-0",
|
||||
"lg:[&:nth-child(3n)]:border-r-0 lg:[&:nth-child(n+4)]:border-b-0 lg:border-r"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-2 items-center">
|
||||
<div className="bg-gradient-to-b from-primary to-primary/80 p-2 rounded-lg text-white transition-colors group-hover:from-secondary group-hover:to-secondary/80">
|
||||
{Icon}
|
||||
</div>
|
||||
<h2 className="text-xl font-medium text-card-foreground text-center text-balance">
|
||||
{name}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground text-balance text-center max-w-md mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-primary hover:underline underline-offset-4 transition-colors hover:text-secondary-foreground"
|
||||
>
|
||||
Learn more >
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
49
src/components/sections/footer.tsx
Normal file
49
src/components/sections/footer.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { BorderText } from "@/components/ui/border-number";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="flex flex-col gap-y-5 rounded-lg px-7 py-5 container">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Icons.logo className="h-5 w-5" />
|
||||
<h2 className="text-lg font-bold text-foreground">
|
||||
{siteConfig.name}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
{siteConfig.footer.socialLinks.map((link, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={link.url}
|
||||
className="flex h-5 w-5 items-center justify-center text-muted-foreground transition-all duration-100 ease-linear hover:text-foreground hover:underline hover:underline-offset-4"
|
||||
>
|
||||
{link.icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-y-5 md:flex-row md:items-center">
|
||||
<ul className="flex flex-col gap-x-5 gap-y-2 text-muted-foreground md:flex-row md:items-center">
|
||||
{siteConfig.footer.links.map((link, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-[15px]/normal font-medium text-muted-foreground transition-all duration-100 ease-linear hover:text-foreground hover:underline hover:underline-offset-4"
|
||||
>
|
||||
<a href={link.url}>{link.text}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center justify-between text-sm font-medium tracking-tight text-muted-foreground">
|
||||
<p>{siteConfig.footer.bottomText}</p>
|
||||
</div>
|
||||
</div>
|
||||
<BorderText
|
||||
text={siteConfig.footer.brandText}
|
||||
className="text-[clamp(3rem,15vw,10rem)] overflow-hidden font-mono tracking-tighter font-medium"
|
||||
/>
|
||||
</footer>
|
||||
);
|
||||
}
|
43
src/components/sections/header.tsx
Normal file
43
src/components/sections/header.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { MobileDrawer } from "@/components/mobile-drawer";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { easeInOutCubic } from "@/lib/animation";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion, useAnimation } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 h-[var(--header-height)] z-50 p-0 bg-background/60 backdrop-blur">
|
||||
<div className="flex justify-between items-center container mx-auto p-2">
|
||||
<Link
|
||||
href="/"
|
||||
title="brand-logo"
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="w-auto" />
|
||||
<span className="font-semibold text-lg">{siteConfig.name}</span>
|
||||
</Link>
|
||||
<div className="hidden lg:block">
|
||||
<Link
|
||||
href="#"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"h-8 text-primary-foreground rounded-lg group tracking-tight font-medium"
|
||||
)}
|
||||
>
|
||||
{siteConfig.cta}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-2 cursor-pointer block lg:hidden">
|
||||
<MobileDrawer />
|
||||
</div>
|
||||
</div>
|
||||
<hr className="absolute w-full bottom-0" />
|
||||
</header>
|
||||
);
|
||||
}
|
179
src/components/sections/hero.tsx
Normal file
179
src/components/sections/hero.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Section } from "@/components/section";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1];
|
||||
|
||||
function HeroPill() {
|
||||
return (
|
||||
<motion.a
|
||||
href="/blog/introducing-dev-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-left text-xs font-medium text-primary sm:text-sm">
|
||||
🛠️ New
|
||||
</div>
|
||||
<p className="text-xs font-medium text-primary sm:text-sm">
|
||||
Introducing AI Agent SDK
|
||||
</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-3xl flex-col overflow-hidden pt-8">
|
||||
<motion.h1
|
||||
className="text-left text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease,
|
||||
staggerChildren: 0.2,
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
className="inline-block text-balance"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: 0.5,
|
||||
ease,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">
|
||||
{siteConfig.hero.title}
|
||||
</AuroraText>
|
||||
</motion.span>
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="text-left max-w-xl leading-normal text-muted-foreground sm:text-lg sm:leading-normal text-balance"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.6,
|
||||
duration: 0.8,
|
||||
ease,
|
||||
}}
|
||||
>
|
||||
{siteConfig.hero.description}
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroCTA() {
|
||||
return (
|
||||
<div className="relative mt-6">
|
||||
<motion.div
|
||||
className="flex w-full max-w-2xl flex-col items-start justify-start 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="/download"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2 rounded-lg"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
{siteConfig.hero.cta}
|
||||
</Link>
|
||||
</motion.div>
|
||||
<motion.p
|
||||
className="mt-3 text-sm text-muted-foreground text-left"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.0, duration: 0.8 }}
|
||||
>
|
||||
{siteConfig.hero.ctaDescription}
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const LazySpline = lazy(() => import("@splinetool/react-spline"));
|
||||
|
||||
export function Hero() {
|
||||
const [showSpline, setShowSpline] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024); // Assuming 1024px is the breakpoint for lg
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't show on mobile
|
||||
if (!isMobile) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowSpline(true);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<Section id="hero">
|
||||
<div className="relative grid grid-cols-1 lg:grid-cols-2 gap-x-8 w-full p-6 lg:p-12 border-x overflow-hidden">
|
||||
<div className="flex flex-col justify-start items-start lg:col-span-1">
|
||||
<HeroPill />
|
||||
<HeroTitles />
|
||||
<HeroCTA />
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="relative lg:h-full lg:col-span-1">
|
||||
<Suspense>
|
||||
{showSpline && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 1 }}
|
||||
>
|
||||
<LazySpline
|
||||
scene="https://prod.spline.design/mZBrYNcnoESGlTUG/scene.splinecode"
|
||||
className="absolute inset-0 w-full h-full origin-top-left flex items-center justify-center"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
66
src/components/sections/logos.tsx
Normal file
66
src/components/sections/logos.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Section } from "@/components/section";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const companies = [
|
||||
"Google",
|
||||
"Microsoft",
|
||||
"Amazon",
|
||||
"Netflix",
|
||||
"YouTube",
|
||||
"Instagram",
|
||||
];
|
||||
|
||||
const companies2 = ["Spotify", "Dropbox", "Tinder", "Slack", "Zoom", "Shopify"];
|
||||
|
||||
export function Logos() {
|
||||
const [currentSet, setCurrentSet] = useState(companies);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentSet((prev) => (prev === companies ? companies2 : companies));
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Section id="logos">
|
||||
<div className="border-x border-t">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6">
|
||||
{companies.map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex group items-center justify-center p-4 border-r border-t last:border-r-0 sm:last:border-r md:[&:nth-child(3n)]:border-r md:[&:nth-child(6n)]:border-r-0 md:[&:nth-child(3)]:border-r [&:nth-child(-n+2)]:border-t-0 sm:[&:nth-child(-n+3)]:border-t-0 sm:[&:nth-child(3n)]:border-r-0 md:[&:nth-child(-n+6)]:border-t-0 [&:nth-child(2n)]:border-r-0 sm:[&:nth-child(2n)]:border-r"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentSet[idx]}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
ease: "easeInOut",
|
||||
delay: Math.random() * 0.5,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
width={112}
|
||||
height={40}
|
||||
src={`https://cdn.magicui.design/companies/${currentSet[idx]}.svg`}
|
||||
className="h-10 w-28 dark:brightness-0 dark:invert grayscale hover:grayscale-0 hover:brightness-100 dark:hover:brightness-0 dark:hover:invert transition-all duration-200 ease-out opacity-30 hover:opacity-100"
|
||||
alt={currentSet[idx]}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
228
src/components/sections/pricing.tsx
Normal file
228
src/components/sections/pricing.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
"use client";
|
||||
|
||||
import { Section } from "@/components/section";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface TabsProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: "yearly" | "monthly") => void;
|
||||
className?: string;
|
||||
children: (activeTab: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface TabsListProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TabsTriggerProps {
|
||||
value: string;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const Tabs = ({ activeTab, setActiveTab, className, children }: TabsProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto flex w-full items-center justify-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children(activeTab)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabsList = ({ children }: TabsListProps) => {
|
||||
return (
|
||||
<div className="relative flex w-fit items-center rounded-full border p-1.5">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabsTrigger = ({
|
||||
value,
|
||||
onClick,
|
||||
children,
|
||||
isActive,
|
||||
}: TabsTriggerProps) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn("relative z-[1] px-4 py-2", { "z-0": isActive })}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="active-tab"
|
||||
className="absolute inset-0 rounded-full bg-accent"
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
velocity: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"relative block text-sm font-medium duration-200",
|
||||
isActive ? "delay-100 text-primary" : ""
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
function PricingTier({
|
||||
tier,
|
||||
billingCycle,
|
||||
}: {
|
||||
tier: (typeof siteConfig.pricing)[0];
|
||||
billingCycle: "monthly" | "yearly";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"outline-focus transition-transform-background relative z-10 box-border grid h-full w-full overflow-hidden text-foreground motion-reduce:transition-none lg:border-r border-t last:border-r-0",
|
||||
tier.popular ? "bg-primary/5" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<CardHeader className="border-b p-4 grid grid-rows-2 h-fit">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{tier.name}
|
||||
</span>
|
||||
{tier.popular && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-primary text-primary-foreground hover:bg-secondary-foreground"
|
||||
>
|
||||
Most Popular
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<div className="pt-2 text-3xl font-bold">
|
||||
<motion.div
|
||||
key={tier.price[billingCycle]}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
x: billingCycle === "yearly" ? -10 : 10,
|
||||
filter: "blur(5px)",
|
||||
}}
|
||||
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
|
||||
transition={{
|
||||
duration: 0.25,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
>
|
||||
{tier.price[billingCycle]}
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
/ {tier.frequency[billingCycle]}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
<p className="text-[15px] font-medium text-muted-foreground">
|
||||
{tier.description}
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-grow p-4 pt-5">
|
||||
<ul className="space-y-2">
|
||||
{tier.features.map((feature, featureIndex) => (
|
||||
<li key={featureIndex} className="flex items-center">
|
||||
<Check className="mr-2 size-4 text-green-500" />
|
||||
<span className="font-medium">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className={cn(
|
||||
"w-full rounded-none shadow-none",
|
||||
tier.popular
|
||||
? "bg-primary text-primary-foreground hover:bg-secondary-foreground"
|
||||
: "bg-muted text-foreground hover:bg-muted/80"
|
||||
)}
|
||||
>
|
||||
{tier.cta}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Pricing() {
|
||||
const [billingCycle, setBillingCycle] = useState<"monthly" | "yearly">(
|
||||
"yearly"
|
||||
);
|
||||
|
||||
const handleTabChange = (tab: "yearly" | "monthly") => {
|
||||
setBillingCycle(tab);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section id="pricing" title="Pricing">
|
||||
<div className="border border-b-0 grid grid-rows-1">
|
||||
<div className="grid grid-rows-1 gap-y-10 p-10">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl md:text-5xl font-bold tracking-tighter text-balance">
|
||||
Simple pricing for everyone.
|
||||
</h2>
|
||||
|
||||
<p className="mt-6 text-balance text-muted-foreground">
|
||||
Choose an <strong>affordable plan</strong> that's packed with
|
||||
the best features for engaging your audience, creating customer
|
||||
loyalty, and driving sales.
|
||||
</p>
|
||||
</div>
|
||||
<Tabs
|
||||
activeTab={billingCycle}
|
||||
setActiveTab={handleTabChange}
|
||||
className="mx-auto w-full max-w-md"
|
||||
>
|
||||
{(activeTab) => (
|
||||
<TabsList>
|
||||
{["yearly", "monthly"].map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab}
|
||||
value={tab}
|
||||
onClick={() => handleTabChange(tab as "yearly" | "monthly")}
|
||||
isActive={activeTab === tab}
|
||||
>
|
||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||
{tab === "yearly" && (
|
||||
<span className="ml-2 text-xs font-semibold text-green-500">
|
||||
Save 25%
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3">
|
||||
{siteConfig.pricing.map((tier, index) => (
|
||||
<PricingTier key={index} tier={tier} billingCycle={billingCycle} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
74
src/components/sections/statistics.tsx
Normal file
74
src/components/sections/statistics.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Section } from "@/components/section";
|
||||
import { BorderText } from "@/components/ui/border-number";
|
||||
import Link from "next/link";
|
||||
|
||||
const stats = [
|
||||
{
|
||||
title: "10K+",
|
||||
subtitle: "Stars on GitHub",
|
||||
icon: <Icons.github className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "50K+",
|
||||
subtitle: "Discord Members",
|
||||
icon: <Icons.discord className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "1M+",
|
||||
subtitle: "Downloads",
|
||||
icon: <Icons.npm className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
|
||||
export function Statistics() {
|
||||
return (
|
||||
<Section id="statistics" title="Statistics">
|
||||
<div
|
||||
className="border-x border-t"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at bottom center, hsl(var(--secondary) / 0.4), hsl(var(--background)))",
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3">
|
||||
{stats.map((stat, idx) => (
|
||||
<Link
|
||||
href="#"
|
||||
key={idx}
|
||||
className="flex flex-col items-center justify-center py-8 px-4 border-b sm:border-b-0 last:border-b-0 sm:border-r sm:last:border-r-0 [&:nth-child(-n+2)]:border-t-0 sm:[&:nth-child(-n+3)]:border-t-0 relative group overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-all transform translate-x-full -translate-y-full group-hover:translate-x-0 group-hover:translate-y-0 duration-300 ease-in-out">
|
||||
<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"
|
||||
>
|
||||
<line x1="7" y1="17" x2="17" y2="7"></line>
|
||||
<polyline points="7 7 17 7 17 17"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-center relative">
|
||||
<BorderText text={stat.title} />
|
||||
<div className="flex items-center justify-center gap-2 mt-2">
|
||||
{stat.icon}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{stat.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
66
src/components/sections/testimonials.tsx
Normal file
66
src/components/sections/testimonials.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Section } from "@/components/section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
|
||||
export function Testimonials() {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const initialDisplayCount = 9;
|
||||
|
||||
return (
|
||||
<Section id="testimonials" title="Testimonials">
|
||||
<div className="border-t">
|
||||
<div className="columns-1 sm:columns-2 lg:columns-3 gap-0 lg:bg-grid-3 border-r pb-24 sm:bg-grid-2 relative bg-grid-1">
|
||||
<div className="pointer-events-none absolute bottom-0 left-1/2 -translate-x-1/2 h-2/6 w-[calc(100%-2px)] overflow-hidden bg-gradient-to-t from-background to-transparent"></div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="absolute bottom-12 left-1/2 -translate-x-1/2 border h-10 w-fit px-5 flex items-center justify-center z-10"
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
>
|
||||
{showAll ? "Show less" : "See more"}
|
||||
</Button>
|
||||
|
||||
{siteConfig.testimonials.map((testimonial, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.05 }}
|
||||
className={cn(
|
||||
"flex flex-col border-b break-inside-avoid border-l",
|
||||
"transition-colors hover:bg-secondary/20",
|
||||
!showAll && index >= initialDisplayCount && "hidden"
|
||||
)}
|
||||
>
|
||||
<div className="px-4 py-5 sm:p-6 flex-grow">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
{testimonial.image && (
|
||||
<img
|
||||
src={testimonial.image}
|
||||
alt={testimonial.name}
|
||||
className="w-12 h-12 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
{testimonial.name}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{testimonial.company}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>{testimonial.text}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
362
src/components/sections/use-cases.tsx
Normal file
362
src/components/sections/use-cases.tsx
Normal file
@ -0,0 +1,362 @@
|
||||
"use client";
|
||||
|
||||
import { Section } from "@/components/section";
|
||||
import OrbitingCircles from "@/components/ui/orbiting-circles";
|
||||
import { cubicBezier, motion } from "framer-motion";
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
BrainCircuitIcon,
|
||||
DatabaseIcon,
|
||||
GitForkIcon,
|
||||
HeadsetIcon,
|
||||
InfoIcon,
|
||||
MessageSquareIcon,
|
||||
SearchIcon,
|
||||
SquareTerminal,
|
||||
UserSearch,
|
||||
XCircleIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
const containerVariants = {
|
||||
initial: {},
|
||||
whileHover: {
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function Card1() {
|
||||
const variant1 = {
|
||||
initial: {
|
||||
scale: 0.87,
|
||||
transition: {
|
||||
delay: 0.05,
|
||||
duration: 0.2,
|
||||
ease: "linear",
|
||||
},
|
||||
},
|
||||
whileHover: {
|
||||
scale: 0.8,
|
||||
boxShadow:
|
||||
"rgba(245,40,145,0.35) 0px 20px 70px -10px, rgba(36,42,66,0.04) 0px 10px 24px -8px, rgba(36,42,66,0.06) 0px 1px 4px -1px",
|
||||
transition: {
|
||||
delay: 0.05,
|
||||
duration: 0.2,
|
||||
ease: "linear",
|
||||
},
|
||||
},
|
||||
};
|
||||
const variant2 = {
|
||||
initial: {
|
||||
y: -27,
|
||||
scale: 0.95,
|
||||
transition: {
|
||||
delay: 0,
|
||||
duration: 0.2,
|
||||
ease: "linear",
|
||||
},
|
||||
},
|
||||
whileHover: {
|
||||
y: -55,
|
||||
scale: 0.87,
|
||||
boxShadow:
|
||||
"rgba(39,127,245,0.15) 0px 20px 70px -10px, rgba(36,42,66,0.04) 0px 10px 24px -8px, rgba(36,42,66,0.06) 0px 1px 4px -1px",
|
||||
transition: {
|
||||
delay: 0,
|
||||
duration: 0.2,
|
||||
ease: "linear",
|
||||
},
|
||||
},
|
||||
};
|
||||
const variant3 = {
|
||||
initial: {
|
||||
y: -25,
|
||||
opacity: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
delay: 0.05,
|
||||
duration: 0.2,
|
||||
ease: "linear",
|
||||
},
|
||||
},
|
||||
whileHover: {
|
||||
y: -45,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
boxShadow:
|
||||
"rgba(39,245,76,0.15) 10px 20px 70px -20px, rgba(36,42,66,0.04) 0px 10px 24px -8px, rgba(36,42,66,0.06) 0px 1px 4px -1px",
|
||||
transition: {
|
||||
delay: 0.05,
|
||||
duration: 0.2,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
initial: {},
|
||||
whileHover: {
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-0 h-full overflow-hidden border-b lg:border-b-0 lg:border-r">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="initial"
|
||||
whileHover="whileHover"
|
||||
className="flex flex-col gap-y-5 items-center justify-between h-full w-full cursor-pointer"
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center rounded-t-xl border-b">
|
||||
<div className="relative flex flex-col items-center justify-center gap-y-2 p-10">
|
||||
<motion.div
|
||||
variants={variant1}
|
||||
className="z-[1] flex h-full w-full items-center justify-between gap-x-2 rounded-md border bg-background p-5 px-2.5 "
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<SearchIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="h-2 w-32 rounded-full bg-neutral-800/50 dark:bg-neutral-200/80"></div>
|
||||
<div className="h-2 w-48 rounded-full bg-slate-400/50"></div>
|
||||
<div className="text-xs text-neutral-500">
|
||||
Google Search API integration
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
variants={variant2}
|
||||
className="z-[2] flex h-full w-full items-center justify-between gap-x-2 rounded-md border bg-background p-5 px-2.5 "
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<DatabaseIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="h-2 w-32 rounded-full bg-neutral-800/50 dark:bg-neutral-200/80"></div>
|
||||
<div className="h-2 w-48 rounded-full bg-slate-400/50"></div>
|
||||
<div className="h-2 w-20 rounded-full bg-slate-400/50"></div>
|
||||
<div className="text-xs text-neutral-500">
|
||||
PostgreSQL database connection
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
variants={variant3}
|
||||
className="absolute bottom-0 z-[3] m-auto flex h-fit w-fit items-center justify-between gap-x-2 rounded-md border bg-background p-5 px-2.5 "
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-purple-500 flex items-center justify-center">
|
||||
<MessageSquareIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="h-2 w-32 rounded-full bg-neutral-800/50 dark:bg-neutral-200/80"></div>
|
||||
<div className="h-2 w-48 rounded-full bg-slate-400/50"></div>
|
||||
<div className="h-2 w-20 rounded-full bg-slate-400/50"></div>
|
||||
<div className="h-2 w-48 rounded-full bg-slate-400/50"></div>
|
||||
<div className="text-xs text-neutral-500">
|
||||
OpenAI GPT-3.5 API integration
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1 px-5 pb-4 items-start w-full">
|
||||
<h2 className="font-semibold tracking-tight text-lg">
|
||||
Tool Integration
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Seamlessly integrate external APIs and tools into agent workflows.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Card2 = () => {
|
||||
const logs = [
|
||||
{
|
||||
id: 1,
|
||||
type: "info",
|
||||
timestamp: "2023-12-15 14:23:45",
|
||||
message: "Agent initialized. Starting task execution.",
|
||||
icon: (
|
||||
<div className="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<InfoIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "action",
|
||||
timestamp: "2023-12-15 14:23:47",
|
||||
message: "Retrieving data from external API...",
|
||||
icon: (
|
||||
<div className="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<DatabaseIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "decision",
|
||||
timestamp: "2023-12-15 14:23:50",
|
||||
message: "Analyzing data. Confidence: 85%",
|
||||
icon: (
|
||||
<div className="h-8 w-8 rounded-full bg-purple-500 flex items-center justify-center">
|
||||
<BrainCircuitIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: "warning",
|
||||
timestamp: "2023-12-15 14:23:52",
|
||||
message: "Potential anomaly detected in dataset.",
|
||||
icon: (
|
||||
<div className="h-8 w-8 rounded-full bg-yellow-500 flex items-center justify-center">
|
||||
<AlertTriangleIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: "error",
|
||||
timestamp: "2023-12-15 14:23:55",
|
||||
message: "Failed to connect to secondary database.",
|
||||
icon: (
|
||||
<div className="h-8 w-8 rounded-full bg-red-500 flex items-center justify-center">
|
||||
<XCircleIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="p-0 h-full overflow-hidden border-b lg:border-b-0 lg:border-r">
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="initial"
|
||||
whileHover="whileHover"
|
||||
className="flex flex-col gap-y-5 items-center justify-between h-full w-full cursor-pointer"
|
||||
>
|
||||
<div className="border-b items-center justify-center overflow-hidden bg-transparent rounded-t-xl h-4/5 w-full flex ">
|
||||
<motion.div className="p-5 rounded-t-md cursor-pointer overflow-hidden h-[270px] flex flex-col gap-y-3.5 w-full">
|
||||
{logs.map((log, index) => (
|
||||
<motion.div
|
||||
key={log.id}
|
||||
className="p-4 bg-transparent backdrop-blur-md shadow-[0px_0px_40px_-25px_rgba(0,0,0,0.25)] border border-border origin-right w-full rounded-md flex items-center"
|
||||
custom={index}
|
||||
variants={{
|
||||
initial: (index: number) => ({
|
||||
y: 0,
|
||||
scale: index === 4 ? 0.9 : 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
delay: 0.05,
|
||||
duration: 0.2,
|
||||
ease: cubicBezier(0.22, 1, 0.36, 1),
|
||||
},
|
||||
}),
|
||||
whileHover: (index: number) => ({
|
||||
y: -85,
|
||||
opacity: index === 4 ? 1 : 0.6,
|
||||
scale: index === 0 ? 0.85 : index === 4 ? 1.1 : 1,
|
||||
transition: {
|
||||
delay: 0.05,
|
||||
duration: 0.2,
|
||||
ease: cubicBezier(0.22, 1, 0.36, 1),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
damping: 40,
|
||||
stiffness: 600,
|
||||
}}
|
||||
>
|
||||
<div className="mr-3">{log.icon}</div>
|
||||
<div className="flex-grow">
|
||||
<p className="text-foreground text-xs font-medium">
|
||||
[{log.timestamp}] {log.type.toUpperCase()}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">{log.message}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1 px-5 pb-4 items-start w-full">
|
||||
<h2 className="font-semibold tracking-tight text-lg">
|
||||
Monitor agent activity
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Track and analyze your AI agent performance with detailed activity
|
||||
logs.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Card3 = () => {
|
||||
return (
|
||||
<div className="p-0 min-h-[500px] lg:min-h-fit overflow-hidden border-b lg:border-b-0 -z-0">
|
||||
<div className="relative flex flex-col gap-y-5 items-center justify-between h-full w-full">
|
||||
<div className="border-b items-center justify-center overflow-hidden rounded-t-xl h-4/5 w-full flex">
|
||||
<div className="relative flex items-center justify-center h-full w-full">
|
||||
<div className="absolute top-0 right-0 bottom-0 left-0 bg-[radial-gradient(circle,hsl(var(--accent)/0.3)_0%,transparent_100%)]"></div>
|
||||
<OrbitingCircles duration={15} delay={0} radius={40} reverse>
|
||||
<div className="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<HeadsetIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles duration={15} delay={20} radius={80}>
|
||||
<div className="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center">
|
||||
<SquareTerminal className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles radius={120} duration={20} delay={20}>
|
||||
<div className="h-8 w-8 rounded-full bg-purple-500 flex items-center justify-center">
|
||||
<UserSearch className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles radius={160} duration={40} delay={20}>
|
||||
<div className="h-8 w-8 rounded-full bg-yellow-500 flex items-center justify-center">
|
||||
<MessageSquareIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles radius={200} duration={30}>
|
||||
<div className="h-8 w-8 rounded-full bg-red-500 flex items-center justify-center">
|
||||
<GitForkIcon className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</OrbitingCircles>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-1 px-5 pb-4 items-start w-full">
|
||||
<h2 className="font-semibold tracking-tight text-lg">
|
||||
Build once, run anywhere
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create AI agents that work seamlessly across different platforms.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function UseCases() {
|
||||
return (
|
||||
<Section id="use-cases" title="Use Cases">
|
||||
<div className="grid lg:grid-cols-3 h-full border border-b-0">
|
||||
<Card1 />
|
||||
<Card2 />
|
||||
<Card3 />
|
||||
</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 }
|
50
src/components/ui/avatar.tsx
Normal file
50
src/components/ui/avatar.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
26
src/components/ui/border-number.tsx
Normal file
26
src/components/ui/border-number.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
interface BorderTextProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const BorderText = forwardRef<HTMLDivElement, BorderTextProps>(
|
||||
({ text, className, ...props }, ref) => {
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<span
|
||||
ref={ref}
|
||||
style={{ "--text": `'${text}'` } as React.CSSProperties}
|
||||
className={cn(
|
||||
`relative font-mono pointer-events-none text-center text-[6rem] font-bold leading-none before:bg-gradient-to-b before:from-neutral-300 before:to-neutral-200/70 dark:before:from-neutral-700/70 dark:before:to-neutral-800/30 before:to-80% before:bg-clip-text before:text-transparent before:content-[var(--text)] after:absolute after:inset-0 after:bg-neutral-400/70 dark:after:bg-neutral-600/70 after:bg-clip-text after:text-transparent after:mix-blend-darken dark:after:mix-blend-lighten after:content-[var(--text)] after:[text-shadow:0_1px_0_white] dark:after:[text-shadow:0_1px_0_black]`,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BorderText.displayName = "BorderText";
|
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 }
|
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,
|
||||
}
|
196
src/components/ui/flickering-grid.tsx
Normal file
196
src/components/ui/flickering-grid.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
"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 containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
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,";
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
||||
return `rgba(${r}, ${g}, ${b},`;
|
||||
};
|
||||
return toRGBA(color);
|
||||
}, [color]);
|
||||
|
||||
const setupCanvas = useCallback(
|
||||
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
const cols = Math.floor(width / (squareSize + gridGap));
|
||||
const rows = Math.floor(height / (squareSize + gridGap));
|
||||
|
||||
const squares = new Float32Array(cols * rows);
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
squares[i] = Math.random() * maxOpacity;
|
||||
}
|
||||
|
||||
return { cols, rows, squares, dpr };
|
||||
},
|
||||
[squareSize, gridGap, 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;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let gridParams: ReturnType<typeof setupCanvas>;
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
const newWidth = width || container.clientWidth;
|
||||
const newHeight = height || container.clientHeight;
|
||||
setCanvasSize({ width: newWidth, height: newHeight });
|
||||
gridParams = setupCanvas(canvas, newWidth, newHeight);
|
||||
};
|
||||
|
||||
updateCanvasSize();
|
||||
|
||||
let lastTime = 0;
|
||||
const animate = (time: number) => {
|
||||
if (!isInView) return;
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000;
|
||||
lastTime = time;
|
||||
|
||||
updateSquares(gridParams.squares, deltaTime);
|
||||
drawGrid(
|
||||
ctx,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
gridParams.cols,
|
||||
gridParams.rows,
|
||||
gridParams.squares,
|
||||
gridParams.dpr
|
||||
);
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(container);
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsInView(entry.isIntersecting);
|
||||
},
|
||||
{ threshold: 0 }
|
||||
);
|
||||
|
||||
intersectionObserver.observe(canvas);
|
||||
|
||||
if (isInView) {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resizeObserver.disconnect();
|
||||
intersectionObserver.disconnect();
|
||||
};
|
||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`w-full h-full ${className}`}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="pointer-events-none"
|
||||
style={{
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlickeringGrid;
|
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 }
|
51
src/components/ui/marquee.tsx
Normal file
51
src/components/ui/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/ui/orbiting-circles.tsx
Normal file
58
src/components/ui/orbiting-circles.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface OrbitingCirclesProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
reverse?: boolean;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
radius?: number;
|
||||
path?: boolean;
|
||||
}
|
||||
|
||||
export default function OrbitingCircles({
|
||||
className,
|
||||
children,
|
||||
reverse,
|
||||
duration = 20,
|
||||
delay = 10,
|
||||
radius = 50,
|
||||
path = true,
|
||||
}: OrbitingCirclesProps) {
|
||||
return (
|
||||
<>
|
||||
{path && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
className="pointer-events-none absolute inset-0 size-full"
|
||||
>
|
||||
<circle
|
||||
className="stroke-border stroke-1 dark:stroke-border"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
r={radius}
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--duration": duration,
|
||||
"--radius": radius,
|
||||
"--delay": -delay,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"absolute flex size-[2rem] transform-gpu animate-orbit items-center justify-center rounded-full border border-border bg-background [animation-delay:calc(var(--delay)*1000ms)] dark:bg-background",
|
||||
{ "[animation-direction:reverse]": reverse },
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
57
src/components/ui/ripple.tsx
Normal file
57
src/components/ui/ripple.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { CSSProperties, memo } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RippleProps {
|
||||
mainCircleSize?: number;
|
||||
mainCircleOpacity?: number;
|
||||
numCircles?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Ripple = memo(function Ripple({
|
||||
mainCircleSize = 210,
|
||||
mainCircleOpacity = 0.24,
|
||||
numCircles = 8,
|
||||
className,
|
||||
}: RippleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none select-none absolute inset-0 [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";
|
4
src/lib/animation.ts
Normal file
4
src/lib/animation.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { cubicBezier } from "framer-motion";
|
||||
|
||||
export const easeInOutCubic = cubicBezier(0.645, 0.045, 0.355, 1);
|
||||
export const easeOutCubic = cubicBezier(0, 0, 0.58, 1);
|
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"));
|
||||
}
|
265
src/lib/config.tsx
Normal file
265
src/lib/config.tsx
Normal file
@ -0,0 +1,265 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import {
|
||||
BrainIcon,
|
||||
CodeIcon,
|
||||
GlobeIcon,
|
||||
PlugIcon,
|
||||
UsersIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export const BLUR_FADE_DELAY = 0.15;
|
||||
|
||||
export const siteConfig = {
|
||||
name: "AI Agent SDK",
|
||||
description: "Create AI Agents with just a few lines of code.",
|
||||
cta: "Get Started",
|
||||
url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
||||
keywords: [
|
||||
"AI Agent SDK",
|
||||
"Multi-Agent Systems",
|
||||
"Tool Integration",
|
||||
"Workflow Automation",
|
||||
],
|
||||
links: {
|
||||
email: "support@aiagentsdk.com",
|
||||
twitter: "https://twitter.com/aiagentsdk",
|
||||
discord: "https://discord.gg/aiagentsdk",
|
||||
github: "https://github.com/aiagentsdk",
|
||||
instagram: "https://instagram.com/aiagentsdk",
|
||||
},
|
||||
hero: {
|
||||
title: "AI Agent SDK",
|
||||
description:
|
||||
"Create powerful AI agent workflows with just a few lines of code, enabling complex task automation and decision-making processes.",
|
||||
cta: "Get Started",
|
||||
ctaDescription: "Available for all major programming languages",
|
||||
},
|
||||
features: [
|
||||
{
|
||||
name: "Simple Agent Workflows",
|
||||
description:
|
||||
"Easily create and manage AI agent workflows with intuitive APIs.",
|
||||
icon: <BrainIcon className="h-6 w-6" />,
|
||||
},
|
||||
{
|
||||
name: "Multi-Agent Systems",
|
||||
description:
|
||||
"Build complex systems with multiple AI agents working together.",
|
||||
icon: <UsersIcon className="h-6 w-6" />,
|
||||
},
|
||||
{
|
||||
name: "Tool Integration",
|
||||
description:
|
||||
"Seamlessly integrate external tools and APIs into your agent workflows.",
|
||||
icon: <PlugIcon className="h-6 w-6" />,
|
||||
},
|
||||
{
|
||||
name: "Cross-Language Support",
|
||||
description:
|
||||
"Available in all major programming languages for maximum flexibility.",
|
||||
icon: <GlobeIcon className="h-6 w-6" />,
|
||||
},
|
||||
{
|
||||
name: "Customizable Agents",
|
||||
description:
|
||||
"Design and customize agents to fit your specific use case and requirements.",
|
||||
icon: <CodeIcon className="h-6 w-6" />,
|
||||
},
|
||||
{
|
||||
name: "Efficient Execution",
|
||||
description:
|
||||
"Optimize agent performance with built-in efficiency and scalability features.",
|
||||
icon: <ZapIcon className="h-6 w-6" />,
|
||||
},
|
||||
],
|
||||
pricing: [
|
||||
{
|
||||
name: "Basic",
|
||||
price: { monthly: "$9", yearly: "$99" },
|
||||
frequency: { monthly: "month", yearly: "year" },
|
||||
description: "Perfect for individuals and small projects.",
|
||||
features: [
|
||||
"100 AI generations per month",
|
||||
"Basic text-to-image conversion",
|
||||
"Email support",
|
||||
"Access to community forum",
|
||||
],
|
||||
cta: "Get Started",
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
price: { monthly: "$29", yearly: "$290" },
|
||||
frequency: { monthly: "month", yearly: "year" },
|
||||
description: "Ideal for professionals and growing businesses.",
|
||||
features: [
|
||||
"1000 AI generations per month",
|
||||
"Advanced text-to-image conversion",
|
||||
"Priority email support",
|
||||
"API access",
|
||||
"Custom AI model fine-tuning",
|
||||
"Collaboration tools",
|
||||
],
|
||||
cta: "Get Started",
|
||||
},
|
||||
{
|
||||
name: "Enterprise",
|
||||
price: { monthly: "$999", yearly: "Custom" },
|
||||
frequency: { monthly: "month", yearly: "year" },
|
||||
description: "Tailored solutions for large organizations.",
|
||||
features: [
|
||||
"Unlimited AI generations",
|
||||
"Dedicated account manager",
|
||||
"24/7 phone and email support",
|
||||
"Custom AI model development",
|
||||
"On-premises deployment option",
|
||||
"Advanced analytics and reporting",
|
||||
],
|
||||
popular: true,
|
||||
cta: "Get Started",
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
socialLinks: [
|
||||
{
|
||||
icon: <Icons.github className="h-5 w-5" />,
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
icon: <Icons.twitter className="h-5 w-5" />,
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{ text: "Pricing", url: "#" },
|
||||
{ text: "Contact", url: "#" },
|
||||
],
|
||||
bottomText: "All rights reserved.",
|
||||
brandText: "AGENT SDK",
|
||||
},
|
||||
|
||||
testimonials: [
|
||||
{
|
||||
id: 1,
|
||||
text: "The AI Agent SDK has revolutionized how we build intelligent systems. It's incredibly intuitive and powerful.",
|
||||
name: "Alice Johnson",
|
||||
company: "OpenMind Labs",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: "We've significantly reduced development time for our AI projects using this SDK. The multi-agent feature is a game-changer.",
|
||||
name: "Bob Brown",
|
||||
company: "NeuralForge",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: "The cross-language support allowed us to seamlessly integrate AI agents into our existing tech stack.",
|
||||
name: "Charlie Davis",
|
||||
company: "CodeHarbor",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
text: "The AI Agent SDK's tool integration feature has streamlined our workflow automation processes.",
|
||||
name: "Diana Evans",
|
||||
company: "AutomateX",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
text: "The customizable agent behaviors have allowed us to create highly specialized AI solutions for our clients.",
|
||||
name: "Ethan Ford",
|
||||
company: "AICore",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
text: "The AI Agent SDK's efficiency features have significantly improved our system's performance and scalability.",
|
||||
name: "Fiona Grant",
|
||||
company: "ScaleAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDB8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
text: "The SDK's intuitive APIs have made it easy for our team to quickly prototype and deploy AI agent systems.",
|
||||
name: "George Harris",
|
||||
company: "RapidAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDR8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
text: "The AI Agent SDK's multi-agent system has enabled us to build complex, collaborative AI solutions with ease.",
|
||||
name: "Hannah Irving",
|
||||
company: "CollabAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NTJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
text: "The SDK's flexibility in integrating external tools has expanded our AI agents' capabilities tremendously.",
|
||||
name: "Ian Johnson",
|
||||
company: "FlexAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NTZ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
text: "The AI Agent SDK's documentation and support have made our learning curve much smoother.",
|
||||
name: "Julia Kim",
|
||||
company: "DevAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjR8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
text: "We've seen a significant boost in our AI's decision-making capabilities thanks to the AI Agent SDK.",
|
||||
name: "Kevin Lee",
|
||||
company: "DecisionTech",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NzB8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
text: "The SDK's multi-agent system has revolutionized our approach to complex problem-solving.",
|
||||
name: "Laura Martinez",
|
||||
company: "SolveX",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NzZ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
text: "The customization options in the AI Agent SDK have allowed us to create truly unique AI solutions.",
|
||||
name: "Michael Chen",
|
||||
company: "UniqueAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8ODJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
text: "The efficiency of the AI Agent SDK has significantly reduced our development time and costs.",
|
||||
name: "Natalie Wong",
|
||||
company: "FastTrackAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8ODh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
text: "The cross-language support has made it easy for our diverse team to collaborate on AI projects.",
|
||||
name: "Oliver Smith",
|
||||
company: "GlobalAI",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OTR8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
14
src/lib/fonts.ts
Normal file
14
src/lib/fonts.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {
|
||||
JetBrains_Mono as FontMono,
|
||||
Inter as FontSans,
|
||||
} from "next/font/google";
|
||||
|
||||
export const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const fontMono = FontMono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
});
|
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)`;
|
||||
}
|
||||
}
|
158
tailwind.config.ts
Normal file
158
tailwind.config.ts
Normal file
@ -0,0 +1,158 @@
|
||||
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: "1rem",
|
||||
screens: {
|
||||
"2xl": "1000px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["var(--font-geist-sans)"],
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
},
|
||||
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",
|
||||
orbit: "orbit calc(var(--duration)*1s) linear 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)",
|
||||
},
|
||||
},
|
||||
orbit: {
|
||||
"0%": {
|
||||
transform:
|
||||
"rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
|
||||
},
|
||||
"100%": {
|
||||
transform:
|
||||
"rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
|
||||
},
|
||||
},
|
||||
},
|
||||
backgroundSize: {
|
||||
"grid-1": "100% 100%",
|
||||
"grid-2": "50% 100%",
|
||||
"grid-3": "calc(100%/3) 100%",
|
||||
"grid-4": "25% 100%",
|
||||
"grid-5": "20% 100%",
|
||||
"grid-6": "calc(100%/6) 100%",
|
||||
},
|
||||
backgroundImage: {
|
||||
"grid-1":
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px)",
|
||||
"grid-2":
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px)",
|
||||
"grid-3":
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px)",
|
||||
"grid-4":
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px)",
|
||||
"grid-5":
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px)",
|
||||
"grid-6":
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
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