init project

This commit is contained in:
ErkiKadhafi 2025-01-13 07:08:59 +07:00
commit 13bc112ad3
79 changed files with 11240 additions and 0 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
NEXT_PUBLIC_APP_URL=https://www.acme.ai

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

17
components.json Normal file
View 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"
}
}

View File

@ -0,0 +1,53 @@
---
title: Introducing Acme.ai
publishedAt: "2024-08-29"
summary: Introducing Acme.ai, a cutting-edge AI solution for modern businesses.
author: "dillionverma"
image: "/introducing.png"
---
We're excited to unveil **Acme.ai**, an innovative AI-powered platform designed to transform your business operations and skyrocket productivity. 🚀
## The Challenge We're Addressing
In today's AI-driven world, businesses face several hurdles:
- Overwhelming data analysis
- Inefficient decision-making processes
- Difficulty in predicting market trends
Acme.ai tackles these challenges head-on, offering a sophisticated AI solution that simplifies complex business processes.
## Our Mission
1. **Accelerate Decision-Making**: By leveraging AI to analyze vast datasets, we help you make informed decisions faster.
2. **Enhance Forecasting**: Our advanced predictive models provide accurate insights into future trends.
3. **Optimize Operations**: With AI-driven recommendations, streamline your business processes effortlessly.
## Core Capabilities
- **AI-Powered Dashboard**: Get real-time, AI-interpreted insights at a glance
- **Predictive Analytics**: Forecast trends and make data-driven decisions
- **Natural Language Processing**: Interact with your data using simple language queries
- **Automated Reporting**: Generate comprehensive reports with a single click
- **Customizable AI Models**: Tailor the AI to your specific industry needs
## Why Acme.ai Stands Out
> "Acme.ai has revolutionized our strategic planning. It's like having a crystal ball for our business!" - John Smith, CFO of FutureTech
Our AI solution isn't just a tool; it's your competitive edge. Here's how we compare:
| Feature | Acme.ai | Traditional BI Tools |
| ------------------------ | ------- | -------------------- |
| AI-Powered Insights | ✅ | ❌ |
| Predictive Capabilities | ✅ | ❌ |
| Natural Language Queries | ✅ | ❌ |
## Embarking on Your AI Journey
Getting started with Acme.ai is seamless:
1. Sign up for a demo
2. Integrate your data sources
3. Start unlocking AI-driven insights

8
next.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ hostname: "localhost" }, { hostname: "randomuser.me" }],
},
};
export default nextConfig;

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "saas-template-magicui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.1.7",
"framer-motion": "^11.3.21",
"lucide-react": "^0.417.0",
"next": "14.2.7",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
"recharts": "^2.12.7",
"rehype-pretty-code": "^0.13.2",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"vaul": "^0.9.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.7",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

5267
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/author.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
public/introducing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
public/og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

11
src/app/(auth)/layout.tsx Normal file
View File

@ -0,0 +1,11 @@
interface MarketingLayoutProps {
children: React.ReactNode;
}
export default async function Layout({ children }: MarketingLayoutProps) {
return (
<main className="flex flex-col items-center justify-center h-screen">
{children}
</main>
);
}

View File

@ -0,0 +1,76 @@
import Link from "next/link";
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function LoginForm() {
return (
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<Button variant="outline" className="w-full">
<Icons.google className="w-4 h-4 mr-2" />
Login with Google
</Button>
<Button variant="outline" className="w-full">
<Icons.github className="w-4 h-4 mr-2" />
Login with GitHub
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link href="#" className="ml-auto inline-block text-sm underline">
Forgot your password?
</Link>
</div>
<Input id="password" type="password" required />
</div>
<Button type="submit" className="w-full">
Login
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<Link href="/signup" className="underline">
Sign up
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,64 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function LoginForm() {
return (
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-xl">Sign Up</CardTitle>
<CardDescription>
Enter your information to create an account
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="first-name">First name</Label>
<Input id="first-name" placeholder="Max" required />
</div>
<div className="grid gap-2">
<Label htmlFor="last-name">Last name</Label>
<Input id="last-name" placeholder="Robinson" required />
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
<Button type="submit" className="w-full">
Create an account
</Button>
<Button variant="outline" className="w-full">
Sign up with GitHub
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{" "}
<Link href="/login" className="underline">
Sign in
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,135 @@
import Author from "@/components/blog-author";
import CtaSection from "@/components/sections/cta";
import { getPost } from "@/lib/blog";
import { siteConfig } from "@/lib/config";
import { formatDate } from "@/lib/utils";
import type { Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
import { Suspense } from "react";
export async function generateMetadata({
params,
}: {
params: {
slug: string;
};
}): Promise<Metadata | undefined> {
let post = await getPost(params.slug);
let {
title,
publishedAt: publishedTime,
summary: description,
image,
} = post.metadata;
return {
title,
description,
openGraph: {
title,
description,
type: "article",
publishedTime,
url: `${siteConfig.url}/blog/${post.slug}`,
images: [
{
url: image,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [image],
},
};
}
export default async function Blog({
params,
}: {
params: {
slug: string;
};
}) {
let post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<section id="blog">
<script
type="application/ld+json"
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.metadata.title,
datePublished: post.metadata.publishedAt,
dateModified: post.metadata.publishedAt,
description: post.metadata.summary,
image: post.metadata.image
? `${siteConfig.url}${post.metadata.image}`
: `${siteConfig.url}/blog/${post.slug}/opengraph-image`,
url: `${siteConfig.url}/blog/${post.slug}`,
author: {
"@type": "Person",
name: siteConfig.name,
},
}),
}}
/>
<div className="mx-auto w-full max-w-[800px] px-4 sm:px-6 lg:px-8 space-y-4 my-12">
<Suspense
fallback={
<div className="mb-8 w-full h-64 bg-gray-200 animate-pulse rounded-lg"></div>
}
>
{post.metadata.image && (
<div className="mb-8">
<Image
width={1920}
height={1080}
src={post.metadata.image}
alt={post.metadata.title}
className="w-full h-auto rounded-lg border shadow-md"
/>
</div>
)}
</Suspense>
<div className="flex flex-col">
<h1 className="title font-medium text-3xl tracking-tighter">
{post.metadata.title}
</h1>
</div>
<div className="flex justify-between items-center text-sm">
<Suspense fallback={<p className="h-5" />}>
<div className="flex items-center space-x-2">
<time
dateTime={post.metadata.publishedAt}
className="text-sm text-gray-500"
>
{formatDate(post.metadata.publishedAt)}
</time>
</div>
</Suspense>
</div>
<div className="flex items-center space-x-2">
<Author
twitterUsername={post.metadata.author}
name={post.metadata.author}
image={"/author.jpg"}
/>
</div>
<article
className="prose dark:prose-invert mx-auto max-w-full"
dangerouslySetInnerHTML={{ __html: post.source }}
></article>
</div>
<CtaSection />
</section>
);
}

View 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 />
</>
);
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

95
src/app/globals.css Normal file
View File

@ -0,0 +1,95 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 349 100% 55.5%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 349 100% 55.5%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 349 100% 55.5%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 349 100% 55.5%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 349 100% 55.5%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 349 100% 55.5%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
:root {
--chart-1: 349 100% 55.5%;
--chart-2: 0 0 90;
--chart-3: 0 0 83;
--chart-4: 0 0 64;
--chart-5: 27 87% 67%;
font-family: Inter, sans-serif;
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "salt";
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
* {
@apply border-border;
}
@supports (font-variation-settings: normal) {
:root {
font-family: InterVariable, sans-serif;
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "salt";
}
}
body {
@apply bg-background text-foreground;
}
}

46
src/app/layout.tsx Normal file
View File

@ -0,0 +1,46 @@
import { TailwindIndicator } from "@/components/tailwind-indicator";
import { ThemeProvider } from "@/components/theme-provider";
import { ThemeToggle } from "@/components/theme-toggle";
import { cn, constructMetadata } from "@/lib/utils";
import type { Metadata, Viewport } from "next";
import "./globals.css";
export const metadata: Metadata = constructMetadata({});
export const viewport: Viewport = {
colorScheme: "light",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
</head>
<body
className={cn(
"min-h-screen bg-background antialiased w-full mx-auto scroll-smooth"
)}
>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem={false}
>
{children}
<ThemeToggle />
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
);
}

105
src/app/og/route.tsx Normal file
View File

@ -0,0 +1,105 @@
import { Icons } from "@/components/icons";
import { siteConfig } from "@/lib/config";
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
export async function GET(req: NextRequest) {
const { searchParams } = req.nextUrl;
const postTitle = searchParams.get("title") || siteConfig.description;
const font = fetch(
new URL("../../assets/fonts/Inter-SemiBold.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
const fontData = await font;
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#fff",
// set background image if needed
backgroundImage: `url(${siteConfig.url}/og.png)`,
fontSize: 32,
fontWeight: 600,
}}
>
<div
style={{
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
top: "125px",
}}
>
<Icons.logo
style={{
width: "64px",
height: "64px",
}}
/>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "64px",
fontWeight: "600",
marginTop: "24px",
textAlign: "center",
width: "80%",
letterSpacing: "-0.05em", // Added tighter tracking
}}
>
{postTitle}
</div>
<div
style={{
display: "flex",
fontSize: "16px",
fontWeight: "500",
marginTop: "16px",
color: "#808080",
}}
>
{siteConfig.name}
</div>
</div>
<img
src={`${siteConfig.url}/dashboard.png`}
width={900}
style={{
position: "relative",
bottom: -160,
aspectRatio: "auto",
border: "4px solid lightgray",
background: "lightgray",
borderRadius: 20,
zIndex: 1,
}}
/>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Inter",
data: fontData,
style: "normal",
},
],
}
);
}

35
src/app/page.tsx Normal file
View File

@ -0,0 +1,35 @@
import Blog from "@/components/sections/blog";
import CTA from "@/components/sections/cta";
import FAQ from "@/components/sections/faq";
import Features from "@/components/sections/features";
import Footer from "@/components/sections/footer";
import Header from "@/components/sections/header";
import Hero from "@/components/sections/hero";
import HowItWorks from "@/components/sections/how-it-works";
import Logos from "@/components/sections/logos";
import Pricing from "@/components/sections/pricing";
import Problem from "@/components/sections/problem";
import Solution from "@/components/sections/solution";
import Testimonials from "@/components/sections/testimonials";
import TestimonialsCarousel from "@/components/sections/testimonials-carousel";
export default function Home() {
return (
<main>
<Header />
<Hero />
<Logos />
<Problem />
<Solution />
<HowItWorks />
<TestimonialsCarousel />
<Features />
<Testimonials />
<Pricing />
<FAQ />
<Blog />
<CTA />
<Footer />
</main>
);
}

21
src/app/sitemap.ts Normal file
View File

@ -0,0 +1,21 @@
import { getBlogPosts } from "@/lib/blog";
import { MetadataRoute } from "next";
import { headers } from "next/headers";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const allPosts = await getBlogPosts();
const headersList = headers();
let domain = headersList.get("host") as string;
let protocol = "https";
return [
{
url: `${protocol}://${domain}`,
lastModified: new Date(),
},
...allPosts.map((post) => ({
url: `${protocol}://${domain}/blog/${post.slug}`,
lastModified: new Date(),
})),
];
}

Binary file not shown.

View File

@ -0,0 +1,31 @@
import { cn } from "@/lib/utils";
import { HTMLAttributes } from "react";
export interface AvatarCirclesProps extends HTMLAttributes<HTMLDivElement> {
numPeople?: number;
avatarUrls: string[];
}
export default function AvatarCircles({
numPeople,
avatarUrls,
className,
}: AvatarCirclesProps) {
return (
<div className={cn("z-10 flex -space-x-4 rtl:space-x-reverse", className)}>
{avatarUrls.map((url, index) => (
<img
key={index}
className="h-10 w-10 rounded-full border-2 border-white dark:border-gray-800"
src={url}
width={40}
height={40}
alt={`Avatar ${index + 1}`}
/>
))}
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-white bg-black text-center text-xs font-medium text-white dark:border-gray-800 dark:bg-white dark:text-black">
+{numPeople}
</div>
</div>
);
}

View File

@ -0,0 +1,73 @@
import { formatDate } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
export default function Author({
name,
image,
twitterUsername,
updatedAt,
imageOnly,
}: {
name: string;
image: string;
twitterUsername: string;
updatedAt?: string;
imageOnly?: boolean;
}) {
if (imageOnly) {
return (
<Image
src={image}
alt={name}
width={36}
height={36}
className="rounded-full transition-all group-hover:brightness-90"
/>
);
}
if (updatedAt) {
return (
<div className="flex items-center space-x-3">
<Image
src={image}
alt={name}
width={36}
height={36}
className="rounded-full"
/>
<div className="flex flex-col">
<p className="text-sm text-gray-500">Written by {name}</p>
<time
dateTime={updatedAt}
className="text-sm font-light text-gray-400"
>
Last updated {formatDate(updatedAt)}
</time>
</div>
</div>
);
}
return (
<Link
href={`https://twitter.com/${twitterUsername}`}
className="group flex items-center space-x-3"
target="_blank"
rel="noopener noreferrer"
>
<Image
src={image}
alt={name}
width={40}
height={40}
className="rounded-full transition-all group-hover:brightness-90"
/>
<div className="flex flex-col">
<p className="font-semibold text-gray-700">{name}</p>
<p className="text-sm text-gray-500">@{twitterUsername}</p>
</div>
</Link>
);
}

View File

@ -0,0 +1,40 @@
import { Post } from "@/lib/blog";
import { formatDate } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
export default function BlogCard({
data,
priority,
}: {
data: Post;
priority?: boolean;
}) {
return (
<Link href={`/blog/${data.slug}`} className="block">
<div className="bg-background rounded-lg p-4 mb-4 border hover:shadow-sm transition-shadow duration-200">
{data.image && (
<Image
className="rounded-t-lg object-cover border"
src={data.image}
width={1200}
height={630}
alt={data.title}
priority={priority}
/>
)}
{!data.image && <div className="bg-gray-200 h-[180px] mb-4 rounded" />}
<p className="mb-2">
<time
dateTime={data.publishedAt}
className="text-sm text-muted-foreground"
>
{formatDate(data.publishedAt)}
</time>
</p>
<h3 className="text-xl font-semibold mb-2">{data.title}</h3>
<p className="text-foreground mb-4">{data.summary}</p>
</div>
</Link>
);
}

70
src/components/drawer.tsx Normal file
View File

@ -0,0 +1,70 @@
import { Icons } from "@/components/icons";
import { buttonVariants } from "@/components/ui/button";
import {
Drawer,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTrigger,
} from "@/components/ui/drawer";
import { siteConfig } from "@/lib/config";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { IoMenuSharp } from "react-icons/io5";
export default function drawerDemo() {
return (
<Drawer>
<DrawerTrigger>
<IoMenuSharp className="text-2xl" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="px-6">
<div className="">
<Link
href="/"
title="brand-logo"
className="relative mr-6 flex items-center space-x-2"
>
<Icons.logo className="w-auto h-[40px]" />
<span className="font-bold text-xl">{siteConfig.name}</span>
</Link>
</div>
<nav>
<ul className="mt-7 text-left">
{siteConfig.header.map((item, index) => (
<li key={index} className="my-3">
{item.trigger ? (
<span className="font-semibold">{item.trigger}</span>
) : (
<Link href={item.href || ""} className="font-semibold">
{item.label}
</Link>
)}
</li>
))}
</ul>
</nav>
</DrawerHeader>
<DrawerFooter>
<Link
href="/login"
className={buttonVariants({ variant: "outline" })}
>
Login
</Link>
<Link
href="/signup"
className={cn(
buttonVariants({ variant: "default" }),
"w-full sm:w-auto text-background flex gap-2"
)}
>
<Icons.logo className="h-6 w-6" />
Get Started for Free
</Link>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}

View File

@ -0,0 +1,342 @@
"use client";
import { BorderBeam } from "@/components/magicui/border-beam";
import * as Accordion from "@radix-ui/react-accordion";
import { motion, useInView } from "framer-motion";
import React, {
forwardRef,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
type AccordionItemProps = {
children: React.ReactNode;
className?: string;
} & Accordion.AccordionItemProps;
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
({ children, className, ...props }, forwardedRef) => (
<Accordion.Item
className={cn(
"mt-px focus-within:relative focus-within:z-10",
className
)}
{...props}
ref={forwardedRef}
>
{children}
</Accordion.Item>
)
);
AccordionItem.displayName = "AccordionItem";
type AccordionTriggerProps = {
children: React.ReactNode;
className?: string;
};
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
({ children, className, ...props }, forwardedRef) => (
<Accordion.Header className="">
<Accordion.Trigger
className={cn("", className)}
{...props}
ref={forwardedRef}
>
{children}
</Accordion.Trigger>
</Accordion.Header>
)
);
AccordionTrigger.displayName = "AccordionTrigger";
type AccordionContentProps = {
children: ReactNode;
className?: string;
} & Accordion.AccordionContentProps;
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
({ children, className, ...props }, forwardedRef) => (
<Accordion.Content
className={cn(
"data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down",
className
)}
{...props}
ref={forwardedRef}
>
<div className="px-5 py-2">{children}</div>
</Accordion.Content>
)
);
AccordionContent.displayName = "AccordionContent";
type CardDataProps = {
id: number;
title: string;
content: string;
image?: string;
video?: string;
icon?: React.ReactNode;
};
export type FeaturesProps = {
collapseDelay?: number;
ltr?: boolean;
linePosition?: "left" | "right" | "top" | "bottom";
data: CardDataProps[];
};
export default function Features({
collapseDelay = 5000,
ltr = false,
linePosition = "left",
data = [],
}: FeaturesProps) {
const [currentIndex, setCurrentIndex] = useState<number>(-1);
const carouselRef = useRef<HTMLUListElement>(null);
const ref = useRef(null);
const isInView = useInView(ref, {
once: true,
amount: 0.5,
});
useEffect(() => {
const timer = setTimeout(() => {
if (isInView) {
setCurrentIndex(0);
} else {
setCurrentIndex(-1);
}
}, 100);
return () => clearTimeout(timer);
}, [isInView]);
const scrollToIndex = (index: number) => {
if (carouselRef.current) {
const card = carouselRef.current.querySelectorAll(".card")[index];
if (card) {
const cardRect = card.getBoundingClientRect();
const carouselRect = carouselRef.current.getBoundingClientRect();
const offset =
cardRect.left -
carouselRect.left -
(carouselRect.width - cardRect.width) / 2;
carouselRef.current.scrollTo({
left: carouselRef.current.scrollLeft + offset,
behavior: "smooth",
});
}
}
};
useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prevIndex) =>
prevIndex !== undefined ? (prevIndex + 1) % data.length : 0
);
}, collapseDelay);
return () => clearInterval(timer);
}, [currentIndex]);
useEffect(() => {
const handleAutoScroll = () => {
const nextIndex =
(currentIndex !== undefined ? currentIndex + 1 : 0) % data.length;
scrollToIndex(nextIndex);
};
const autoScrollTimer = setInterval(handleAutoScroll, collapseDelay);
return () => clearInterval(autoScrollTimer);
}, [currentIndex]);
useEffect(() => {
const carousel = carouselRef.current;
if (carousel) {
const handleScroll = () => {
const scrollLeft = carousel.scrollLeft;
const cardWidth = carousel.querySelector(".card")?.clientWidth || 0;
const newIndex = Math.min(
Math.floor(scrollLeft / cardWidth),
data.length - 1
);
setCurrentIndex(newIndex);
};
carousel.addEventListener("scroll", handleScroll);
return () => carousel.removeEventListener("scroll", handleScroll);
}
}, []);
return (
<section ref={ref} id="features">
<div className="container">
<div className="max-w-6xl mx-auto ">
<div className="">
<div
className={`hidden md:flex order-1 md:order-[0] ${
ltr ? "md:order-2 md:justify-end" : "justify-start"
}`}
>
<Accordion.Root
className="grid md:grid-cols-4 gap-x-10 py-8"
type="single"
defaultValue={`item-${currentIndex}`}
value={`item-${currentIndex}`}
onValueChange={(value) =>
setCurrentIndex(Number(value.split("-")[1]))
}
>
{data.map((item, index) => (
<AccordionItem
key={item.id}
className="relative mb-8"
value={`item-${index}`}
>
{linePosition === "left" || linePosition === "right" ? (
<div
className={`absolute bottom-0 top-0 h-full w-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
linePosition === "right"
? "left-auto right-0"
: "left-0 right-auto"
}`}
>
<div
className={`absolute left-0 top-0 w-full ${
currentIndex === index ? "h-full" : "h-0"
} origin-top bg-primary transition-all ease-linear dark:bg-white`}
style={{
transitionDuration:
currentIndex === index
? `${collapseDelay}ms`
: "0s",
}}
></div>
</div>
) : null}
{linePosition === "top" || linePosition === "bottom" ? (
<div
className={`absolute left-0 right-0 w-full h-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
linePosition === "bottom" ? "bottom-0" : "top-0"
}`}
>
<div
className={`absolute left-0 ${
linePosition === "bottom" ? "bottom-0" : "top-0"
} h-full ${
currentIndex === index ? "w-full" : "w-0"
} origin-left bg-primary transition-all ease-linear dark:bg-white`}
style={{
transitionDuration:
currentIndex === index
? `${collapseDelay}ms`
: "0s",
}}
></div>
</div>
) : null}
<AccordionTrigger>
<div className="flex items-center relative flex-col">
<div className="item-box size-16 bg-primary/10 rounded-full sm:mx-6 mx-2 shrink-0 flex items-center justify-center">
{item.icon}
</div>
<div className="font-bold text-xl my-3 ">
{item.title}
</div>
<div className="justify-center text-center mb-4">
{item.content}
</div>
</div>
</AccordionTrigger>
</AccordionItem>
))}
</Accordion.Root>
</div>
<div
className={`w-auto overflow-hidden relative rounded-lg ${
ltr && "md:order-1"
}`}
>
{data[currentIndex]?.image ? (
<motion.img
key={currentIndex}
src={data[currentIndex].image}
alt="feature"
className="aspect-auto h-full w-full object-cover relative border rounded-lg shadow-lg"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.25, ease: "easeOut" }}
/>
) : data[currentIndex]?.video ? (
<video
preload="auto"
src={data[currentIndex].video}
className="aspect-auto h-full w-full rounded-lg object-cover border shadow-lg"
autoPlay
loop
muted
/>
) : (
<div className="aspect-auto h-full w-full rounded-xl border border-neutral-300/50 bg-gray-200 p-1 min-h-[600px]"></div>
)}
<BorderBeam
size={400}
duration={12}
delay={9}
borderWidth={1.5}
colorFrom="hsl(var(--primary))"
colorTo="hsl(var(--primary)/0)"
/>
</div>
<ul
ref={carouselRef}
className="flex h-full snap-x flex-nowrap overflow-x-auto py-10 [-ms-overflow-style:none] [-webkit-mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [scrollbar-width:none] md:hidden [&::-webkit-scrollbar]:hidden snap-mandatory"
style={{
padding: "50px calc(50%)",
}}
>
{data.map((item, index) => (
<div
key={item.id}
className="card relative mr-8 grid h-full max-w-60 shrink-0 items-start justify-center py-4 last:mr-0"
onClick={() => setCurrentIndex(index)}
style={{
scrollSnapAlign: "center",
}}
>
<div className="absolute bottom-0 left-0 right-auto top-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30">
<div
className={`absolute left-0 top-0 h-full ${
currentIndex === index ? "w-full" : "w-0"
} origin-top bg-primary transition-all ease-linear dark:bg-white`}
style={{
transitionDuration:
currentIndex === index ? `${collapseDelay}ms` : "0s",
}}
></div>
</div>
<h2 className="text-xl font-bold">{item.title}</h2>
<p className="mx-0 max-w-sm text-balance text-sm">
{item.content}
</p>
</div>
))}
</ul>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,337 @@
"use client";
import * as Accordion from "@radix-ui/react-accordion";
import { motion, useInView } from "framer-motion";
import React, {
forwardRef,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
type AccordionItemProps = {
children: React.ReactNode;
className?: string;
} & Accordion.AccordionItemProps;
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
({ children, className, ...props }, forwardedRef) => (
<Accordion.Item
className={cn(
"mt-px overflow-hidden focus-within:relative focus-within:z-10",
className
)}
{...props}
ref={forwardedRef}
>
{children}
</Accordion.Item>
)
);
AccordionItem.displayName = "AccordionItem";
type AccordionTriggerProps = {
children: React.ReactNode;
className?: string;
};
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
({ children, className, ...props }, forwardedRef) => (
<Accordion.Header className="flex">
<Accordion.Trigger
className={cn(
"group flex flex-1 cursor-pointer items-center justify-between px-5 text-[15px] leading-none outline-none",
className
)}
{...props}
ref={forwardedRef}
>
{children}
</Accordion.Trigger>
</Accordion.Header>
)
);
AccordionTrigger.displayName = "AccordionTrigger";
type AccordionContentProps = {
children: ReactNode;
className?: string;
} & Accordion.AccordionContentProps;
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
({ children, className, ...props }, forwardedRef) => (
<Accordion.Content
className={cn(
"overflow-hidden text-[15px] font-medium data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down",
className
)}
{...props}
ref={forwardedRef}
>
<div className="px-5 py-2">{children}</div>
</Accordion.Content>
)
);
AccordionContent.displayName = "AccordionContent";
export type FeaturesDataProps = {
id: number;
title: string;
content: string;
image?: string;
video?: string;
icon?: React.ReactNode;
};
export type FeaturesProps = {
collapseDelay?: number;
ltr?: boolean;
linePosition?: "left" | "right" | "top" | "bottom";
data: FeaturesDataProps[];
};
export default function Features({
collapseDelay = 5000,
ltr = false,
linePosition = "left",
data = [],
}: FeaturesProps) {
const [currentIndex, setCurrentIndex] = useState<number>(-1);
const carouselRef = useRef<HTMLUListElement>(null);
const ref = useRef(null);
const isInView = useInView(ref, {
once: true,
amount: 0.5,
});
useEffect(() => {
const timer = setTimeout(() => {
if (isInView) {
setCurrentIndex(0);
} else {
setCurrentIndex(-1);
}
}, 100);
return () => clearTimeout(timer);
}, [isInView]);
const scrollToIndex = (index: number) => {
if (carouselRef.current) {
const card = carouselRef.current.querySelectorAll(".card")[index];
if (card) {
const cardRect = card.getBoundingClientRect();
const carouselRect = carouselRef.current.getBoundingClientRect();
const offset =
cardRect.left -
carouselRect.left -
(carouselRect.width - cardRect.width) / 2;
carouselRef.current.scrollTo({
left: carouselRef.current.scrollLeft + offset,
behavior: "smooth",
});
}
}
};
useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prevIndex) =>
prevIndex !== undefined ? (prevIndex + 1) % data.length : 0
);
}, collapseDelay);
return () => clearInterval(timer);
}, [collapseDelay, currentIndex, data.length]);
useEffect(() => {
const handleAutoScroll = () => {
const nextIndex =
(currentIndex !== undefined ? currentIndex + 1 : 0) % data.length;
scrollToIndex(nextIndex);
};
const autoScrollTimer = setInterval(handleAutoScroll, collapseDelay);
return () => clearInterval(autoScrollTimer);
}, [collapseDelay, currentIndex, data.length]);
useEffect(() => {
const carousel = carouselRef.current;
if (carousel) {
const handleScroll = () => {
const scrollLeft = carousel.scrollLeft;
const cardWidth = carousel.querySelector(".card")?.clientWidth || 0;
const newIndex = Math.min(
Math.floor(scrollLeft / cardWidth),
data.length - 1
);
setCurrentIndex(newIndex);
};
carousel.addEventListener("scroll", handleScroll);
return () => carousel.removeEventListener("scroll", handleScroll);
}
}, [data.length]);
return (
<section ref={ref} id="features">
<div className="container">
<div className="max-w-6xl mx-auto">
<div className="mx-auto my-12 h-full grid lg:grid-cols-2 gap-10 items-center">
<div
className={` hidden lg:flex order-1 lg:order-[0] ${
ltr ? "lg:order-2 lg:justify-end" : "justify-start"
}`}
>
<Accordion.Root
className=""
type="single"
defaultValue={`item-${currentIndex}`}
value={`item-${currentIndex}`}
onValueChange={(value) =>
setCurrentIndex(Number(value.split("-")[1]))
}
>
{data.map((item, index) => (
<AccordionItem
key={item.id}
className="relative mb-8 last:mb-0"
value={`item-${index}`}
>
{linePosition === "left" || linePosition === "right" ? (
<div
className={`absolute bottom-0 top-0 h-full w-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
linePosition === "right"
? "left-auto right-0"
: "left-0 right-auto"
}`}
>
<div
className={`absolute left-0 top-0 w-full ${
currentIndex === index ? "h-full" : "h-0"
} origin-top bg-primary transition-all ease-linear dark:bg-white`}
style={{
transitionDuration:
currentIndex === index
? `${collapseDelay}ms`
: "0s",
}}
></div>
</div>
) : null}
{linePosition === "top" || linePosition === "bottom" ? (
<div
className={`absolute left-0 right-0 w-full h-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
linePosition === "bottom" ? "bottom-0" : "top-0"
}`}
>
<div
className={`absolute left-0 ${
linePosition === "bottom" ? "bottom-0" : "top-0"
} h-full ${
currentIndex === index ? "w-full" : "w-0"
} origin-left bg-primary transition-all ease-linear dark:bg-white`}
style={{
transitionDuration:
currentIndex === index
? `${collapseDelay}ms`
: "0s",
}}
></div>
</div>
) : null}
<div className="flex items-center relative">
<div className="item-box w-12 h-12 bg-primary/10 rounded-full sm:mx-6 mx-2 shrink-0 flex items-center justify-center">
{item.icon}
</div>
<div>
<AccordionTrigger className="text-xl font-bold pl-0">
{item.title}
</AccordionTrigger>
<AccordionTrigger className="justify-start text-left leading-4 text-[16px] pl-0">
{item.content}
</AccordionTrigger>
</div>
</div>
</AccordionItem>
))}
</Accordion.Root>
</div>
<div
className={`h-[350px] min-h-[200px] w-auto ${
ltr && "lg:order-1"
}`}
>
{data[currentIndex]?.image ? (
<motion.img
key={currentIndex}
src={data[currentIndex].image}
alt="feature"
className="aspect-auto h-full w-full rounded-xl border border-neutral-300/50 object-cover object-left-top p-1 shadow-lg"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.25, ease: "easeOut" }}
/>
) : data[currentIndex]?.video ? (
<video
preload="auto"
src={data[currentIndex].video}
className="aspect-auto h-full w-full rounded-lg object-cover shadow-lg"
autoPlay
loop
muted
/>
) : (
<div className="aspect-auto h-full w-full rounded-xl border border-neutral-300/50 bg-gray-200 p-1"></div>
)}
</div>
<ul
ref={carouselRef}
className=" flex h-full snap-x flex-nowrap overflow-x-auto py-10 [-ms-overflow-style:none] [-webkit-mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [scrollbar-width:none] lg:hidden [&::-webkit-scrollbar]:hidden snap-mandatory"
style={{
padding: "50px calc(50%)",
}}
>
{data.map((item, index) => (
<div
key={item.id}
className="card relative mr-8 grid h-full max-w-60 shrink-0 items-start justify-center py-4 last:mr-0"
onClick={() => setCurrentIndex(index)}
style={{
scrollSnapAlign: "center",
}}
>
<div className="absolute bottom-0 left-0 right-auto top-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30">
<div
className={`absolute left-0 top-0 h-full ${
currentIndex === index ? "w-full" : "w-0"
} origin-top bg-primary transition-all ease-linear`}
style={{
transitionDuration:
currentIndex === index ? `${collapseDelay}ms` : "0s",
}}
></div>
</div>
<h2 className="text-xl font-bold">{item.title}</h2>
<p className="mx-0 max-w-sm text-balance text-sm">
{item.content}
</p>
</div>
))}
</ul>
</div>
</div>
</div>
</section>
);
}

138
src/components/icons.tsx Normal file
View File

@ -0,0 +1,138 @@
type IconProps = React.HTMLAttributes<SVGElement>;
export const Icons = {
logo: (props: IconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<rect width="7" height="7" x="14" y="3" rx="1" />
<path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3" />
</svg>
),
twitter: (props: IconProps) => (
<svg
{...props}
height="23"
viewBox="0 0 1200 1227"
width="23"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" />
</svg>
),
github: (props: IconProps) => (
<svg viewBox="0 0 438.549 438.549" {...props}>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
radix: (props: IconProps) => (
<svg viewBox="0 0 25 25" fill="none" {...props}>
<path
d="M12 25C7.58173 25 4 21.4183 4 17C4 12.5817 7.58173 9 12 9V25Z"
fill="currentcolor"
></path>
<path d="M12 0H4V8H12V0Z" fill="currentcolor"></path>
<path
d="M17 8C19.2091 8 21 6.20914 21 4C21 1.79086 19.2091 0 17 0C14.7909 0 13 1.79086 13 4C13 6.20914 14.7909 8 17 8Z"
fill="currentcolor"
></path>
</svg>
),
aria: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M13.966 22.624l-1.69-4.281H8.122l3.892-9.144 5.662 13.425zM8.884 1.376H0v21.248zm15.116 0h-8.884L24 22.624Z" />
</svg>
),
npm: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
fill="currentColor"
/>
</svg>
),
yarn: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M12 0C5.375 0 0 5.375 0 12s5.375 12 12 12 12-5.375 12-12S18.625 0 12 0zm.768 4.105c.183 0 .363.053.525.157.125.083.287.185.755 1.154.31-.088.468-.042.551-.019.204.056.366.19.463.375.477.917.542 2.553.334 3.605-.241 1.232-.755 2.029-1.131 2.576.324.329.778.899 1.117 1.825.278.774.31 1.478.273 2.015a5.51 5.51 0 0 0 .602-.329c.593-.366 1.487-.917 2.553-.931.714-.009 1.269.445 1.353 1.103a1.23 1.23 0 0 1-.945 1.362c-.649.158-.95.278-1.821.843-1.232.797-2.539 1.242-3.012 1.39a1.686 1.686 0 0 1-.704.343c-.737.181-3.266.315-3.466.315h-.046c-.783 0-1.214-.241-1.45-.491-.658.329-1.51.19-2.122-.134a1.078 1.078 0 0 1-.58-1.153 1.243 1.243 0 0 1-.153-.195c-.162-.25-.528-.936-.454-1.946.056-.723.556-1.367.88-1.71a5.522 5.522 0 0 1 .408-2.256c.306-.727.885-1.348 1.32-1.737-.32-.537-.644-1.367-.329-2.21.227-.602.412-.936.82-1.08h-.005c.199-.074.389-.153.486-.259a3.418 3.418 0 0 1 2.298-1.103c.037-.093.079-.185.125-.283.31-.658.639-1.029 1.024-1.168a.94.94 0 0 1 .328-.06zm.006.7c-.507.016-1.001 1.519-1.001 1.519s-1.27-.204-2.266.871c-.199.218-.468.334-.746.44-.079.028-.176.023-.417.672-.371.991.625 2.094.625 2.094s-1.186.839-1.626 1.881c-.486 1.144-.338 2.261-.338 2.261s-.843.732-.899 1.487c-.051.663.139 1.2.343 1.515.227.343.51.176.51.176s-.561.653-.037.931c.477.25 1.283.394 1.71-.037.31-.31.371-1.001.486-1.283.028-.065.12.111.209.199.097.093.264.195.264.195s-.755.324-.445 1.066c.102.246.468.403 1.066.398.222-.005 2.664-.139 3.313-.296.375-.088.505-.283.505-.283s1.566-.431 2.998-1.357c.917-.598 1.293-.76 2.034-.936.612-.148.57-1.098-.241-1.084-.839.009-1.575.44-2.196.825-1.163.718-1.742.672-1.742.672l-.018-.032c-.079-.13.371-1.293-.134-2.678-.547-1.515-1.413-1.881-1.344-1.997.297-.5 1.038-1.297 1.334-2.78.176-.899.13-2.377-.269-3.151-.074-.144-.732.241-.732.241s-.616-1.371-.788-1.483a.271.271 0 0 0-.157-.046z"
fill="currentColor"
/>
</svg>
),
pnpm: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z"
fill="currentColor"
/>
</svg>
),
react: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"
fill="currentColor"
/>
</svg>
),
tailwind: (props: IconProps) => (
<svg viewBox="0 0 24 24" {...props}>
<path
d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z"
fill="currentColor"
/>
</svg>
),
google: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
/>
</svg>
),
apple: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
),
paypal: (props: IconProps) => (
<svg role="img" viewBox="0 0 24 24" {...props}>
<path
d="M7.076 21.337H2.47a.641.641 0 0 1-.633-.74L4.944.901C5.026.382 5.474 0 5.998 0h7.46c2.57 0 4.578.543 5.69 1.81 1.01 1.15 1.304 2.42 1.012 4.287-.023.143-.047.288-.077.437-.983 5.05-4.349 6.797-8.647 6.797h-2.19c-.524 0-.968.382-1.05.9l-1.12 7.106zm14.146-14.42a3.35 3.35 0 0 0-.607-.541c-.013.076-.026.175-.041.254-.93 4.778-4.005 7.201-9.138 7.201h-2.19a.563.563 0 0 0-.556.479l-1.187 7.527h-.506l-.24 1.516a.56.56 0 0 0 .554.647h3.882c.46 0 .85-.334.922-.788.06-.26.76-4.852.816-5.09a.932.932 0 0 1 .923-.788h.58c3.76 0 6.705-1.528 7.565-5.946.36-1.847.174-3.388-.777-4.471z"
fill="currentColor"
/>
</svg>
),
spinner: (props: IconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
),
};

View File

@ -0,0 +1,60 @@
"use client";
import { AnimatePresence, motion, useInView, Variants } from "framer-motion";
import { useRef } from "react";
type MarginType = `${number}${"px" | "%"}`;
interface BlurFadeProps {
children: React.ReactNode;
className?: string;
variant?: {
hidden: { y: number };
visible: { y: number };
};
duration?: number;
delay?: number;
yOffset?: number;
inView?: boolean;
inViewMargin?: MarginType;
blur?: string;
}
export default function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
yOffset = 6,
inView = false,
inViewMargin = "-50px",
blur = "6px",
}: BlurFadeProps) {
const ref = useRef(null);
const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
const isInView = !inView || inViewResult;
const defaultVariants: Variants = {
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
};
const combinedVariants = variant || defaultVariants;
return (
<AnimatePresence>
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: "easeOut",
}}
className={className}
>
{children}
</motion.div>
</AnimatePresence>
);
}

View File

@ -0,0 +1,49 @@
import { cn } from "@/lib/utils";
interface BorderBeamProps {
className?: string;
size?: number;
duration?: number;
borderWidth?: number;
anchor?: number;
colorFrom?: string;
colorTo?: string;
delay?: number;
}
export const BorderBeam = ({
className,
size = 200,
duration = 15,
anchor = 90,
borderWidth = 1.5,
colorFrom = "#ffaa40",
colorTo = "#9c40ff",
delay = 0,
}: BorderBeamProps) => {
return (
<div
style={
{
"--size": size,
"--duration": duration,
"--anchor": anchor,
"--border-width": borderWidth,
"--color-from": colorFrom,
"--color-to": colorTo,
"--delay": `-${delay}s`,
} as React.CSSProperties
}
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]",
// mask styles
"![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
// pseudo styles
"after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]",
className,
)}
/>
);
};

View File

@ -0,0 +1,51 @@
interface DotPatternProps {
width?: any;
height?: any;
x?: any;
y?: any;
cx?: any;
cy?: any;
cr?: any;
className?: string;
[key: string]: any;
}
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
}: DotPatternProps) {
const id = "pattern-circle";
return (
<svg
aria-hidden="true"
className={
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80"
}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
);
}
export default DotPattern;

View File

@ -0,0 +1,194 @@
"use client";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
interface FlickeringGridProps {
squareSize?: number;
gridGap?: number;
flickerChance?: number;
color?: string;
width?: number;
height?: number;
className?: string;
maxOpacity?: number;
}
const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4,
gridGap = 6,
flickerChance = 0.3,
color = "rgb(0, 0, 0)",
width,
height,
className,
maxOpacity = 0.3,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isInView, setIsInView] = useState(false);
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return `rgba(0, 0, 0,`;
}
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d");
if (!ctx) return "rgba(255, 0, 0,";
// Handle HSL colors
if (color.startsWith("hsl")) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
return `rgba(${r}, ${g}, ${b},`;
}
// Handle other color formats (rgb, hex, etc.)
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
return `rgba(${r}, ${g}, ${b},`;
};
return toRGBA(color);
}, [color]);
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement) => {
const canvasWidth = width || canvas.clientWidth;
const canvasHeight = height || canvas.clientHeight;
const dpr = window.devicePixelRatio || 1;
canvas.width = canvasWidth * dpr;
canvas.height = canvasHeight * dpr;
canvas.style.width = `${canvasWidth}px`;
canvas.style.height = `${canvasHeight}px`;
const cols = Math.floor(canvasWidth / (squareSize + gridGap));
const rows = Math.floor(canvasHeight / (squareSize + gridGap));
const squares = new Float32Array(cols * rows);
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity;
}
return {
width: canvasWidth,
height: canvasHeight,
cols,
rows,
squares,
dpr,
};
},
[squareSize, gridGap, width, height, maxOpacity]
);
const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity;
}
}
},
[flickerChance, maxOpacity]
);
const drawGrid = useCallback(
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cols: number,
rows: number,
squares: Float32Array,
dpr: number
) => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "transparent";
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j];
ctx.fillStyle = `${memoizedColor}${opacity})`;
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr
);
}
}
},
[memoizedColor, squareSize, gridGap]
);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let animationFrameId: number;
let { width, height, cols, rows, squares, dpr } = setupCanvas(canvas);
let lastTime = 0;
const animate = (time: number) => {
if (!isInView) return;
const deltaTime = (time - lastTime) / 1000;
lastTime = time;
updateSquares(squares, deltaTime);
drawGrid(ctx, width * dpr, height * dpr, cols, rows, squares, dpr);
animationFrameId = requestAnimationFrame(animate);
};
const handleResize = () => {
({ width, height, cols, rows, squares, dpr } = setupCanvas(canvas));
};
const observer = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
},
{ threshold: 0 }
);
observer.observe(canvas);
window.addEventListener("resize", handleResize);
if (isInView) {
animationFrameId = requestAnimationFrame(animate);
}
return () => {
window.removeEventListener("resize", handleResize);
cancelAnimationFrame(animationFrameId);
observer.disconnect();
};
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
return (
<canvas
ref={canvasRef}
className={`size-full pointer-events-none ${className}`}
style={{
width: width || "100%",
height: height || "100%",
}}
width={width}
height={height}
/>
);
};
export default FlickeringGrid;

View File

@ -0,0 +1,140 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { Play, XIcon } from "lucide-react";
import Image from "next/image";
import { useState } from "react";
type AnimationStyle =
| "from-bottom"
| "from-center"
| "from-top"
| "from-left"
| "from-right"
| "fade"
| "top-in-bottom-out"
| "left-in-right-out";
interface HeroVideoProps {
animationStyle?: AnimationStyle;
videoSrc: string;
thumbnailSrc: string;
thumbnailAlt?: string;
className?: string;
}
const animationVariants = {
"from-bottom": {
initial: { y: "100%", opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: "100%", opacity: 0 },
},
"from-center": {
initial: { scale: 0.5, opacity: 0 },
animate: { scale: 1, opacity: 1 },
exit: { scale: 0.5, opacity: 0 },
},
"from-top": {
initial: { y: "-100%", opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: "-100%", opacity: 0 },
},
"from-left": {
initial: { x: "-100%", opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: "-100%", opacity: 0 },
},
"from-right": {
initial: { x: "100%", opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: "100%", opacity: 0 },
},
fade: {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
},
"top-in-bottom-out": {
initial: { y: "-100%", opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: "100%", opacity: 0 },
},
"left-in-right-out": {
initial: { x: "-100%", opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { x: "100%", opacity: 0 },
},
};
export default function HeroVideoDialog({
animationStyle = "from-center",
videoSrc,
thumbnailSrc,
thumbnailAlt = "Video thumbnail",
className,
}: HeroVideoProps) {
const [isVideoOpen, setIsVideoOpen] = useState(false);
const selectedAnimation = animationVariants[animationStyle];
return (
<div className={cn("relative", className)}>
<div
className="relative cursor-pointer group rounded-md p-2 ring-1 ring-slate-200/50 dark:bg-gray-900/70 dark:ring-white/10 backdrop-blur-md"
onClick={() => setIsVideoOpen(true)}
>
<Image
src={thumbnailSrc}
alt={thumbnailAlt}
width={1920}
height={1080}
className="transition-all duration-200 group-hover:brightness-[0.8] ease-out rounded-md border"
/>
<div className="absolute inset-0 flex items-center justify-center group-hover:scale-100 scale-[0.9] transition-all duration-200 ease-out rounded-2xl">
<div className="z-30 bg-primary/10 flex items-center justify-center rounded-full backdrop-blur-md size-28">
<div
className={`flex items-center justify-center bg-gradient-to-b from-primary/30 to-primary shadow-md rounded-full size-20 transition-all ease-out duration-200 relative group-hover:scale-[1.2] scale-100`}
>
<Play
className="size-8 text-white fill-white group-hover:scale-105 scale-100 transition-transform duration-200 ease-out"
style={{
filter:
"drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))",
}}
/>
</div>
</div>
</div>
</div>
<AnimatePresence>
{isVideoOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onClick={() => setIsVideoOpen(false)}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
>
<motion.div
{...selectedAnimation}
transition={{ type: "spring", damping: 30, stiffness: 300 }}
className="relative w-full max-w-4xl aspect-video mx-4 md:mx-0"
>
<motion.button className="absolute -top-16 right-0 text-white text-xl bg-neutral-900/50 ring-1 backdrop-blur-md rounded-full p-2 dark:bg-neutral-100/50 dark:text-black">
<XIcon className="size-5" />
</motion.button>
<div className="size-full border-2 border-white rounded-2xl overflow-hidden isolate z-[1] relative">
<iframe
src={videoSrc}
className="size-full rounded-2xl"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View 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>
);
}

View File

@ -0,0 +1,58 @@
import { cn } from "@/lib/utils";
import React, { CSSProperties } from "react";
interface RippleProps {
mainCircleSize?: number;
mainCircleOpacity?: number;
numCircles?: number;
className?: string;
}
const Ripple = React.memo(function Ripple({
mainCircleSize = 210,
mainCircleOpacity = 0.24,
numCircles = 8,
className,
}: RippleProps) {
return (
<div
className={cn(
"absolute inset-0 bg-white/5 [mask-image:linear-gradient(to_bottom,white,transparent)]",
className
)}
>
{Array.from({ length: numCircles }, (_, i) => {
const size = mainCircleSize + i * 70;
const opacity = mainCircleOpacity - i * 0.03;
const animationDelay = `${i * 0.06}s`;
const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
const borderOpacity = 5 + i * 5;
return (
<div
key={i}
className={`absolute animate-ripple rounded-full bg-foreground/25 shadow-xl border [--i:${i}]`}
style={
{
width: `${size}px`,
height: `${size}px`,
opacity,
animationDelay,
borderStyle,
borderWidth: "1px",
borderColor: `hsl(var(--foreground), ${borderOpacity / 100})`,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%) scale(1)",
} as CSSProperties
}
/>
);
})}
</div>
);
});
Ripple.displayName = "Ripple";
export default Ripple;

110
src/components/menu.tsx Normal file
View File

@ -0,0 +1,110 @@
"use client";
import Link from "next/link";
import * as React from "react";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { siteConfig } from "@/lib/config";
import { cn } from "@/lib/utils";
export default function NavigationMenuDemo() {
return (
<NavigationMenu>
<NavigationMenuList>
{siteConfig.header.map((item, index) => (
<NavigationMenuItem key={index}>
{item.trigger ? (
<>
<NavigationMenuTrigger>{item.trigger}</NavigationMenuTrigger>
<NavigationMenuContent>
<ul
className={`grid gap-3 p-6 ${
item.content.main
? "md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]"
: "w-[400px] md:w-[500px] md:grid-cols-2 lg:w-[600px]"
}`}
>
{item.content.main && (
<li className="row-span-3">
<NavigationMenuLink asChild>
<Link
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-primary/10 from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
href={item.content.main.href}
>
{item.content.main.icon}
<div className="mb-2 mt-4 text-lg font-medium">
{item.content.main.title}
</div>
<p className="text-sm leading-tight text-muted-foreground">
{item.content.main.description}
</p>
</Link>
</NavigationMenuLink>
</li>
)}
{item.content.items.map((subItem, subIndex) => (
<ListItem
key={subIndex}
href={subItem.href}
title={subItem.title}
className="hover:bg-primary/10"
>
{subItem.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</>
) : (
<Link
href={item.href || ""}
target="_arya"
legacyBehavior
passHref
>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
{item.label}
</NavigationMenuLink>
</Link>
)}
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
);
}
const ListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
);
});
ListItem.displayName = "ListItem";

View File

@ -0,0 +1,66 @@
"use client";
import { Pie, PieChart } from "recharts";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
const chartData = [
{ browser: "chrome", visitors: 187, fill: "var(--color-chrome)" },
{ browser: "safari", visitors: 110, fill: "var(--color-safari)" },
{ browser: "firefox", visitors: 165, fill: "var(--color-firefox)" },
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
];
const chartConfig = {
visitors: {
label: "Visitors",
},
chrome: {
label: "Chrome",
color: "hsl(var(--chart-1))",
},
safari: {
label: "Safari",
color: "hsl(var(--chart-2))",
},
firefox: {
label: "Firefox",
color: "hsl(var(--chart-3))",
},
edge: {
label: "Edge",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
export default function Component() {
return (
<Card className="flex flex-col border-none shadow-none">
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie data={chartData} dataKey="visitors" nameKey="browser" />
</PieChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="leading-2 text-muted-foreground text-center">
Effective marketing and advertising materials. It is also a great
tool.
</div>
</CardFooter>
</Card>
);
}

View File

@ -0,0 +1,76 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
import {
Label,
PolarGrid,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts";
const chartData = [
{ browser: "safari", visitors: 1260, fill: "var(--color-safari)" },
];
const chartConfig = {
visitors: {
label: "Visitors",
},
safari: {
label: "Safari",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export default function Component() {
return (
<Card className="border-none shadow-none">
<CardContent className="flex items-center">
<div>
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square w-[100px] h-[110px]"
>
<RadialBarChart
data={chartData}
startAngle={-190}
endAngle={70}
innerRadius={34}
outerRadius={50}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[37, 30]}
/>
<RadialBar dataKey="visitors" background cornerRadius={20} />
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
></text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</div>
<div className="ml-5">
<h2 className="font-semibold text-xl">Title</h2>
<p>Effective marketing and advertising materials.</p>
</div>
</CardContent>
</Card>
);
}

153
src/components/safari.tsx Normal file
View File

@ -0,0 +1,153 @@
import { SVGProps } from "react";
export interface SafariProps extends SVGProps<SVGSVGElement> {
url?: string;
src?: string;
width?: number;
height?: number;
}
export default function Safari({
src,
url,
width = 1203,
height = 753,
...props
}: SafariProps) {
return (
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#path0)">
<path
d="M0 52H1202V741C1202 747.627 1196.63 753 1190 753H12C5.37258 753 0 747.627 0 741V52Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 12C0 5.37258 5.37258 0 12 0H1190C1196.63 0 1202 5.37258 1202 12V52H0L0 12Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.06738 12C1.06738 5.92487 5.99225 1 12.0674 1H1189.93C1196.01 1 1200.93 5.92487 1200.93 12V51H1.06738V12Z"
className="fill-white dark:fill-[#262626]"
/>
<circle
cx="27"
cy="25"
r="6"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<circle
cx="47"
cy="25"
r="6"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<circle
cx="67"
cy="25"
r="6"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M286 17C286 13.6863 288.686 11 292 11H946C949.314 11 952 13.6863 952 17V35C952 38.3137 949.314 41 946 41H292C288.686 41 286 38.3137 286 35V17Z"
fill="#F5F5F5"
/>
<g className="mix-blend-luminosity">
<path
d="M566.269 32.0852H572.426C573.277 32.0852 573.696 31.6663 573.696 30.7395V25.9851C573.696 25.1472 573.353 24.7219 572.642 24.6521V23.0842C572.642 20.6721 571.036 19.5105 569.348 19.5105C567.659 19.5105 566.053 20.6721 566.053 23.0842V24.6711C565.393 24.7727 565 25.1917 565 25.9851V30.7395C565 31.6663 565.418 32.0852 566.269 32.0852ZM567.272 22.97C567.272 21.491 568.211 20.6785 569.348 20.6785C570.478 20.6785 571.423 21.491 571.423 22.97V24.6394L567.272 24.6458V22.97Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<text
x="580"
y="30"
fill="#A3A3A3"
fontSize="12"
fontFamily="Arial, sans-serif"
>
{url}
</text>
</g>
<g className="mix-blend-luminosity">
<path
d="M265.5 33.8984C265.641 33.8984 265.852 33.8516 266.047 33.7422C270.547 31.2969 272.109 30.1641 272.109 27.3203V21.4219C272.109 20.4844 271.742 20.1484 270.961 19.8125C270.094 19.4453 267.18 18.4297 266.328 18.1406C266.07 18.0547 265.766 18 265.5 18C265.234 18 264.93 18.0703 264.672 18.1406C263.82 18.3828 260.906 19.4531 260.039 19.8125C259.258 20.1406 258.891 20.4844 258.891 21.4219V27.3203C258.891 30.1641 260.461 31.2812 264.945 33.7422C265.148 33.8516 265.359 33.8984 265.5 33.8984ZM265.922 19.5781C266.945 19.9766 269.172 20.7656 270.344 21.1875C270.562 21.2656 270.617 21.3828 270.617 21.6641V27.0234C270.617 29.3125 269.469 29.9375 265.945 32.0625C265.727 32.1875 265.617 32.2344 265.508 32.2344V19.4844C265.617 19.4844 265.734 19.5156 265.922 19.5781Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M936.273 24.9766C936.5 24.9766 936.68 24.9062 936.82 24.7578L940.023 21.5312C940.195 21.3594 940.273 21.1719 940.273 20.9531C940.273 20.7422 940.188 20.5391 940.023 20.3828L936.82 17.125C936.68 16.9688 936.5 16.8906 936.273 16.8906C935.852 16.8906 935.516 17.2422 935.516 17.6719C935.516 17.8828 935.594 18.0547 935.727 18.2031L937.594 20.0312C937.227 19.9766 936.852 19.9453 936.477 19.9453C932.609 19.9453 929.516 23.0391 929.516 26.9141C929.516 30.7891 932.633 33.9062 936.5 33.9062C940.375 33.9062 943.484 30.7891 943.484 26.9141C943.484 26.4453 943.156 26.1094 942.688 26.1094C942.234 26.1094 941.93 26.4453 941.93 26.9141C941.93 29.9297 939.516 32.3516 936.5 32.3516C933.492 32.3516 931.07 29.9297 931.07 26.9141C931.07 23.875 933.469 21.4688 936.477 21.4688C936.984 21.4688 937.453 21.5078 937.867 21.5781L935.734 23.6875C935.594 23.8281 935.516 24 935.516 24.2109C935.516 24.6406 935.852 24.9766 936.273 24.9766Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M1134 33.0156C1134.49 33.0156 1134.89 32.6094 1134.89 32.1484V27.2578H1139.66C1140.13 27.2578 1140.54 26.8594 1140.54 26.3672C1140.54 25.8828 1140.13 25.4766 1139.66 25.4766H1134.89V20.5859C1134.89 20.1172 1134.49 19.7188 1134 19.7188C1133.52 19.7188 1133.11 20.1172 1133.11 20.5859V25.4766H1128.34C1127.88 25.4766 1127.46 25.8828 1127.46 26.3672C1127.46 26.8594 1127.88 27.2578 1128.34 27.2578H1133.11V32.1484C1133.11 32.6094 1133.52 33.0156 1134 33.0156Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M1161.8 31.0703H1163.23V32.375C1163.23 34.0547 1164.12 34.9219 1165.81 34.9219H1174.2C1175.89 34.9219 1176.77 34.0547 1176.77 32.3828V24.0469C1176.77 22.375 1175.89 21.5 1174.2 21.5H1172.77V20.2578C1172.77 18.5859 1171.88 17.7109 1170.19 17.7109H1161.8C1160.1 17.7109 1159.23 18.5781 1159.23 20.2578V28.5234C1159.23 30.1953 1160.1 31.0703 1161.8 31.0703ZM1161.9 29.5078C1161.18 29.5078 1160.78 29.1328 1160.78 28.3828V20.3984C1160.78 19.6406 1161.18 19.2656 1161.9 19.2656H1170.09C1170.8 19.2656 1171.2 19.6406 1171.2 20.3984V21.5H1165.81C1164.12 21.5 1163.23 22.375 1163.23 24.0469V29.5078H1161.9ZM1165.91 33.3672C1165.19 33.3672 1164.8 32.9922 1164.8 32.2422V24.1875C1164.8 23.4297 1165.19 23.0625 1165.91 23.0625H1174.1C1174.81 23.0625 1175.21 23.4297 1175.21 24.1875V32.2422C1175.21 32.9922 1174.81 33.3672 1174.1 33.3672H1165.91Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M1099.51 28.4141C1099.91 28.4141 1100.24 28.0859 1100.24 27.6953V19.8359L1100.18 18.6797L1100.66 19.25L1101.75 20.4141C1101.88 20.5547 1102.06 20.625 1102.24 20.625C1102.6 20.625 1102.9 20.3672 1102.9 20C1102.9 19.8047 1102.82 19.6641 1102.69 19.5312L1100.06 17.0078C1099.88 16.8203 1099.7 16.7578 1099.51 16.7578C1099.32 16.7578 1099.14 16.8203 1098.95 17.0078L1096.33 19.5312C1096.2 19.6641 1096.12 19.8047 1096.12 20C1096.12 20.3672 1096.41 20.625 1096.77 20.625C1096.95 20.625 1097.14 20.5547 1097.27 20.4141L1098.35 19.25L1098.84 18.6719L1098.78 19.8359V27.6953C1098.78 28.0859 1099.11 28.4141 1099.51 28.4141ZM1095 34.6562H1104C1105.7 34.6562 1106.57 33.7812 1106.57 32.1094V24.4297C1106.57 22.7578 1105.7 21.8828 1104 21.8828H1101.89V23.4375H1103.9C1104.61 23.4375 1105.02 23.8125 1105.02 24.5625V31.9688C1105.02 32.7188 1104.61 33.0938 1103.9 33.0938H1095.1C1094.38 33.0938 1093.98 32.7188 1093.98 31.9688V24.5625C1093.98 23.8125 1094.38 23.4375 1095.1 23.4375H1097.13V21.8828H1095C1093.31 21.8828 1092.43 22.75 1092.43 24.4297V32.1094C1092.43 33.7812 1093.31 34.6562 1095 34.6562Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M99.5703 33.6016H112.938C114.633 33.6016 115.516 32.7266 115.516 31.0547V21.5469C115.516 19.875 114.633 19 112.938 19H99.5703C97.8828 19 97 19.8672 97 21.5469V31.0547C97 32.7266 97.8828 33.6016 99.5703 33.6016ZM99.6719 32.0469C98.9531 32.0469 98.5547 31.6719 98.5547 30.9141V21.6875C98.5547 20.9297 98.9531 20.5547 99.6719 20.5547H103.234V32.0469H99.6719ZM112.836 20.5547C113.555 20.5547 113.953 20.9297 113.953 21.6875V30.9141C113.953 31.6719 113.555 32.0469 112.836 32.0469H104.711V20.5547H112.836ZM101.703 23.4141C101.984 23.4141 102.219 23.1719 102.219 22.9062C102.219 22.6406 101.984 22.4062 101.703 22.4062H100.102C99.8203 22.4062 99.5859 22.6406 99.5859 22.9062C99.5859 23.1719 99.8203 23.4141 100.102 23.4141H101.703ZM101.703 25.5156C101.984 25.5156 102.219 25.2812 102.219 25.0078C102.219 24.7422 101.984 24.5078 101.703 24.5078H100.102C99.8203 24.5078 99.5859 24.7422 99.5859 25.0078C99.5859 25.2812 99.8203 25.5156 100.102 25.5156H101.703ZM101.703 27.6094C101.984 27.6094 102.219 27.3828 102.219 27.1094C102.219 26.8438 101.984 26.6172 101.703 26.6172H100.102C99.8203 26.6172 99.5859 26.8438 99.5859 27.1094C99.5859 27.3828 99.8203 27.6094 100.102 27.6094H101.703Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M143.914 32.5938C144.094 32.7656 144.312 32.8594 144.562 32.8594C145.086 32.8594 145.492 32.4531 145.492 31.9375C145.492 31.6797 145.391 31.4453 145.211 31.2656L139.742 25.9219L145.211 20.5938C145.391 20.4141 145.492 20.1719 145.492 19.9219C145.492 19.4062 145.086 19 144.562 19C144.312 19 144.094 19.0938 143.922 19.2656L137.844 25.2031C137.625 25.4062 137.516 25.6562 137.516 25.9297C137.516 26.2031 137.625 26.4375 137.836 26.6484L143.914 32.5938Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M168.422 32.8594C168.68 32.8594 168.891 32.7656 169.07 32.5938L175.148 26.6562C175.359 26.4375 175.469 26.2109 175.469 25.9297C175.469 25.6562 175.367 25.4141 175.148 25.2109L169.07 19.2656C168.891 19.0938 168.68 19 168.422 19C167.898 19 167.492 19.4062 167.492 19.9219C167.492 20.1719 167.602 20.4141 167.773 20.5938L173.25 25.9375L167.773 31.2656C167.594 31.4531 167.492 31.6797 167.492 31.9375C167.492 32.4531 167.898 32.8594 168.422 32.8594Z"
fill="#A3A3A3"
/>
</g>
<image
href={src}
width="1200"
height="700"
x="1"
y="52"
preserveAspectRatio="xMidYMid slice"
clipPath="url(#roundedBottom)"
/>
</g>
<defs>
<clipPath id="path0">
<rect width={width} height={height} fill="white" />
</clipPath>
<clipPath id="roundedBottom">
<path
d="M1 52H1201V741C1201 747.075 1196.08 752 1190 752H12C5.92486 752 1 747.075 1 741V52Z"
fill="white"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,45 @@
interface SectionProps {
id?: string;
title?: string;
subtitle?: string;
description?: string;
children?: React.ReactNode;
className?: string;
}
export default function Section({
id,
title,
subtitle,
description,
children,
className,
}: SectionProps) {
const sectionId = title ? title.toLowerCase().replace(/\s+/g, "-") : id;
return (
<section id={id || sectionId}>
<div className={className}>
<div className="relative container mx-auto px-4 py-16 max-w-7xl">
<div className="text-center space-y-4 pb-6 mx-auto">
{title && (
<h2 className="text-sm text-primary font-mono font-medium tracking-wider uppercase">
{title}
</h2>
)}
{subtitle && (
<h3 className="mx-auto mt-4 max-w-xs text-3xl font-semibold sm:max-w-none sm:text-4xl md:text-5xl">
{subtitle}
</h3>
)}
{description && (
<p className="mt-6 text-lg leading-8 text-slate-600 max-w-2xl mx-auto">
{description}
</p>
)}
</div>
{children}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,21 @@
import BlogCard from "@/components/blog-card";
import Section from "@/components/section";
import { getBlogPosts } from "@/lib/blog";
export default async function BlogSection() {
const allPosts = await getBlogPosts();
const articles = await Promise.all(
allPosts.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
);
return (
<Section title="Blog" subtitle="Latest Articles">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{articles.map((data, idx) => (
<BlogCard key={data.slug} data={data} priority={idx <= 1} />
))}
</div>
</Section>
);
}

View File

@ -0,0 +1,29 @@
import { Icons } from "@/components/icons";
import Section from "@/components/section";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Link from "next/link";
export default function CtaSection() {
return (
<Section
id="cta"
title="Ready to get started?"
subtitle="Start your free trial today."
className="bg-primary/10 rounded-xl py-16"
>
<div className="flex flex-col w-full sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4 pt-4">
<Link
href="/signup"
className={cn(
buttonVariants({ variant: "default" }),
"w-full sm:w-auto text-background flex gap-2"
)}
>
<Icons.logo className="h-6 w-6" />
Get started for free
</Link>
</div>
</Section>
);
}

View File

@ -0,0 +1,41 @@
import Section from "@/components/section";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { siteConfig } from "@/lib/config";
export default function FAQ() {
return (
<Section title="FAQ" subtitle="Frequently asked questions">
<div className="mx-auto my-12 md:max-w-[800px]">
<Accordion
type="single"
collapsible
className="flex w-full flex-col items-center justify-center space-y-2"
>
{siteConfig.faqs.map((faq, idx) => (
<AccordionItem
key={idx}
value={faq.question}
className="w-full border rounded-lg overflow-hidden"
>
<AccordionTrigger className="px-4">
{faq.question}
</AccordionTrigger>
<AccordionContent className="px-4">{faq.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
<h4 className="mb-12 text-center text-sm font-medium tracking-tight text-foreground/80">
Still have questions? Email us at{" "}
<a href={`mailto:${siteConfig.links.email}`} className="underline">
{siteConfig.links.email}
</a>
</h4>
</Section>
);
}

View File

@ -0,0 +1,42 @@
import Features from "@/components/features-horizontal";
import Section from "@/components/section";
import { BarChart3, Brain, FileText, LineChart } from "lucide-react";
const data = [
{
id: 1,
title: "AI-Powered Dashboard",
content: "Visualize trends and gain insights at a glance.",
image: "/dashboard.png",
icon: <BarChart3 className="h-6 w-6 text-primary" />,
},
{
id: 2,
title: "Natural Language Processing",
content: "Analyze text and extract sentiment effortlessly.",
image: "/dashboard.png",
icon: <Brain className="h-6 w-6 text-primary" />,
},
{
id: 3,
title: "Predictive Analytics",
content: "Forecast trends and make data-driven decisions.",
image: "/dashboard.png",
icon: <LineChart className="h-6 w-6 text-primary" />,
},
{
id: 4,
title: "Automated Reporting",
content: "Generate comprehensive reports with one click.",
image: "/dashboard.png",
icon: <FileText className="h-6 w-6 text-primary" />,
},
];
export default function Component() {
return (
<Section title="Features" subtitle="User Flows and Navigational Structures">
<Features collapseDelay={5000} linePosition="bottom" data={data} />
</Section>
);
}

View File

@ -0,0 +1,64 @@
import { Icons } from "@/components/icons";
import { siteConfig } from "@/lib/config";
import { ChevronRight } from "lucide-react";
import Link from "next/link";
export default function Footer() {
return (
<footer>
<div className="max-w-6xl mx-auto py-16 sm:px-10 px-5 pb-0">
<a
href="/"
title={siteConfig.name}
className="relative mr-6 flex items-center space-x-2"
>
<Icons.logo className="w-auto h-[40px]" />
<span className="font-bold text-xl">{siteConfig.name}</span>
</a>
<div className="grid md:grid-cols-3 lg:grid-cols-4 sm:grid-cols-2 mt-8">
{siteConfig.footer.map((section, index) => (
<div key={index} className="mb-5">
<h2 className="font-semibold">{section.title}</h2>
<ul>
{section.links.map((link, linkIndex) => (
<li key={linkIndex} className="my-2">
<Link
href={link.href}
className="group inline-flex cursor-pointer items-center justify-start gap-1 text-muted-foreground duration-200 hover:text-foreground hover:opacity-90"
>
{link.icon && link.icon}
{link.text}
<ChevronRight className="h-4 w-4 translate-x-0 transform opacity-0 transition-all duration-300 ease-out group-hover:translate-x-1 group-hover:opacity-100" />
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<div className="max-w-6xl mx-auto border-t py-2 grid md:grid-cols-2 h-full justify-between w-full grid-cols-1 gap-1">
<span className="text-sm tracking-tight text-foreground">
Copyright © {new Date().getFullYear()}{" "}
<Link href="/" className="cursor-pointer">
{siteConfig.name}
</Link>{" "}
- {siteConfig.description}
</span>
<ul className="flex justify-start md:justify-end text-sm tracking-tight text-foreground">
<li className="mr-3 md:mx-4">
<Link href="#" target="_blank" rel="noopener noreferrer">
Privacy Policy
</Link>
</li>
<li className="mr-3 md:mx-4">
<Link href="#" target="_blank" rel="noopener noreferrer">
Terms of Service
</Link>
</li>
</ul>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,85 @@
"use client";
import Drawer from "@/components/drawer";
import { Icons } from "@/components/icons";
import Menu from "@/components/menu";
import { buttonVariants } from "@/components/ui/button";
import { siteConfig } from "@/lib/config";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { useEffect, useState } from "react";
export default function Header() {
const [addBorder, setAddBorder] = useState(false);
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 20) {
setAddBorder(true);
} else {
setAddBorder(false);
}
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<header
className={
"relative sticky top-0 z-50 py-2 bg-background/60 backdrop-blur"
}
>
<div className="flex justify-between items-center container">
<Link
href="/"
title="brand-logo"
className="relative mr-6 flex items-center space-x-2"
>
<Icons.logo className="w-auto h-[40px]" />
<span className="font-bold text-xl">{siteConfig.name}</span>
</Link>
<div className="hidden lg:block">
<div className="flex items-center ">
<nav className="mr-10">
<Menu />
</nav>
<div className="gap-2 flex">
<Link
href="/login"
className={buttonVariants({ variant: "outline" })}
>
Login
</Link>
<Link
href="/signup"
className={cn(
buttonVariants({ variant: "default" }),
"w-full sm:w-auto text-background flex gap-2"
)}
>
<Icons.logo className="h-6 w-6" />
Get Started for Free
</Link>
</div>
</div>
</div>
<div className="mt-2 cursor-pointer block lg:hidden">
<Drawer />
</div>
</div>
<hr
className={cn(
"absolute w-full bottom-0 transition-opacity duration-300 ease-in-out",
addBorder ? "opacity-100" : "opacity-0"
)}
/>
</header>
);
}

View File

@ -0,0 +1,153 @@
"use client";
import { motion } from "framer-motion";
import { Icons } from "@/components/icons";
import HeroVideoDialog from "@/components/magicui/hero-video";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Link from "next/link";
const ease = [0.16, 1, 0.3, 1];
function HeroPill() {
return (
<motion.a
href="/blog/introducing-acme-ai"
className="flex w-auto items-center space-x-2 rounded-full bg-primary/20 px-2 py-1 ring-1 ring-accent whitespace-pre"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease }}
>
<div className="w-fit rounded-full bg-accent px-2 py-0.5 text-center text-xs font-medium text-primary sm:text-sm">
📣 Announcement
</div>
<p className="text-xs font-medium text-primary sm:text-sm">
Introducing Acme.ai
</p>
<svg
width="12"
height="12"
className="ml-1"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.78141 5.33312L5.20541 1.75712L6.14808 0.814453L11.3334 5.99979L6.14808 11.1851L5.20541 10.2425L8.78141 6.66645H0.666748V5.33312H8.78141Z"
fill="hsl(var(--primary))"
/>
</svg>
</motion.a>
);
}
function HeroTitles() {
return (
<div className="flex w-full max-w-2xl flex-col space-y-4 overflow-hidden pt-8">
<motion.h1
className="text-center text-4xl font-medium leading-tight text-foreground sm:text-5xl md:text-6xl"
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
transition={{
duration: 1,
ease,
staggerChildren: 0.2,
}}
>
{["Automate", "your", "workflow", "with AI"].map((text, index) => (
<motion.span
key={index}
className="inline-block px-1 md:px-2 text-balance font-semibold"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: index * 0.2,
ease,
}}
>
{text}
</motion.span>
))}
</motion.h1>
<motion.p
className="mx-auto max-w-xl text-center text-lg leading-7 text-muted-foreground sm:text-xl sm:leading-9 text-balance"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: 0.6,
duration: 0.8,
ease,
}}
>
No matter what problem you have, our AI can help you solve it.
</motion.p>
</div>
);
}
function HeroCTA() {
return (
<>
<motion.div
className="mx-auto mt-6 flex w-full max-w-2xl flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.8, ease }}
>
<Link
href="/signup"
className={cn(
buttonVariants({ variant: "default" }),
"w-full sm:w-auto text-background flex gap-2"
)}
>
<Icons.logo className="h-6 w-6" />
Get started for free
</Link>
</motion.div>
<motion.p
className="mt-5 text-sm text-muted-foreground"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.0, duration: 0.8 }}
>
7 day free trial. No credit card required.
</motion.p>
</>
);
}
function HeroImage() {
return (
<motion.div
className="relative mx-auto flex w-full items-center justify-center"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.2, duration: 1, ease }}
>
<HeroVideoDialog
animationStyle="from-center"
videoSrc="https://www.youtube.com/embed/qh3NGpYRG3I?si=4rb-zSdDkVK9qxxb"
thumbnailSrc="/dashboard.png"
thumbnailAlt="Hero Video"
className="border rounded-lg shadow-lg max-w-screen-lg mt-16"
/>
</motion.div>
);
}
export default function Hero2() {
return (
<section id="hero">
<div className="relative flex w-full flex-col items-center justify-start px-4 pt-32 sm:px-6 sm:pt-24 md:pt-32 lg:px-8">
<HeroPill />
<HeroTitles />
<HeroCTA />
<HeroImage />
<div className="pointer-events-none absolute inset-x-0 -bottom-12 h-1/3 bg-gradient-to-t from-background via-background to-transparent lg:h-1/4"></div>
</div>
</section>
);
}

View File

@ -0,0 +1,38 @@
import Features from "@/components/features-vertical";
import Section from "@/components/section";
import { Sparkles, Upload, Zap } from "lucide-react";
const data = [
{
id: 1,
title: "1. Upload Your Data",
content:
"Simply upload your data to our secure platform. We support various file formats and data types to ensure a seamless integration with your existing systems.",
image: "/dashboard.png",
icon: <Upload className="w-6 h-6 text-primary" />,
},
{
id: 2,
title: "2. Click Start",
content:
"Our advanced AI algorithms automatically process and analyze your data, extracting valuable insights and patterns that would be difficult to identify manually.",
image: "/dashboard.png",
icon: <Zap className="w-6 h-6 text-primary" />,
},
{
id: 3,
title: "3. Get Actionable Insights",
content:
"Receive clear, actionable insights and recommendations based on the AI analysis. Use these insights to make data-driven decisions and improve your business strategies.",
image: "/dashboard.png",
icon: <Sparkles className="w-6 h-6 text-primary" />,
},
];
export default function Component() {
return (
<Section title="How it works" subtitle="Just 3 steps to get started">
<Features data={data} />
</Section>
);
}

View File

@ -0,0 +1,41 @@
import Marquee from "@/components/magicui/marquee";
import Image from "next/image";
const companies = [
"Google",
"Microsoft",
"Amazon",
"Netflix",
"YouTube",
"Instagram",
"Uber",
"Spotify",
];
export default function Logos() {
return (
<section id="logos">
<div className="container mx-auto px-4 md:px-8 py-12">
<h3 className="text-center text-sm font-semibold text-gray-500">
TRUSTED BY LEADING TEAMS
</h3>
<div className="relative mt-6">
<Marquee className="max-w-full [--duration:40s]">
{companies.map((logo, idx) => (
<Image
key={idx}
width={112}
height={40}
src={`https://cdn.magicui.design/companies/${logo}.svg`}
className="h-10 w-28 dark:brightness-0 dark:invert grayscale opacity-30"
alt={logo}
/>
))}
</Marquee>
<div className="pointer-events-none absolute inset-y-0 left-0 h-full w-1/3 bg-gradient-to-r from-background"></div>
<div className="pointer-events-none absolute inset-y-0 right-0 h-full w-1/3 bg-gradient-to-l from-background"></div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,139 @@
"use client";
import Section from "@/components/section";
import { buttonVariants } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { siteConfig } from "@/lib/config";
import useWindowSize from "@/lib/hooks/use-window-size";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { Check } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { FaStar } from "react-icons/fa";
export default function PricingSection() {
const [isMonthly, setIsMonthly] = useState(true);
const { isDesktop } = useWindowSize();
const handleToggle = () => {
setIsMonthly(!isMonthly);
};
return (
<Section title="Pricing" subtitle="Choose the plan that's right for you">
<div className="flex justify-center mb-10">
<span className="mr-2 font-semibold">Monthly</span>
<label className="relative inline-flex items-center cursor-pointer">
<Label>
<Switch checked={!isMonthly} onCheckedChange={handleToggle} />
</Label>
</label>
<span className="ml-2 font-semibold">Yearly</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 sm:2 gap-4">
{siteConfig.pricing.map((plan, index) => (
<motion.div
key={index}
initial={{ y: 50, opacity: 1 }}
whileInView={
isDesktop
? {
y: 0,
opacity: 1,
x:
index === siteConfig.pricing.length - 1
? -30
: index === 0
? 30
: 0,
scale:
index === 0 || index === siteConfig.pricing.length - 1
? 0.94
: 1.0,
}
: {}
}
viewport={{ once: true }}
transition={{
duration: 1.6,
type: "spring",
stiffness: 100,
damping: 30,
delay: 0.4,
opacity: { duration: 0.5 },
}}
className={cn(
`rounded-2xl border-[1px] p-6 bg-background text-center lg:flex lg:flex-col lg:justify-center relative`,
plan.isPopular ? "border-primary border-[2px]" : "border-border",
index === 0 || index === siteConfig.pricing.length - 1
? "z-0 transform translate-x-0 translate-y-0 -translate-z-[50px] rotate-y-[10deg]"
: "z-10",
index === 0 && "origin-right",
index === siteConfig.pricing.length - 1 && "origin-left"
)}
>
{plan.isPopular && (
<div className="absolute top-0 right-0 bg-primary py-0.5 px-2 rounded-bl-xl rounded-tr-xl flex items-center">
<FaStar className="text-white" />
<span className="text-white ml-1 font-sans font-semibold">
Popular
</span>
</div>
)}
<div>
<p className="text-base font-semibold text-muted-foreground">
{plan.name}
</p>
<p className="mt-6 flex items-center justify-center gap-x-2">
<span className="text-5xl font-bold tracking-tight text-foreground">
{isMonthly ? plan.price : plan.yearlyPrice}
</span>
{plan.period !== "Next 3 months" && (
<span className="text-sm font-semibold leading-6 tracking-wide text-muted-foreground">
/ {plan.period}
</span>
)}
</p>
<p className="text-xs leading-5 text-muted-foreground">
{isMonthly ? "billed monthly" : "billed annually"}
</p>
<ul className="mt-5 gap-2 flex flex-col">
{plan.features.map((feature, idx) => (
<li key={idx} className="flex items-center">
<Check className="mr-2 h-4 w-4 text-primary" />
<span>{feature}</span>
</li>
))}
</ul>
<hr className="w-full my-4" />
<Link
href={plan.href}
className={cn(
buttonVariants({
variant: "outline",
}),
"group relative w-full gap-2 overflow-hidden text-lg font-semibold tracking-tighter",
"transform-gpu ring-offset-current transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-1 hover:bg-primary hover:text-white",
plan.isPopular
? "bg-primary text-white"
: "bg-white text-black"
)}
>
{plan.buttonText}
</Link>
<p className="mt-6 text-xs leading-5 text-muted-foreground">
{plan.description}
</p>
</div>
</motion.div>
))}
</div>
</Section>
);
}

View File

@ -0,0 +1,50 @@
import BlurFade from "@/components/magicui/blur-fade";
import Section from "@/components/section";
import { Card, CardContent } from "@/components/ui/card";
import { Brain, Shield, Zap } from "lucide-react";
const problems = [
{
title: "Data Overload",
description:
"Businesses struggle to make sense of vast amounts of complex data, missing out on valuable insights that could drive growth and innovation.",
icon: Brain,
},
{
title: "Slow Decision-Making",
description:
"Traditional data processing methods are too slow, causing businesses to lag behind market changes and miss crucial opportunities.",
icon: Zap,
},
{
title: "Data Security Concerns",
description:
"With increasing cyber threats, businesses worry about the safety of their sensitive information when adopting new technologies.",
icon: Shield,
},
];
export default function Component() {
return (
<Section
title="Problem"
subtitle="Manually entering your data is a hassle."
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-12">
{problems.map((problem, index) => (
<BlurFade key={index} delay={0.2 + index * 0.2} inView>
<Card className="bg-background border-none shadow-none">
<CardContent className="p-6 space-y-4">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<problem.icon className="w-6 h-6 text-primary" />
</div>
<h3 className="text-xl font-semibold">{problem.title}</h3>
<p className="text-muted-foreground">{problem.description}</p>
</CardContent>
</Card>
</BlurFade>
))}
</div>
</Section>
);
}

View File

@ -0,0 +1,125 @@
"use client";
import FlickeringGrid from "@/components/magicui/flickering-grid";
import Ripple from "@/components/magicui/ripple";
import Safari from "@/components/safari";
import Section from "@/components/section";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
const features = [
{
title: "Advanced AI Algorithms",
description:
"Our platform utilizes cutting-edge AI algorithms to provide accurate and efficient solutions for your business needs.",
className: "hover:bg-red-500/10 transition-all duration-500 ease-out",
content: (
<>
<Safari
src={`/dashboard.png`}
url="https://acme.ai"
className="-mb-32 mt-4 max-h-64 w-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-y-[-10px] transition-all duration-300"
/>
</>
),
},
{
title: "Secure Data Handling",
description:
"We prioritize your data security with state-of-the-art encryption and strict privacy protocols, ensuring your information remains confidential.",
className:
"order-3 xl:order-none hover:bg-blue-500/10 transition-all duration-500 ease-out",
content: (
<Safari
src={`/dashboard.png`}
url="https://acme.ai"
className="-mb-32 mt-4 max-h-64 w-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-y-[-10px] transition-all duration-300"
/>
),
},
{
title: "Seamless Integration",
description:
"Easily integrate our AI solutions into your existing workflows and systems for a smooth and efficient operation.",
className:
"md:row-span-2 hover:bg-orange-500/10 transition-all duration-500 ease-out",
content: (
<>
<FlickeringGrid
className="z-0 absolute inset-0 [mask:radial-gradient(circle_at_center,#fff_400px,transparent_0)]"
squareSize={4}
gridGap={6}
color="#000"
maxOpacity={0.1}
flickerChance={0.1}
height={800}
width={800}
/>
<Safari
src={`/dashboard.png`}
url="https://acme.ai"
className="-mb-48 ml-12 mt-16 h-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-x-[-10px] transition-all duration-300"
/>
</>
),
},
{
title: "Customizable Solutions",
description:
"Tailor our AI services to your specific needs with flexible customization options, allowing you to get the most out of our platform.",
className:
"flex-row order-4 md:col-span-2 md:flex-row xl:order-none hover:bg-green-500/10 transition-all duration-500 ease-out",
content: (
<>
<Ripple className="absolute -bottom-full" />
<Safari
src={`/dashboard.png`}
url="https://acme.ai"
className="-mb-32 mt-4 max-h-64 w-full px-4 select-none drop-shadow-[0_0_28px_rgba(0,0,0,.1)] group-hover:translate-y-[-10px] transition-all duration-300"
/>
</>
),
},
];
export default function Component() {
return (
<Section
title="Solution"
subtitle="Empower Your Business with AI Workflows"
description="Generic AI tools won't suffice. Our platform is purpose-built to provide exceptional AI-driven solutions for your unique business needs."
className="bg-neutral-100 dark:bg-neutral-900"
>
<div className="mx-auto mt-16 grid max-w-sm grid-cols-1 gap-6 text-gray-500 md:max-w-3xl md:grid-cols-2 xl:grid-rows-2 md:grid-rows-3 xl:max-w-6xl xl:auto-rows-fr xl:grid-cols-3">
{features.map((feature, index) => (
<motion.div
key={index}
className={cn(
"group relative items-start overflow-hidden bg-neutral-50 dark:bg-neutral-800 p-6 rounded-2xl",
feature.className
)}
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
type: "spring",
stiffness: 100,
damping: 30,
delay: index * 0.1,
}}
viewport={{ once: true }}
>
<div>
<h3 className="font-semibold mb-2 text-primary">
{feature.title}
</h3>
<p className="text-foreground">{feature.description}</p>
</div>
{feature.content}
<div className="absolute bottom-0 left-0 h-32 w-full bg-gradient-to-t from-neutral-50 dark:from-neutral-900 pointer-events-none"></div>
</motion.div>
))}
</div>
</Section>
);
}

View File

@ -0,0 +1,91 @@
import BlurFade from "@/components/magicui/blur-fade";
import Section from "@/components/section";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import Image from "next/image";
import { MdOutlineFormatQuote } from "react-icons/md";
const companies = [
"Google",
"Microsoft",
"Amazon",
"Netflix",
"YouTube",
"Instagram",
"Uber",
"Spotify",
];
export default function Component() {
return (
<Section
title="Testimonial Highlight"
subtitle="What our customers are saying"
>
<Carousel>
<div className="max-w-2xl mx-auto relative">
<CarouselContent>
{Array.from({ length: 7 }).map((_, index) => (
<CarouselItem key={index}>
<div className="p-2 pb-5">
<div className="text-center">
<MdOutlineFormatQuote className="text-4xl text-themeDarkGray my-4 mx-auto" />
<BlurFade delay={0.25} inView>
<h4 className="text-1xl font-semibold max-w-lg mx-auto px-10">
There is a lot of exciting stuff going on in the stars
above us that make astronomy so much fun. The truth is
the universe is a constantly changing, moving, some
would say living thing because you just never know
what you are going to see on any given night of
stargazing.
</h4>
</BlurFade>
<BlurFade delay={0.25 * 2} inView>
<div className="mt-8">
<Image
width={0}
height={40}
key={index}
src={`https://cdn.magicui.design/companies/${
companies[index % companies.length]
}.svg`}
alt={`${companies[index % companies.length]} Logo`}
className="mx-auto w-auto h-[40px] grayscale opacity-30"
/>
</div>
</BlurFade>
<div className="">
<BlurFade delay={0.25 * 3} inView>
<h4 className="text-1xl font-semibold my-2">
Leslie Alexander
</h4>
</BlurFade>
</div>
<BlurFade delay={0.25 * 4} inView>
<div className=" mb-3">
<span className="text-sm text-themeDarkGray">
UI Designer
</span>
</div>
</BlurFade>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<div className="pointer-events-none absolute inset-y-0 left-0 h-full w-2/12 bg-gradient-to-r from-background"></div>
<div className="pointer-events-none absolute inset-y-0 right-0 h-full w-2/12 bg-gradient-to-l from-background"></div>
</div>
<div className="md:block hidden">
<CarouselPrevious />
<CarouselNext />
</div>
</Carousel>
</Section>
);
}

View File

@ -0,0 +1,313 @@
"use client";
import Marquee from "@/components/magicui/marquee";
import Section from "@/components/section";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { Star } from "lucide-react";
import Image from "next/image";
export const Highlight = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<span
className={cn(
"bg-primary/20 p-1 py-0.5 font-bold text-primary dark:bg-primary/20 dark:text-primary",
className
)}
>
{children}
</span>
);
};
export interface TestimonialCardProps {
name: string;
role: string;
img?: string;
description: React.ReactNode;
className?: string;
[key: string]: any;
}
export const TestimonialCard = ({
description,
name,
img,
role,
className,
...props // Capture the rest of the props
}: TestimonialCardProps) => (
<div
className={cn(
"mb-4 flex w-full cursor-pointer break-inside-avoid flex-col items-center justify-between gap-6 rounded-xl p-4",
// light styles
" border border-neutral-200 bg-white",
// dark styles
"dark:bg-black dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
className
)}
{...props} // Spread the rest of the props here
>
<div className="select-none text-sm font-normal text-neutral-700 dark:text-neutral-400">
{description}
<div className="flex flex-row py-1">
<Star className="size-4 text-yellow-500 fill-yellow-500" />
<Star className="size-4 text-yellow-500 fill-yellow-500" />
<Star className="size-4 text-yellow-500 fill-yellow-500" />
<Star className="size-4 text-yellow-500 fill-yellow-500" />
<Star className="size-4 text-yellow-500 fill-yellow-500" />
</div>
</div>
<div className="flex w-full select-none items-center justify-start gap-5">
<Image
width={40}
height={40}
src={img || ""}
alt={name}
className="h-10 w-10 rounded-full ring-1 ring-border ring-offset-4"
/>
<div>
<p className="font-medium text-neutral-500">{name}</p>
<p className="text-xs font-normal text-neutral-400">{role}</p>
</div>
</div>
</div>
);
const testimonials = [
{
name: "Alex Rivera",
role: "CTO at InnovateTech",
img: "https://randomuser.me/api/portraits/men/91.jpg",
description: (
<p>
The AI-driven analytics from #QuantumInsights have revolutionized our
product development cycle.
<Highlight>
Insights are now more accurate and faster than ever.
</Highlight>{" "}
A game-changer for tech companies.
</p>
),
},
{
name: "Samantha Lee",
role: "Marketing Director at NextGen Solutions",
img: "https://randomuser.me/api/portraits/women/12.jpg",
description: (
<p>
Implementing #AIStream&apos;s customer prediction model has drastically
improved our targeting strategy.
<Highlight>Seeing a 50% increase in conversion rates!</Highlight> Highly
recommend their solutions.
</p>
),
},
{
name: "Raj Patel",
role: "Founder & CEO at StartUp Grid",
img: "https://randomuser.me/api/portraits/men/45.jpg",
description: (
<p>
As a startup, we need to move fast and stay ahead. #CodeAI&apos;s
automated coding assistant helps us do just that.
<Highlight>Our development speed has doubled.</Highlight> Essential tool
for any startup.
</p>
),
},
{
name: "Emily Chen",
role: "Product Manager at Digital Wave",
img: "https://randomuser.me/api/portraits/women/83.jpg",
description: (
<p>
#VoiceGen&apos;s AI-driven voice synthesis has made creating global
products a breeze.
<Highlight>Localization is now seamless and efficient.</Highlight> A
must-have for global product teams.
</p>
),
},
{
name: "Michael Brown",
role: "Data Scientist at FinTech Innovations",
img: "https://randomuser.me/api/portraits/men/1.jpg",
description: (
<p>
Leveraging #DataCrunch&apos;s AI for our financial models has given us
an edge in predictive accuracy.
<Highlight>
Our investment strategies are now powered by real-time data analytics.
</Highlight>{" "}
Transformative for the finance industry.
</p>
),
},
{
name: "Linda Wu",
role: "VP of Operations at LogiChain Solutions",
img: "https://randomuser.me/api/portraits/women/5.jpg",
description: (
<p>
#LogiTech&apos;s supply chain optimization tools have drastically
reduced our operational costs.
<Highlight>
Efficiency and accuracy in logistics have never been better.
</Highlight>{" "}
</p>
),
},
{
name: "Carlos Gomez",
role: "Head of R&D at EcoInnovate",
img: "https://randomuser.me/api/portraits/men/14.jpg",
description: (
<p>
By integrating #GreenTech&apos;s sustainable energy solutions,
we&apos;ve seen a significant reduction in carbon footprint.
<Highlight>
Leading the way in eco-friendly business practices.
</Highlight>{" "}
Pioneering change in the industry.
</p>
),
},
{
name: "Aisha Khan",
role: "Chief Marketing Officer at Fashion Forward",
img: "https://randomuser.me/api/portraits/women/56.jpg",
description: (
<p>
#TrendSetter&apos;s market analysis AI has transformed how we approach
fashion trends.
<Highlight>
Our campaigns are now data-driven with higher customer engagement.
</Highlight>{" "}
Revolutionizing fashion marketing.
</p>
),
},
{
name: "Tom Chen",
role: "Director of IT at HealthTech Solutions",
img: "https://randomuser.me/api/portraits/men/18.jpg",
description: (
<p>
Implementing #MediCareAI in our patient care systems has improved
patient outcomes significantly.
<Highlight>
Technology and healthcare working hand in hand for better health.
</Highlight>{" "}
A milestone in medical technology.
</p>
),
},
{
name: "Sofia Patel",
role: "CEO at EduTech Innovations",
img: "https://randomuser.me/api/portraits/women/73.jpg",
description: (
<p>
#LearnSmart&apos;s AI-driven personalized learning plans have doubled
student performance metrics.
<Highlight>
Education tailored to every learner&apos;s needs.
</Highlight>{" "}
Transforming the educational landscape.
</p>
),
},
{
name: "Jake Morrison",
role: "CTO at SecureNet Tech",
img: "https://randomuser.me/api/portraits/men/25.jpg",
description: (
<p>
With #CyberShield&apos;s AI-powered security systems, our data
protection levels are unmatched.
<Highlight>Ensuring safety and trust in digital spaces.</Highlight>{" "}
Redefining cybersecurity standards.
</p>
),
},
{
name: "Nadia Ali",
role: "Product Manager at Creative Solutions",
img: "https://randomuser.me/api/portraits/women/78.jpg",
description: (
<p>
#DesignPro&apos;s AI has streamlined our creative process, enhancing
productivity and innovation.
<Highlight>Bringing creativity and technology together.</Highlight> A
game-changer for creative industries.
</p>
),
},
{
name: "Omar Farooq",
role: "Founder at Startup Hub",
img: "https://randomuser.me/api/portraits/men/54.jpg",
description: (
<p>
#VentureAI&apos;s insights into startup ecosystems have been invaluable
for our growth and funding strategies.
<Highlight>Empowering startups with data-driven decisions.</Highlight> A
catalyst for startup success.
</p>
),
},
];
export default function Testimonials() {
return (
<Section
title="Testimonials"
subtitle="What our customers are saying"
className="max-w-8xl"
>
<div className="relative mt-6 max-h-screen overflow-hidden">
<div className="gap-4 md:columns-2 xl:columns-3 2xl:columns-4">
{Array(Math.ceil(testimonials.length / 3))
.fill(0)
.map((_, i) => (
<Marquee
vertical
key={i}
className={cn({
"[--duration:60s]": i === 1,
"[--duration:30s]": i === 2,
"[--duration:70s]": i === 3,
})}
>
{testimonials.slice(i * 3, (i + 1) * 3).map((card, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{
delay: Math.random() * 0.8,
duration: 1.2,
}}
>
<TestimonialCard {...card} />
</motion.div>
))}
</Marquee>
))}
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-1/4 w-full bg-gradient-to-t from-background from-20%"></div>
<div className="pointer-events-none absolute inset-x-0 top-0 h-1/4 w-full bg-gradient-to-b from-background from-20%"></div>
</div>
</Section>
);
}

View 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>
);
}

View 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>;
}

View 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>
);
}

View 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 }

View 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 }

View 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 }

View File

@ -0,0 +1,262 @@
"use client";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
);
});
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
);
});
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "left-1/2 -translate-x-16 bottom-0 translate-y-4"
: "-top-12 right-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
});
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "right-1/2 bottom-0 translate-y-4 translate-x-16"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
});
CarouselNext.displayName = "CarouselNext";
export {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
type CarouselApi,
};

365
src/components/ui/chart.tsx Normal file
View File

@ -0,0 +1,365 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View 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,
}

View 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 }

View 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 }

View File

@ -0,0 +1,128 @@
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:bg-primary/10 hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-primary/10 data-[state=open]:bg-primary/10"
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
NavigationMenu,
NavigationMenuContent,
NavigationMenuIndicator,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
NavigationMenuViewport,
};

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

96
src/lib/blog.ts Normal file
View File

@ -0,0 +1,96 @@
import { siteConfig } from "@/lib/config";
import fs from "fs";
import path from "path";
import rehypePrettyCode from "rehype-pretty-code";
import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
export type Post = {
title: string;
publishedAt: string;
summary: string;
author: string;
slug: string;
image?: string;
};
function parseFrontmatter(fileContent: string) {
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
let match = frontmatterRegex.exec(fileContent);
let frontMatterBlock = match![1];
let content = fileContent.replace(frontmatterRegex, "").trim();
let frontMatterLines = frontMatterBlock.trim().split("\n");
let metadata: Partial<Post> = {};
frontMatterLines.forEach((line) => {
let [key, ...valueArr] = line.split(": ");
let value = valueArr.join(": ").trim();
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
metadata[key.trim() as keyof Post] = value;
});
return { data: metadata as Post, content };
}
function getMDXFiles(dir: string) {
return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx");
}
export async function markdownToHTML(markdown: string) {
const p = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypePrettyCode, {
// https://rehype-pretty.pages.dev/#usage
theme: {
light: "min-light",
dark: "min-dark",
},
keepBackground: false,
})
.use(rehypeStringify)
.process(markdown);
return p.toString();
}
export async function getPost(slug: string) {
const filePath = path.join("content", `${slug}.mdx`);
const source = fs.readFileSync(filePath, "utf-8");
const { content: rawContent, data: metadata } = parseFrontmatter(source);
const content = await markdownToHTML(rawContent);
const defaultImage = `${siteConfig.url}/og?title=${encodeURIComponent(
metadata.title
)}`;
return {
source: content,
metadata: {
...metadata,
image: metadata.image || defaultImage,
},
slug,
};
}
async function getAllPosts(dir: string) {
const mdxFiles = getMDXFiles(dir);
return Promise.all(
mdxFiles.map(async (file) => {
const slug = path.basename(file, path.extname(file));
const { metadata, source } = await getPost(slug);
return {
...metadata,
slug,
source,
};
})
);
}
export async function getBlogPosts() {
return getAllPosts(path.join(process.cwd(), "content"));
}

251
src/lib/config.tsx Normal file
View File

@ -0,0 +1,251 @@
import { Icons } from "@/components/icons";
import { FaTwitter } from "react-icons/fa";
import { FaYoutube } from "react-icons/fa6";
import { RiInstagramFill } from "react-icons/ri";
export const BLUR_FADE_DELAY = 0.15;
export const siteConfig = {
name: "acme.ai",
description: "Automate your workflow with AI",
url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
keywords: ["SaaS", "Template", "Next.js", "React", "Tailwind CSS"],
links: {
email: "support@acme.ai",
twitter: "https://twitter.com/magicuidesign",
discord: "https://discord.gg/87p2vpsat5",
github: "https://github.com/magicuidesign/magicui",
instagram: "https://instagram.com/magicuidesign/",
},
header: [
{
trigger: "Features",
content: {
main: {
icon: <Icons.logo className="h-6 w-6" />,
title: "AI-Powered Automation",
description: "Streamline your workflow with intelligent automation.",
href: "#",
},
items: [
{
href: "#",
title: "Task Automation",
description: "Automate repetitive tasks and save time.",
},
{
href: "#",
title: "Workflow Optimization",
description: "Optimize your processes with AI-driven insights.",
},
{
href: "#",
title: "Intelligent Scheduling",
description: "AI-powered scheduling for maximum efficiency.",
},
],
},
},
{
trigger: "Solutions",
content: {
items: [
{
title: "For Small Businesses",
href: "#",
description: "Tailored automation solutions for growing companies.",
},
{
title: "Enterprise",
href: "#",
description: "Scalable AI automation for large organizations.",
},
{
title: "Developers",
href: "#",
description: "API access and integration tools for developers.",
},
{
title: "Healthcare",
href: "#",
description: "Specialized automation for healthcare workflows.",
},
{
title: "Finance",
href: "#",
description: "AI-driven process automation for financial services.",
},
{
title: "Education",
href: "#",
description:
"Streamline administrative tasks in educational institutions.",
},
],
},
},
{
href: "/blog",
label: "Blog",
},
],
pricing: [
{
name: "BASIC",
href: "#",
price: "$19",
period: "month",
yearlyPrice: "$16",
features: [
"1 User",
"5GB Storage",
"Basic Support",
"Limited API Access",
"Standard Analytics",
],
description: "Perfect for individuals and small projects",
buttonText: "Subscribe",
isPopular: false,
},
{
name: "PRO",
href: "#",
price: "$49",
period: "month",
yearlyPrice: "$40",
features: [
"5 Users",
"50GB Storage",
"Priority Support",
"Full API Access",
"Advanced Analytics",
],
description: "Ideal for growing businesses and teams",
buttonText: "Subscribe",
isPopular: true,
},
{
name: "ENTERPRISE",
href: "#",
price: "$99",
period: "month",
yearlyPrice: "$82",
features: [
"Unlimited Users",
"500GB Storage",
"24/7 Premium Support",
"Custom Integrations",
"AI-Powered Insights",
],
description: "For large-scale operations and high-volume users",
buttonText: "Subscribe",
isPopular: false,
},
],
faqs: [
{
question: "What is acme.ai?",
answer: (
<span>
acme.ai is a platform that helps you build and manage your AI-powered
applications. It provides tools and services to streamline the
development and deployment of AI solutions.
</span>
),
},
{
question: "How can I get started with acme.ai?",
answer: (
<span>
You can get started with acme.ai by signing up for an account on our
website, creating a new project, and following our quick-start guide.
We also offer tutorials and documentation to help you along the way.
</span>
),
},
{
question: "What types of AI models does acme.ai support?",
answer: (
<span>
acme.ai supports a wide range of AI models, including but not limited
to natural language processing, computer vision, and predictive
analytics. We continuously update our platform to support the latest
AI technologies.
</span>
),
},
{
question: "Is acme.ai suitable for beginners in AI development?",
answer: (
<span>
Yes, acme.ai is designed to be user-friendly for both beginners and
experienced developers. We offer intuitive interfaces, pre-built
templates, and extensive learning resources to help users of all skill
levels create AI-powered applications.
</span>
),
},
{
question: "What kind of support does acme.ai provide?",
answer: (
<span>
acme.ai provides comprehensive support including documentation, video
tutorials, a community forum, and dedicated customer support. We also
offer premium support plans for enterprises with more complex needs.
</span>
),
},
],
footer: [
{
title: "Product",
links: [
{ href: "#", text: "Features", icon: null },
{ href: "#", text: "Pricing", icon: null },
{ href: "#", text: "Documentation", icon: null },
{ href: "#", text: "API", icon: null },
],
},
{
title: "Company",
links: [
{ href: "#", text: "About Us", icon: null },
{ href: "#", text: "Careers", icon: null },
{ href: "#", text: "Blog", icon: null },
{ href: "#", text: "Press", icon: null },
{ href: "#", text: "Partners", icon: null },
],
},
{
title: "Resources",
links: [
{ href: "#", text: "Community", icon: null },
{ href: "#", text: "Contact", icon: null },
{ href: "#", text: "Support", icon: null },
{ href: "#", text: "Status", icon: null },
],
},
{
title: "Social",
links: [
{
href: "#",
text: "Twitter",
icon: <FaTwitter />,
},
{
href: "#",
text: "Instagram",
icon: <RiInstagramFill />,
},
{
href: "#",
text: "Youtube",
icon: <FaYoutube />,
},
],
},
],
};
export type SiteConfig = typeof siteConfig;

View File

@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
export default function useWindowSize() {
const [windowSize, setWindowSize] = useState<{
width: number | undefined;
height: number | undefined;
}>({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return {
windowSize,
isMobile: typeof windowSize?.width === "number" && windowSize?.width < 768,
isDesktop:
typeof windowSize?.width === "number" && windowSize?.width >= 768,
};
}

89
src/lib/utils.ts Normal file
View File

@ -0,0 +1,89 @@
import { siteConfig } from "@/lib/config";
import { type ClassValue, clsx } from "clsx";
import { Metadata } from "next";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL || siteConfig.url}${path}`;
}
export function constructMetadata({
title = siteConfig.name,
description = siteConfig.description,
image = absoluteUrl("/og"),
...props
}: {
title?: string;
description?: string;
image?: string;
[key: string]: Metadata[keyof Metadata];
}): Metadata {
return {
title: {
template: "%s | " + siteConfig.name,
default: siteConfig.name,
},
description: description || siteConfig.description,
keywords: siteConfig.keywords,
openGraph: {
title,
description,
url: siteConfig.url,
siteName: siteConfig.name,
images: [
{
url: image,
width: 1200,
height: 630,
alt: title,
},
],
type: "website",
locale: "en_US",
},
icons: "/favicon.ico",
metadataBase: new URL(siteConfig.url),
authors: [
{
name: siteConfig.name,
url: siteConfig.url,
},
],
...props,
};
}
export function formatDate(date: string) {
let currentDate = new Date().getTime();
if (!date.includes("T")) {
date = `${date}T00:00:00`;
}
let targetDate = new Date(date).getTime();
let timeDifference = Math.abs(currentDate - targetDate);
let daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
let fullDate = new Date(date).toLocaleString("en-us", {
month: "long",
day: "numeric",
year: "numeric",
});
if (daysAgo < 1) {
return "Today";
} else if (daysAgo < 7) {
return `${fullDate} (${daysAgo}d ago)`;
} else if (daysAgo < 30) {
const weeksAgo = Math.floor(daysAgo / 7);
return `${fullDate} (${weeksAgo}w ago)`;
} else if (daysAgo < 365) {
const monthsAgo = Math.floor(daysAgo / 30);
return `${fullDate} (${monthsAgo}mo ago)`;
} else {
const yearsAgo = Math.floor(daysAgo / 365);
return `${fullDate} (${yearsAgo}y ago)`;
}
}

105
tailwind.config.ts Normal file
View File

@ -0,0 +1,105 @@
import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
animation: {
marquee: "marquee var(--duration) linear infinite",
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite",
},
keyframes: {
marquee: {
from: { transform: "translateX(0)" },
to: { transform: "translateX(calc(-100% - var(--gap)))" },
},
"marquee-vertical": {
from: { transform: "translateY(0)" },
to: { transform: "translateY(calc(-100% - var(--gap)))" },
},
"border-beam": {
"100%": {
"offset-distance": "100%",
},
},
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
ripple: {
"0%, 100%": {
transform: "translate(-50%, -50%) scale(1)",
},
"50%": {
transform: "translate(-50%, -50%) scale(0.9)",
},
},
},
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
} satisfies Config;
export default config;

28
tsconfig.json Normal file
View 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"]
}