Showing preview only (240K chars total). Download the full file or copy to clipboard to get everything.
Repository: pdsuwwz/nextjs-nextra-starter
Branch: main
Commit: af664c3c8082
Files: 92
Total size: 205.4 KB
Directory structure:
gitextract_lpxxmf0u/
├── .gitignore
├── .npmrc
├── .vscode/
│ ├── settings.json
│ └── tailwind.json
├── LICENSE
├── README-en.md
├── README.md
├── components.json
├── eslint.config.js
├── next-env.d.ts
├── next-sitemap.config.mjs
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── src/
│ ├── app/
│ │ ├── [lang]/
│ │ │ ├── [[...mdxPath]]/
│ │ │ │ └── page.tsx
│ │ │ ├── _components/
│ │ │ │ ├── ThemeProvider.tsx
│ │ │ │ └── ThirdPartyScripts.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── not-found.ts
│ │ │ └── styles/
│ │ │ ├── index.css
│ │ │ └── overrides.css
│ │ └── _dictionaries/
│ │ └── get-dictionary.ts
│ ├── components/
│ │ ├── AIDemoLanding/
│ │ │ ├── EntryCard.tsx
│ │ │ ├── index.tsx
│ │ │ └── interactions.tsx
│ │ ├── CustomFooter/
│ │ │ └── index.tsx
│ │ ├── HomepageHero/
│ │ │ ├── Section.tsx
│ │ │ ├── Setup.tsx
│ │ │ ├── SetupHero.module.css
│ │ │ └── index.tsx
│ │ ├── MotionWrapper/
│ │ │ ├── FadeIn.tsx
│ │ │ ├── Flash.tsx
│ │ │ └── index.ts
│ │ ├── PanelParticles/
│ │ │ └── index.tsx
│ │ ├── ScrollProgressBar/
│ │ │ └── index.tsx
│ │ ├── ThemeSwitcher/
│ │ │ └── index.tsx
│ │ ├── TitleBadge/
│ │ │ └── index.tsx
│ │ ├── auth/
│ │ │ ├── login-form.client.tsx
│ │ │ └── login-form.tsx
│ │ └── ui/
│ │ ├── accordion.tsx
│ │ ├── alert.tsx
│ │ ├── button.tsx
│ │ ├── card-hover-effect.tsx
│ │ ├── card.tsx
│ │ ├── flip-words.tsx
│ │ ├── hover-card.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── link-preview.tsx
│ │ ├── loader.tsx
│ │ ├── separator.tsx
│ │ ├── sonner.tsx
│ │ └── toggle.tsx
│ ├── content/
│ │ ├── en/
│ │ │ ├── _meta.tsx
│ │ │ ├── ai-demo.mdx
│ │ │ ├── docs/
│ │ │ │ ├── _meta.tsx
│ │ │ │ ├── examples/
│ │ │ │ │ ├── test-tailwind.mdx
│ │ │ │ │ └── theme-update.mdx
│ │ │ │ ├── i18n.mdx
│ │ │ │ └── index.mdx
│ │ │ ├── index.mdx
│ │ │ ├── introduction.mdx
│ │ │ ├── login.mdx
│ │ │ └── upgrade.mdx
│ │ └── zh/
│ │ ├── _meta.tsx
│ │ ├── ai-demo.mdx
│ │ ├── docs/
│ │ │ ├── _meta.tsx
│ │ │ ├── examples/
│ │ │ │ ├── test-tailwind.mdx
│ │ │ │ └── theme-update.mdx
│ │ │ ├── i18n.mdx
│ │ │ └── index.mdx
│ │ ├── index.mdx
│ │ ├── introduction.mdx
│ │ ├── login.mdx
│ │ └── upgrade.mdx
│ ├── hooks/
│ │ ├── index.ts
│ │ ├── useBreakpoint.ts
│ │ ├── useLocale.ts
│ │ └── useServerLocale.ts
│ ├── i18n/
│ │ ├── ai-demo.ts
│ │ ├── en.ts
│ │ ├── index.ts
│ │ └── zh.ts
│ ├── lib/
│ │ └── utils.ts
│ ├── mdx-components.ts
│ ├── proxy.ts
│ └── widgets/
│ ├── auth-button.tsx
│ ├── locale-toggle.tsx
│ ├── mobile-menu-auth.tsx
│ ├── navbar-extras.tsx
│ └── theme-toggle.tsx
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
.pnpm-store/
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
_pagefind/
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# sitemap and robots.txt
public/robots.txt
public/sitemap.xml
public/sitemap-*.xml
================================================
FILE: .npmrc
================================================
enable-pre-post-scripts=true
store-dir=.pnpm-store/v10
================================================
FILE: .vscode/settings.json
================================================
{
"css.customData": [".vscode/tailwind.json"],
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never",
"source.fixAll.stylelint": "explicit"
},
"eslint.run": "onType",
"eslint.format.enable": true,
"files.autoSaveDelay": 500,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"gql",
"graphql"
],
"javascript.preferences.importModuleSpecifier": "project-relative",
"typescript.suggest.jsdoc.generateReturns": false,
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
================================================
FILE: .vscode/tailwind.json
================================================
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
},
{
"name": "@reference",
"description": "If you want to use @apply or @variant in the <style> block of a Vue or Svelte component, or within CSS modules, you will need to import your theme configuration to make those values available in that context.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#reference-directive"
}
]
}
]
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020-PRESENT Wisdom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README-en.md
================================================
<p style="text-align:center;" align="center"><a href="https://github.com/pdsuwwz/nextjs-nextra-starter"><picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://i.stardots.io/wisdom/1745917125609.png" width="100%" align="center" style="margin-bottom:20px;">
<source media="(prefers-color-scheme: light)" srcset="https://i.stardots.io/wisdom/1745917153483.png" width="100%" align="center" style="margin-bottom:20px;">
<img alt="color mode" src="https://i.stardots.io/wisdom/1745917153483.png" width="100%" align="center" style="margin-bottom:20px;">
</picture></a><br /><br /></p>
# Nextjs Nextra Starter
English | [中文](README.md)
[](https://github.com/pdsuwwz/nextjs-nextra-starter/deployments)
[](https://github.com/pdsuwwz/nextjs-nextra-starter/deployments/Production)
[](https://github.com/pdsuwwz)
[](https://github.com/pdsuwwz/nextjs-nextra-starter/blob/main/LICENSE)
🔥 A Next.js 16 starter for indie developers and small teams: Tailwind CSS 4, React 19, Nextra 4, TypeScript, Shadcn UI, Radix UI, Aceternity UI, Sass, ESLint 9, Iconify, and i18n multilingual support. Built for Blog, Docs, and AI SaaS landing pages with responsive layout, dark mode, login page, and frontend auth examples. Deploy-ready for Vercel and Netlify.
- [🚀 Live Demo](https://nextjs-nextra.netlify.app/en)
- [🤖 AI Demo Landing Page](https://nextjs-nextra.netlify.app/en/ai-demo)
- [✨ Alternative address 1](https://nextjs-nextra-starter-green.vercel.app/en)
- [✨ Alternative address 2](https://nextra.likemashang.com/en)
## 🛠️ Maintenance Commitment
<div align="center">
<table>
<tr>
<td><strong>🔄 Continuous update</strong><br/>Dependency and features are updated irregularly</td>
<td><strong>🐛 Fast Response</strong><br/>Reply within 2 hours on average Issue</td>
</tr>
<tr>
<td><strong>💎 Elaboration</strong><br/>Spend 100+ hours to perfect template details</td>
<td><strong>🛡️ Stable and Reliable</strong><br/>Ensure that each function is fully tested</td>
</tr>
</table>
</div>
<div align="center">
<img src="https://media.giphy.com/media/a5viI92PAF89q/giphy.gif" width="400"/>
💝 **If you appreciate this effort, please show your support with a ⭐ Star.**
</div>
## 🚀 What's New
- **Tailwind CSS v4 Upgrade**: Fully upgraded to Tailwind CSS v4, optimizing performance and introducing new features.
- **Nextra v4 Refactoring**: Upgraded to Nextra v4, enhancing document generation efficiency and development experience.
👉 [Click to view detailed upgrade notes](https://nextjs-nextra.netlify.app/en/upgrade)
## 🎉 Features
- ⚡️ **Next.js 16 + React 19 + TypeScript**: Modern core stack with strong type safety and developer productivity
- 🎨 **Tailwind CSS v4 + Sass**: Utility-first styling with scalable style organization
- 📚 **Nextra v4 (content-driven)**: Great fit for docs, knowledge bases, and blogs
- 🧩 **Shadcn UI + Radix UI + Aceternity UI**: Composable UI system for fast product page building
- 🌍 **i18n multilingual support**: Built-in structure for localized content and routes
- 🌙 **Dark mode + responsive design**: Consistent UX across desktop and mobile
- 🔐 **Login page + frontend auth examples**: Practical auth flow reference for rapid integration
- 🖼️ **Iconify icon support**: Unified icon strategy with low integration cost
- 🛠️ **ESLint v9**: Consistent code quality and team-friendly standards
- 🚀 **Deployment-ready**: Works smoothly with Vercel / Netlify
## 🎯 Use Cases
- Personal blog (Blog Starter Template)
- Developer docs and product documentation sites
- AI product websites and conversion-focused SaaS landing pages
- Personal projects and small team product showcases
## Prerequisites
- React 19.x
- Node >= 20.x
- Pnpm 9.x
- **VS Code plugin `dbaeumer.vscode-eslint` >= v3.0.5 (pre-release)**
## Preview





## Installation and Running
- Install dependencies
```bash
pnpm i
```
- Local development
```bash
pnpm dev
```
Then open http://localhost:8000 in your browser to access the service
🎉 **Successfully running?** If you like the clean setup of this template, don’t forget to show some support!
[](https://github.com/pdsuwwz/nextjs-nextra-starter)
## Using Shadcn UI Components
This project has integrated [Shadcn UI](https://ui.shadcn.com). Follow these steps to install/edit components and use them:
### Shadcn Structure Initialization
Execute `pnpm dlx shadcn@latest init` command to initialize the basic project structure for `Shadcn UI` (if not already initialized)
💡 Note
> This initialization command is used to create the basic project structure for `Shadcn UI`
>
> **This project has already been initialized, so there's no need to run this command again**
### Component Installation
1. Use `Shadcn CLI` to add components:
```bash
pnpm dlx shadcn@latest add <component-name>
```
For example, to add the `<Alert />` component, execute the following command, [see documentation](https://ui.shadcn.com/docs/components/alert#installation)
```bash
pnpm dlx shadcn@latest add alert
```
2. Using components
```tsx
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
export default function Home() {
return (
<Alert>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components and dependencies to your app using the cli.
</AlertDescription>
</Alert>
)
}
```
3. Customizing component styles (optional)
`Shadcn UI` components typically provide popular default styles and functionality that meet most needs. If you truly need to customize, you can edit the respective component files, such as:
Open [`src/components/ui/alert.tsx`](src/components/ui/alert.tsx) to modify the styles of the `Alert` component
> Tips: In most cases, the default styles provided by `Shadcn UI` are sufficient to meet requirements without additional modifications
## 🐱 A Word from the Heart
<div align="center">
If you've made it this far and still haven't starred the repo, then all I can say is...
<img src="https://media.giphy.com/media/l0HlKrB02QY0f1mbm/giphy.gif" width="500"/>
**Pretty please, drop a ⭐ Star!** 🥺👉👈
Right now, my bug count is still higher than my star count 😭
<a href="https://github.com/pdsuwwz/nextjs-nextra-starter">
<img src="https://img.shields.io/badge/Discovered%20with%20care-Drop%20a%20Star%20%E2%AD%90-orange?style=for-the-badge&logo=github&logoColor=white" alt="Give a Star"/>
</a>
</div>
## 🌟 Related Projects
Here are some projects that developers and teams are using, referencing, or inspired by this project:
| Project Name | Description |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| [ClaudeCode101](https://www.claudecode101.com) | A Chinese tutorial site for Claude Code, featuring best practices and hands-on guides. |
| [EdgeOne Saas Starter](https://github.com/TencentEdgeOne/saas-starter) | [The fastest way to create and deploy your SaaS product with EdgeOne and Tencent Cloud](https://saas-starter-docs.edgeone.app/en) |
| [Talking Web3](https://talkingweb3.io/en) | A Web3 project accelerator dedicated to creating outstanding Web3 projects. |
| [CodeCrack](https://www.codecrack.cn/en) | A free and in-depth interview preparation website helping developers improve their technical skills and prepare for interviews. |
### 📢 Community Contributions
💡 If your project is also using or referencing this project, we sincerely welcome you to:
- Share your project link by submitting an [Issue](https://github.com/pdsuwwz/nextjs-nextra-starter/issues)
- Submit a Pull Request (PR) to add your project to the list
## 🚨 Disclaimer
This template is provided as a technical reference solution. Users must acknowledge the following risks and obligations:
- **Technical Risks**:
Dependent frameworks (Next.js/Nextra/Tailwind CSS) carry version iteration risks. Third-party components (e.g. Shadcn UI) follow their original repositories' specifications. Environment configuration changes may cause unforeseen build exceptions
- **Usage Restrictions**:
Prohibited for use in scenarios violating open-source licenses or applicable laws/regulations. Users must conduct independent code security audits and production environment validation
- **Liability Exclusion**:
No guarantees are provided regarding:
1. Business applicability of technical solutions
2. Security assurance of dependencies
3. Official customization support
Users assume full responsibility for any direct/indirect consequences arising from usage or modifications. Continued use constitutes acceptance of these terms
## License
[MIT](./LICENSE) License | Copyright © 2020-PRESENT [Wisdom](https://github.com/pdsuwwz)
================================================
FILE: README.md
================================================
<p style="text-align:center;" align="center"><a href="https://github.com/pdsuwwz/nextjs-nextra-starter"><picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://i.stardots.io/wisdom/1745917125609.png" width="100%" align="center" style="margin-bottom:20px;">
<source media="(prefers-color-scheme: light)" srcset="https://i.stardots.io/wisdom/1745917153483.png" width="100%" align="center" style="margin-bottom:20px;">
<img alt="color mode" src="https://i.stardots.io/wisdom/1745917153483.png" width="100%" align="center" style="margin-bottom:20px;">
</picture></a><br /><br /></p>
# Nextjs Nextra Starter
中文 | [English](README-en.md)
[](https://github.com/pdsuwwz/nextjs-nextra-starter/deployments)
[](https://github.com/pdsuwwz/nextjs-nextra-starter/deployments/Production)
[](https://github.com/pdsuwwz)
[](https://github.com/pdsuwwz/nextjs-nextra-starter/blob/main/LICENSE)
🔥 面向独立开发者与小团队的 Next.js 16 启动模板:集成 Tailwind CSS 4、React 19、Nextra 4、TypeScript、Shadcn UI、Radix UI、Aceternity UI、Sass、ESLint 9、Iconify 与 i18n 多语言。覆盖 Blog、Docs、AI SaaS Landing Page 等核心场景,支持响应式布局、暗黑模式、登录页与前端鉴权示例,兼顾快速起步与长期可维护性,支持 Vercel / Netlify 部署。
- [🚀 Live Demo 在线体验](https://nextjs-nextra.netlify.app/zh)
- [🤖 AI Demo 落地页](https://nextjs-nextra.netlify.app/zh/ai-demo)
- [✨ 备用地址1](https://nextjs-nextra-starter-green.vercel.app/zh)
- [✨ 备用地址2](https://nextra.likemashang.com/zh)
## 🛠️ 项目维护承诺
<div align="center">
<table>
<tr>
<td><strong>🔄 持续更新</strong><br/>不定期更新依赖和功能</td>
<td><strong>🐛 快速响应</strong><br/>平均 2 小时内回复 Issue</td>
</tr>
<tr>
<td><strong>💎 精心打磨</strong><br/>花费 100+ 小时完善模板细节</td>
<td><strong>🛡️ 稳定可靠</strong><br/>确保每个功能都充分测试</td>
</tr>
</table>
</div>
<div align="center">
<img src="https://media.giphy.com/media/a5viI92PAF89q/giphy.gif" width="400"/>
💝 **如果你感受到了这份用心,请用 Star ⭐ 给予支持**
</div>
## 🚀 更新说明
- **Tailwind CSS v4 升级**:全面升级至 Tailwind CSS v4,优化性能并引入新特性。
- **Nextra v4 重构**:升级至 Nextra v4,提升文档生成效率和开发体验。
👉 [点击查看详细升级说明](https://nextjs-nextra.netlify.app/zh/upgrade)
## 🎉 Features
- ⚡️ **Next.js 16 + React 19 + TypeScript**:现代前端核心栈,类型安全与开发效率兼顾
- 🎨 **Tailwind CSS v4 + Sass**:支持原子化样式与工程化样式组织,快速构建响应式 UI
- 📚 **Nextra v4(内容驱动)**:适合文档站、知识库、博客等内容型项目
- 🧩 **Shadcn UI + Radix UI + Aceternity UI**:可组合、可扩展的组件体系,便于快速搭建产品页面
- 🌍 **i18n 多语言国际化**:中英文内容组织与路由支持,适配多语言产品站点
- 🌙 **暗黑模式 + 响应式设计**:覆盖桌面端与移动端体验,支持主题切换
- 🔐 **登录页与前端鉴权示例**:提供基础鉴权流程参考,便于业务快速接入
- 🖼️ **Iconify 图标集支持**:统一图标方案,降低图标接入成本
- 🛠️ **ESLint v9 规范化**:统一代码风格与质量约束,适合团队协作
- 🚀 **部署友好**:开箱支持 Vercel / Netlify 部署
## 🎯 适用场景
- 个人博客(Blog Starter Template)
- 技术文档与产品文档站(Docs Site)
- AI 产品官网与营销落地页(AI SaaS Landing Page)
- 个人项目主页与小团队产品展示站
## 前置条件
- React 19.x
- Node >= 20.x
- Pnpm 9.x
- **VS Code 插件 `dbaeumer.vscode-eslint` >= v3.0.5 (pre-release)**
## 运行效果





## 安装和运行
- 安装依赖
```bash
pnpm i
```
- 本地开发
```bash
pnpm dev
```
接着用浏览器打开 http://localhost:8000 即可访问服务
🎉 **成功运行了?** 如果你喜欢这个模板的简洁配置,别忘了鼓励一下:
[](https://github.com/pdsuwwz/nextjs-nextra-starter)
## 使用 Shadcn UI 组件
本项目已集成 [Shadcn UI](https://ui.shadcn.com), 按照以下步骤安装/编辑组件并使用:
### Shadcn 结构初始化
首次执行 `pnpm dlx shadcn@latest init` 命令初始化 `Shadcn UI` 基本项目结构(如果尚未初始化)
> [!IMPORTANT]
> 该初始化命令用于创建 `Shadcn UI` 的基本项目结构
>
> **本项目已完成初始化,无需再次运行此命令**
### 组件安装
1. 使用 `Shadcn CLI` 添加组件:
```bash
pnpm dlx shadcn@latest add <组件名>
```
如添加 `<Alert />` 组件,执行以下命令即可,[详见文档](https://ui.shadcn.com/docs/components/alert#installation)
```bash
pnpm dlx shadcn@latest add alert
```
2. 使用组件
```tsx
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
export default function Home() {
return (
<Alert>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components and dependencies to your app using the cli.
</AlertDescription>
</Alert>
)
}
```
3. 自定义组件样式(可选)
`Shadcn UI` 组件通常已提供了流行的默认样式和功能,能满足大多数需求,若确实需要进行自定义定制,可编辑相应的组件文件,如:
打开 [`src/components/ui/alert.tsx`](src/components/ui/alert.tsx) 文件来修改 `Alert` 组件的样式
> 注意:在大多数情况下,`Shadcn UI` 提供的默认样式已经足够满足需求,无需进行额外修改
## 🐱 说句心里话
<div align="center">
如果你看到这里还没有点 Star, 那我只能说...
<img src="https://media.giphy.com/media/l0HlKrB02QY0f1mbm/giphy.gif" width="500"/>
**求求了,给个 Star 吧!** 🥺👉👈
我的 Star 数量还不如我的 Bug 数量多 😭
<a href="https://github.com/pdsuwwz/nextjs-nextra-starter">
<img src="https://img.shields.io/badge/%E8%89%AF%E5%BF%83%E5%8F%91%E7%8E%B0-%E8%B5%8F%E4%B8%AAStar%20%E2%AD%90%EF%B8%8F%EF%B8%8F-orange?style=for-the-badge&logo=github&logoColor=white" alt="给个Star"/>
</a>
</div>
## 🌟 相关项目
以下是一些开发者和团队正在使用、参考或受本项目启发的项目:
| 项目名 | 简介 |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| [ClaudeCode101 中文教程](https://www.claudecode101.com/zh) | 面向中文用户的 Claude Code AI 编程助手教程网站,汇集官方最佳实践与社区经验,助你高效掌握 AI 编程技能。 |
| [EdgeOne Saas 模板](https://github.com/TencentEdgeOne/saas-starter) | [腾讯云官方模板:助你快速构建并部署下一款 SaaS 应用](https://saas-starter-docs.edgeone.app/zh) |
| [Talking Web3](https://talkingweb3.io/zh) | 一个 Web3 项目出圈加速器,致力于打造卓越的Web3项目。 |
| [面试宝典](https://www.codecrack.cn/zh) | 一个免费且深入的八股文网站,帮助开发者提升技术能力并应对面试。 |
### 📢 社区贡献
💡 如果您的项目也在使用或借鉴了本项目,我们诚挚欢迎您:
- 通过提交 [Issue](https://github.com/pdsuwwz/nextjs-nextra-starter/issues) 分享您的项目链接
- 提交 Pull Request (PR) 将您的项目添加到列表中
## 🚨 免责声明
本模板作为技术方案参考提供,使用者需知悉以下风险及义务:
- **技术风险**:依赖框架(Next.js/Nextra/Tailwind CSS等)存在版本迭代风险,第三方组件(如 Shadcn UI)的行为规范以原始仓库为准,环境配置变更可能导致不可预见的构建异常
- **使用限制**:禁止用于违反开源协议或法律法规的场景,使用者需自行完成代码安全审查及生产环境验证
- **责任免除**:不承诺技术方案的业务适用性、安全性担保及定制支持,因使用/修改引发的直接或间接后果均由使用者自行承担
## License
[MIT](./LICENSE) License | Copyright © 2020-PRESENT [Wisdom](https://github.com/pdsuwwz)
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/[lang]/styles/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@aceternity": "https://ui.aceternity.com/registry/{name}.json"
}
}
================================================
FILE: eslint.config.js
================================================
import antfu from '@antfu/eslint-config'
const OFF = 0
const WARN = 1
const ERROR = 2
export default antfu({
ignores: [
'public',
'build',
'dist',
'node_modules',
'coverage',
'src/assets/**',
],
stylistic: {
indent: 2,
quotes: 'single',
overrides: {
'antfu/top-level-function': 'off',
'style/arrow-parens': 'off',
curly: 'off',
},
},
jsonc: true,
formatters: {
markdown: true,
},
typescript: true,
react: true,
markdown: true,
extends: [
'next/core-web-vitals',
],
rules: {
'antfu/top-level-function': OFF,
'react-dom/no-unsafe-target-blank': OFF,
'react-dom/no-missing-button-type': OFF,
'react-hooks/exhaustive-deps': WARN,
'react/no-useless-fragment': OFF,
'react/no-array-index-key': OFF,
'react-hooks/rules-of-hooks': OFF,
'react/no-comment-textnodes': OFF,
'react-refresh/only-export-components': OFF,
'react-hooks-extra/no-unnecessary-use-prefix': OFF,
'react-hooks-extra/prefer-use-state-lazy-initialization': OFF,
'react-dom/no-dangerously-set-innerhtml': OFF,
'unused-imports/no-unused-vars': WARN,
curly: [ERROR, 'multi-line', 'consistent'],
'no-multiple-empty-lines': [
ERROR,
{
max: 3,
},
],
'no-console': WARN,
'style/jsx-self-closing-comp': [ERROR, {
component: true,
html: false,
}],
'style/no-multiple-empty-lines': [ERROR, {
max: 2,
maxEOF: 0,
}],
'style/max-statements-per-line': ERROR,
'style/quote-props': [ERROR, 'as-needed'],
'ts/no-use-before-define': OFF,
'ts/ban-ts-comment': OFF,
},
})
================================================
FILE: next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
================================================
FILE: next-sitemap.config.mjs
================================================
/** @type {import('next-sitemap').IConfig} */
export default {
siteUrl: process.env.SITE_URL || 'https://example.com',
generateRobotsTxt: true,
}
================================================
FILE: next.config.ts
================================================
import createWithNextra from 'nextra'
const withNextra = createWithNextra({
defaultShowCopyCode: true,
unstable_shouldAddLocaleToLinks: true,
})
/**
* @type {import("next").NextConfig}
*/
export default withNextra({
images: {
unoptimized: true,
},
reactStrictMode: true,
cleanDistDir: true,
i18n: {
locales: ['zh', 'en'],
defaultLocale: 'zh',
},
sassOptions: {
silenceDeprecations: ['legacy-js-api'],
},
})
================================================
FILE: package.json
================================================
{
"name": "nextjs-nextra-starter",
"type": "module",
"version": "0.0.1",
"description": "Next.js Nextra (v4) Tailwind CSS (v4) docs starter",
"author": "Wisdom <pdsu.wwz@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pdsuwwz/nextjs-nextra-starter#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/pdsuwwz/nextjs-nextra-starter.git"
},
"bugs": {
"url": "https://github.com/pdsuwwz/nextjs-nextra-starter/issues"
},
"engines": {
"node": ">=20.x",
"pnpm": ">=9.x"
},
"scripts": {
"dev": "next dev --turbopack -p 8000",
"build": "next build",
"postbuild": "next-sitemap --config next-sitemap.config.mjs && pagefind --site .next/server/app --output-path public/_pagefind",
"start": "next start -p 7000",
"lint": "eslint --fix ."
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-toggle": "^1.1.10",
"@tsparticles/engine": "^3.9.1",
"@tsparticles/react": "^3.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.38.0",
"lucide-react": "^1.8.0",
"motion": "^12.38.0",
"next": "16.2.4",
"next-sitemap": "^4.2.3",
"next-themes": "^0.4.6",
"nextra": "^4.6.1",
"nextra-theme-docs": "4.6.1",
"qss": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-fast-marquee": "^1.6.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"tsparticles": "^3.9.1"
},
"devDependencies": {
"@antfu/eslint-config": "^8.2.0",
"@eslint-react/eslint-plugin": "^3.0.0",
"@eslint/compat": "^2.0.5",
"@iconify/json": "^2.2.464",
"@iconify/tailwind4": "^1.2.3",
"@next/third-parties": "16.2.4",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4.2.3",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^10.2.1",
"eslint-config-next": "16.2.4",
"eslint-plugin-format": "^2.0.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"pagefind": "^1.5.2",
"postcss": "^8.5.10",
"react-responsive": "^10.0.1",
"sass": "^1.99.0",
"tailwindcss": "^4.2.3",
"typescript": "^5.9.3"
}
}
================================================
FILE: postcss.config.mjs
================================================
/** @type {import('postcss').Postcss} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config
================================================
FILE: src/app/[lang]/[[...mdxPath]]/page.tsx
================================================
import { generateStaticParamsFor, importPage } from 'nextra/pages'
import { useMDXComponents } from '@/mdx-components'
export const generateStaticParams = generateStaticParamsFor('mdxPath')
export async function generateMetadata(props: PageProps) {
const params = await props.params
const { metadata } = await importPage(params.mdxPath, params.lang)
return metadata
}
type PageProps = Readonly<{
params: Promise<{
mdxPath: string[]
lang: string
}>
}>
const Wrapper = useMDXComponents().wrapper
export default async function Page(props: PageProps) {
const params = await props.params
const result = await importPage(params.mdxPath, params.lang)
const { default: MDXContent, toc, metadata, sourceCode } = result
return (
<Wrapper toc={toc} metadata={metadata} sourceCode={sourceCode}>
<MDXContent {...props} params={params} />
</Wrapper>
)
}
================================================
FILE: src/app/[lang]/_components/ThemeProvider.tsx
================================================
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import * as React from 'react'
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
================================================
FILE: src/app/[lang]/_components/ThirdPartyScripts.tsx
================================================
'use client'
import { useEffect } from 'react'
const GA_ID = 'G-VCR6017LB8'
const BAIDU_SRC = 'https://hm.baidu.com/hm.js?d5ad5e04e6af914c01767926567602be'
export default function ThirdPartyScripts() {
useEffect(() => {
if (typeof window === 'undefined') {
return
}
if (!document.querySelector(`script[src*="${GA_ID}"]`)) {
const gtagScript = document.createElement('script')
gtagScript.async = true
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`
document.head.appendChild(gtagScript)
const inline = document.createElement('script')
inline.text = `
window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}');
`
document.head.appendChild(inline)
}
if (!document.querySelector(`script[src="${BAIDU_SRC}"]`)) {
const hmScript = document.createElement('script')
hmScript.src = BAIDU_SRC
document.head.appendChild(hmScript)
}
}, [])
return null
}
================================================
FILE: src/app/[lang]/layout.tsx
================================================
import type { Metadata } from 'next'
import type { I18nLangAsyncProps, I18nLangKeys } from '@/i18n'
import ThirdPartyScripts from './_components/ThirdPartyScripts'
import { Footer, LastUpdated, Layout, Navbar } from 'nextra-theme-docs'
import { Banner, Head, Search } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import { CustomFooter } from '@/components/CustomFooter'
import { Toaster } from '@/components/ui/sonner'
import { useServerLocale } from '@/hooks'
import NavbarExtras from '@/widgets/navbar-extras'
import { getDictionary, getDirection } from '../_dictionaries/get-dictionary'
import './styles/index.css'
export const metadata = {
// Define your metadata here
// For more information on metadata API, see: https://nextjs.org/docs/app/building-your-application/optimizing/metadata
metadataBase: new URL('https://nextjs-nextra-starter-green.vercel.app'),
icons: '/img/favicon.svg',
} satisfies Metadata
const repo = 'https://github.com/pdsuwwz/nextjs-nextra-starter'
const CustomBanner = async ({ lang }: I18nLangAsyncProps) => {
const { t } = await useServerLocale(lang)
return (
<Banner
storageKey="starter-banner"
>
<div className="flex justify-center items-center gap-1">
{ t('banner.title') }
{' '}
<a
className="max-sm:hidden text-warning hover:underline"
target="_blank"
href={repo}
>
{ t('banner.more') }
</a>
</div>
</Banner>
)
}
const CustomNavbar = async ({ lang }: I18nLangAsyncProps) => {
const { t } = await useServerLocale(lang)
return (
<Navbar
logo={(
<span>{ t('systemTitle') }</span>
)}
logoLink={`/${lang}`}
projectLink={repo}
>
<NavbarExtras />
</Navbar>
)
}
const BaiduTrack = () => null
// interface Props {
// children: ReactNode
// params: Promise<{ lang: I18nLangKeys }>
// }
export default async function RootLayout({ children, params }: LayoutProps<'/[lang]'>) {
const getterParams = await params
const { lang } = getterParams as { lang: I18nLangKeys }
const dictionary = await getDictionary(lang)
const pageMap = await getPageMap(lang)
const title = 'My Nextra Starter'
const description = 'A Starter template with Next.js, Nextra'
const { t } = await useServerLocale(lang)
return (
<html
// Not required, but good for SEO
lang={lang}
// Required to be set
// dir="ltr"
// Suggested by `next-themes` package https://github.com/pacocoursey/next-themes#with-app
dir={getDirection(lang)}
suppressHydrationWarning
>
<Head
// ... Your additional head options
>
{/* <title>{asPath !== '/' ? `${normalizePagesResult.title} - ${title}` : title}</title> */}
<meta property="og:title" content={title} />
<meta name="description" content={description} />
<meta property="og:description" content={description} />
<link rel="canonical" href={repo} />
</Head>
<body>
<Layout
copyPageButton={false}
banner={
<CustomBanner lang={lang} />
}
navbar={
<CustomNavbar lang={lang} />
}
lastUpdated={(
<LastUpdated>
{ t('lastUpdated') }
</LastUpdated>
)}
editLink={null}
docsRepositoryBase="https://github.com/pdsuwwz/nextjs-nextra-starter"
footer={(
<Footer className="bg-background py-5!">
<CustomFooter />
</Footer>
)}
search={(
<Search
placeholder={t('search.placeholder')}
emptyResult={t('search.noResults')}
errorText={t('search.errorText')}
loading={t('search.loading')}
/>
)}
i18n={[
{ locale: 'en', name: 'English' },
{ locale: 'zh', name: '简体中文' },
]}
toc={{
backToTop: t('backToTop'),
title: t('pageTitle'),
}}
pageMap={pageMap}
feedback={{ content: '' }}
nextThemes={{
attribute: 'class',
defaultTheme: 'system',
storageKey: 'starter-theme-provider',
disableTransitionOnChange: true,
}}
// ... Your additional layout options
>
{children}
</Layout>
<Toaster position="top-center" />
<ThirdPartyScripts />
</body>
<BaiduTrack />
</html>
)
}
================================================
FILE: src/app/[lang]/not-found.ts
================================================
export { NotFoundPage as default } from 'nextra-theme-docs'
================================================
FILE: src/app/[lang]/styles/index.css
================================================
@import 'tailwindcss';
@import 'nextra-theme-docs/style.css';
@plugin 'tailwindcss-animate';
@plugin "@iconify/tailwind4";
@plugin '@tailwindcss/typography';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-warning: hsl(var(--warning));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--radius-lg: calc(var(--radius) + 2px);
--radius-md: calc(var(--radius));
--radius-sm: calc(var(--radius) - 2px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 1400px) {
max-width: 1400px;
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-warning: var(--warning);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@reference "./overrides.css";
.x\:bg-gray-100 {
@apply bg-background;
}
.nextra-sidebar-footer,
.nextra-navbar-blur {
@apply bg-background;
}
.nextra-mobile-nav .nextra-sidebar-footer {
@apply mx-0 px-4;
}
@keyframes heartbeat {
0%,100% {
transform: scale(.75)
}
20% {
transform: scale(1)
}
40% {
transform: scale(.75)
}
60% {
transform: scale(1)
}
80% {
transform: scale(.75)
}
}
/*
---break---
*/
:root {
--warning: hsl(38.21 94.06% 60.39%);
--background: hsl(0 0% 100%);
--foreground: hsl(0 0% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%);
--primary: hsl(221.2, 83.2%, 53.3%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(210, 40%, 96.1%);
--secondary-foreground: hsl(222.2, 47.4%, 11.2%);
--muted: hsl(0 0% 96.1%);
--muted-foreground: hsl(215.4, 16.3%, 46.9%);
--accent: hsl(0 0% 96.1%);
--accent-foreground: hsl(222.2, 47.4%, 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 89.8%);
--input: hsl(0 0% 89.8%);
--ring: hsl(221.2, 83.2%, 53.3%);
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.6rem;
}
/*
---break---
*/
.dark {
--background: hsl(0 0% 3.9%);
--foreground: hsl(0 0% 98%);
--card: hsl(0 0% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(0 0% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(217.2, 91.2%, 59.8%);
--primary-foreground: hsl(222.2, 47.4%, 11.2%);
--secondary: hsl(217.2, 32.6%, 17.5%);
--secondary-foreground: hsl(210, 40%, 98%);
--muted: hsl(217.2, 32.6%, 17.5%);
--muted-foreground: hsl(215, 20.2%, 65.1%);
--accent: hsl(0 0% 14.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
/*
---break---
*/
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
================================================
FILE: src/app/[lang]/styles/overrides.css
================================================
body {
font-family: 'sans-serif', 'Arial', 'Microsoft Yahei';
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
a,
summary,
button,
input,
[tabindex]:not([tabindex='-1']) {
&:focus-visible {
box-shadow: none;
}
}
.button-switch {
@apply max-md:hidden;
svg {
@apply size-[14px];
}
}
================================================
FILE: src/app/_dictionaries/get-dictionary.ts
================================================
import type Zh from '@/i18n/zh'
import 'server-only'
// We enumerate all dictionaries here for better linting and TypeScript support
// We also get the default import for cleaner types
const dictionaries = {
en: () => import('@/i18n/en'),
zh: () => import('@/i18n/zh'),
} as const satisfies Record<string, () => Promise<{ default: typeof Zh }>>
export const getDictionary = async (
locale: keyof typeof dictionaries,
): Promise<typeof Zh> => (await dictionaries[locale]()).default
export const getDirection = (locale: keyof typeof dictionaries) => {
switch (locale) {
case 'en':
case 'zh':
default:
return 'ltr' as const
}
}
================================================
FILE: src/components/AIDemoLanding/EntryCard.tsx
================================================
'use client'
import { ArrowUpRight } from 'lucide-react'
import { useLocale } from '@/hooks'
export default function EntryCard() {
const { currentLocale } = useLocale()
const isZh = currentLocale === 'zh'
return (
<div className="relative z-2 mx-auto mt-8 w-full max-w-5xl px-6">
<section className="relative overflow-hidden rounded-2xl border border-slate-200/80 bg-white/88 p-5 shadow-[0_16px_34px_-26px_rgba(15,23,42,0.75)] backdrop-blur-sm md:p-6 dark:border-slate-700/80 dark:bg-slate-900/75 dark:shadow-[0_20px_38px_-28px_rgba(0,0,0,0.9)]">
<div className="pointer-events-none absolute -left-16 -top-16 h-40 w-40 rounded-full bg-[#2563EB]/12 blur-3xl dark:bg-cyan-500/10" aria-hidden />
<div className="pointer-events-none absolute -bottom-20 right-0 h-44 w-44 rounded-full bg-[#22C55E]/10 blur-3xl dark:bg-emerald-400/10" aria-hidden />
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px] md:items-center">
<div className="relative">
<span className="inline-flex items-center gap-2 rounded-full border border-blue-200/70 bg-blue-50/80 px-3 py-1 text-xs font-medium text-blue-700 dark:border-cyan-500/25 dark:bg-cyan-500/10 dark:text-cyan-300">
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" aria-hidden />
AI Landing Reference
</span>
<h2 className="text-xl font-semibold tracking-tight text-slate-900 md:text-[1.75rem] md:leading-9 dark:text-slate-100">
{isZh ? '参考案例:AI 产品落地页' : 'Reference: AI Product Landing Page'}
</h2>
<p className="mt-2 text-sm leading-7 text-slate-700 md:text-base dark:text-slate-300">
{isZh
? '这是一个专门设计的落地页参考页,用来展示此模板可实现的页面结构、文案节奏与响应式效果。'
: 'A dedicated reference page showing what this starter can deliver: structure, copy rhythm, and responsive quality.'}
</p>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{isZh ? '包含英文与中文两套内容,可直接用于产品演示。' : 'Includes both English and Chinese content for direct product demos.'}
</p>
</div>
<div className="flex md:justify-end">
<a
href={`/${currentLocale}/ai-demo`}
className="inline-flex w-full min-h-11 items-center justify-center gap-1.5 rounded-xl bg-linear-to-r from-[#2563EB] to-[#1D4ED8] px-4 py-2.5 text-sm font-semibold text-white shadow-[0_14px_28px_-18px_rgba(37,99,235,0.95)] transition duration-300 hover:-translate-y-0.5 hover:shadow-[0_20px_34px_-18px_rgba(37,99,235,0.9)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 dark:from-blue-500 dark:to-cyan-500 dark:focus-visible:ring-cyan-400 md:w-auto"
>
{isZh ? '打开参考页' : 'Open Reference Page'}
<ArrowUpRight className="h-4 w-4" aria-hidden />
</a>
</div>
</div>
</section>
</div>
)
}
================================================
FILE: src/components/AIDemoLanding/index.tsx
================================================
'use client'
import type { ReactNode } from 'react'
import { Bot, CheckCircle2, FileText, GaugeCircle, Layers, ShieldCheck, Sparkles, WandSparkles } from 'lucide-react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { useLocale } from '@/hooks'
import { getLandingCopy, type LandingCopy } from '@/i18n/ai-demo'
import { InteractiveDemoPanel, InteractivePricingCards } from './interactions'
type SectionProps = {
id: string
title: string
children: ReactNode
compact?: boolean
}
const panelClass = 'rounded-2xl border border-slate-200/80 bg-white/85 shadow-[0_14px_40px_-24px_rgba(37,99,235,0.5)] backdrop-blur-sm dark:border-zinc-700/70 dark:bg-zinc-900/80 dark:shadow-[0_16px_44px_-30px_rgba(34,197,94,0.42)]'
const primaryCtaClass = 'inline-flex min-h-11 items-center justify-center rounded-xl bg-gradient-to-r from-[#2563EB] to-[#1D4ED8] px-5 py-2.5 text-sm font-semibold text-white shadow-[0_14px_26px_-18px_rgba(37,99,235,0.95)] transition duration-300 hover:-translate-y-0.5 hover:shadow-[0_20px_32px_-18px_rgba(37,99,235,0.9)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950'
const softCardClass = 'rounded-2xl border border-slate-200/80 bg-white/90 p-5 shadow-[0_10px_24px_-20px_rgba(15,23,42,0.7)] transition duration-300 hover:-translate-y-0.5 hover:shadow-[0_20px_30px_-22px_rgba(37,99,235,0.45)] motion-reduce:transition-none dark:border-zinc-700/80 dark:bg-zinc-900/75 dark:shadow-[0_16px_30px_-26px_rgba(0,0,0,0.8)]'
function Section({ id, title, children, compact }: SectionProps) {
return (
<section id={id} className={compact ? 'py-12' : 'py-16 sm:py-20'} aria-labelledby={`${id}-title`}>
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<h2 id={`${id}-title`} className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-zinc-100">
{title}
</h2>
<div className="mt-8">{children}</div>
</div>
</section>
)
}
function FeatureIcon({ index }: { index: number }) {
const icons = [WandSparkles, Bot, Layers, ShieldCheck, GaugeCircle, FileText]
const Icon = icons[index] ?? CheckCircle2
return <Icon className="h-5 w-5 text-[#2563EB] dark:text-[#60A5FA]" aria-hidden />
}
function TopNav({ copy }: { copy: LandingCopy }) {
return (
<header className="sticky top-0 z-50 border-b border-slate-200/70 bg-white/70 backdrop-blur-xl dark:border-zinc-800/70 dark:bg-zinc-950/75">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6 lg:px-8">
<a href="#top" className="inline-flex items-center gap-2 text-base font-semibold text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 dark:text-zinc-100 dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950">
<span className="inline-flex h-2.5 w-2.5 animate-[heartbeat_3.4s_ease-in-out_infinite] rounded-full bg-[#22C55E]" aria-hidden />
{copy.nav.product}
</a>
<nav aria-label="Primary" className="flex items-center gap-2 sm:gap-3">
<a
href="#demo"
className="rounded-xl border border-slate-300/80 bg-white/70 px-3 py-2 text-sm font-medium text-slate-700 transition duration-200 hover:border-slate-400 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 dark:border-zinc-700 dark:bg-zinc-900/80 dark:text-zinc-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-900 dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950"
>
{copy.nav.tryDemo}
</a>
<a
href="#final-cta"
className="hidden rounded-xl border border-slate-300/80 bg-white/70 px-3 py-2 text-sm font-medium text-slate-700 transition duration-200 hover:border-slate-400 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 sm:inline-flex dark:border-zinc-700 dark:bg-zinc-900/80 dark:text-zinc-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-900 dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950"
>
{copy.nav.bookCall}
</a>
<a href="#pricing" className={primaryCtaClass}>
{copy.nav.startTrial}
</a>
</nav>
</div>
</header>
)
}
function Hero({ copy }: { copy: LandingCopy }) {
return (
<section
id="top"
className="relative overflow-hidden border-b border-slate-200/80 bg-[linear-gradient(150deg,#eff6ff_0%,#ffffff_46%,#ecfdf5_100%)] py-16 sm:py-20 dark:border-zinc-800/80 dark:bg-[linear-gradient(150deg,#0f172a_0%,#111827_45%,#0b1322_100%)]"
aria-labelledby="hero-title"
>
<div className="pointer-events-none absolute -top-24 left-[12%] h-64 w-64 rounded-full bg-[#2563EB]/20 blur-3xl motion-safe:animate-pulse dark:bg-[#38BDF8]/18" aria-hidden />
<div className="pointer-events-none absolute -bottom-24 right-[8%] h-72 w-72 rounded-full bg-[#22C55E]/18 blur-3xl motion-safe:animate-pulse dark:bg-[#34D399]/15" aria-hidden />
<div className="mx-auto grid max-w-6xl gap-10 px-4 sm:px-6 lg:grid-cols-[1.18fr_0.82fr] lg:items-center lg:px-8">
<div className="relative z-10">
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-[#2563EB]/20 bg-white/70 px-3 py-1 text-xs font-medium text-[#1E3A8A] backdrop-blur dark:border-[#60A5FA]/25 dark:bg-zinc-900/60 dark:text-[#93C5FD]">
<Sparkles className="h-3.5 w-3.5" aria-hidden />
AI Workflow Assistant for Lean Teams
</div>
<h1 id="hero-title" className="text-balance text-3xl font-semibold tracking-tight text-slate-900 sm:text-5xl dark:text-zinc-100">
{copy.hero.title}
</h1>
<p className="mt-4 max-w-xl text-base leading-7 text-slate-700 sm:text-lg dark:text-zinc-300">
{copy.hero.subtitle}
</p>
<div className="mt-8 flex flex-wrap gap-3">
<a href="#demo" className={primaryCtaClass}>
{copy.hero.tryDemo}
</a>
<a href="#final-cta" className="inline-flex min-h-11 items-center justify-center rounded-xl border border-slate-300/80 bg-white/80 px-5 py-2.5 text-sm font-semibold text-slate-800 shadow-[0_10px_20px_-18px_rgba(15,23,42,0.8)] transition duration-300 hover:-translate-y-0.5 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900/80 dark:text-zinc-100 dark:hover:bg-zinc-900 dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950">
{copy.hero.bookCall}
</a>
</div>
<p className="mt-3 text-sm font-medium text-slate-600 dark:text-zinc-400">{copy.hero.trust}</p>
</div>
<div className={`${panelClass} relative overflow-hidden p-5 sm:p-6`}>
<div className="absolute -right-10 -top-10 h-32 w-32 animate-[spin_18s_linear_infinite] rounded-full border border-[#2563EB]/20" aria-hidden />
<div className="rounded-xl border border-slate-200/80 bg-white/80 p-4 dark:border-zinc-700/80 dark:bg-zinc-950/80">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-900 dark:text-zinc-100">Pipeline Health</p>
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-100/80 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden />
Live
</span>
</div>
<div className="mt-4 space-y-3 text-sm text-slate-700 dark:text-zinc-300">
<div className="rounded-lg border border-slate-200/70 bg-white/85 p-3 dark:border-zinc-700 dark:bg-zinc-900/80">
<div className="mb-1.5 flex items-center justify-between">
<span>Lead triage</span>
<span className="font-semibold text-[#22C55E]">98% success</span>
</div>
<div className="h-1.5 rounded-full bg-slate-200 dark:bg-zinc-700">
<div className="h-1.5 w-[88%] rounded-full bg-gradient-to-r from-[#22C55E] to-[#16A34A] motion-safe:animate-pulse" />
</div>
</div>
<div className="rounded-lg border border-slate-200/70 bg-white/85 p-3 dark:border-zinc-700 dark:bg-zinc-900/80">
<div className="flex items-center justify-between">
<span>Onboarding emails</span>
<span className="font-semibold text-[#22C55E]">4 min avg</span>
</div>
</div>
<div className="rounded-lg border border-slate-200/70 bg-white/85 p-3 dark:border-zinc-700 dark:bg-zinc-900/80">
<div className="flex items-center justify-between">
<span>Weekly report</span>
<span className="font-semibold text-[#22C55E]">Auto-generated</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
)
}
function SocialProof({ copy }: { copy: LandingCopy }) {
return (
<Section id="social-proof" title={copy.socialProofTitle} compact>
<div className="grid gap-6 lg:grid-cols-[1.12fr_0.88fr]">
<ul className="grid auto-rows-fr grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5" aria-label="Customer logos">
{copy.socialProof.logos.map(logo => (
<li key={logo} className={`${softCardClass} flex min-h-[86px] items-center justify-center px-4 text-center text-sm font-semibold text-slate-700 dark:text-zinc-200`}>
{logo}
</li>
))}
</ul>
<ul className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1" aria-label="Key metrics">
{copy.socialProof.stats.map(stat => (
<li key={stat.label} className={`${softCardClass} min-h-[118px]`}>
<p className="text-3xl font-semibold tracking-tight text-slate-900 dark:text-zinc-100">{stat.value}</p>
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-zinc-400">{stat.label}</p>
</li>
))}
</ul>
</div>
</Section>
)
}
function Features({ copy }: { copy: LandingCopy }) {
return (
<Section id="features" title={copy.featureTitle}>
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{copy.features.map((feature, index) => (
<li key={feature.title} className={`${softCardClass} relative overflow-hidden`}>
<div className="pointer-events-none absolute right-0 top-0 h-24 w-24 rounded-full bg-[#2563EB]/10 blur-2xl dark:bg-[#22C55E]/10" aria-hidden />
<div className="mb-4 inline-flex rounded-xl border border-slate-200/80 bg-slate-50/90 p-2.5 dark:border-zinc-700 dark:bg-zinc-950/80">
<FeatureIcon index={index} />
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-zinc-100">{feature.title}</h3>
<p className="mt-2 text-sm leading-6 text-slate-700 dark:text-zinc-300">{feature.description}</p>
</li>
))}
</ul>
</Section>
)
}
function HowItWorks({ copy }: { copy: LandingCopy }) {
return (
<Section id="how-it-works" title={copy.howItWorksTitle}>
<ol className="grid gap-4 md:grid-cols-3">
{copy.steps.map((step, index) => (
<li key={step.title} className={softCardClass}>
<span className="inline-flex h-7 min-w-7 items-center justify-center rounded-full bg-[#2563EB]/12 px-2 text-xs font-semibold text-[#1D4ED8] dark:bg-[#60A5FA]/20 dark:text-[#93C5FD]">
{index + 1}
</span>
<h3 className="mt-3 text-base font-semibold text-slate-900 dark:text-zinc-100">{step.title}</h3>
<p className="mt-2 text-sm leading-6 text-slate-700 dark:text-zinc-300">{step.description}</p>
</li>
))}
</ol>
</Section>
)
}
function UseCases({ copy }: { copy: LandingCopy }) {
return (
<Section id="use-cases" title={copy.useCasesTitle}>
<ul className="grid gap-4 md:grid-cols-3">
{copy.useCases.map(useCase => (
<li key={useCase.title} className={softCardClass}>
<h3 className="text-base font-semibold text-slate-900 dark:text-zinc-100">{useCase.title}</h3>
<p className="mt-2 text-sm leading-6 text-slate-700 dark:text-zinc-300">{useCase.description}</p>
</li>
))}
</ul>
</Section>
)
}
function DemoPreview({ copy, lang }: { copy: LandingCopy, lang: string }) {
return (
<Section id="demo" title={copy.demoTitle}>
<div className="grid gap-6 lg:grid-cols-[1.3fr_0.7fr] lg:items-center">
<InteractiveDemoPanel lang={lang} ctaText={copy.hero.startTrial} />
<div className={panelClass + ' p-5 sm:p-6'}>
<p className="text-base leading-7 text-slate-700 dark:text-zinc-300">{copy.demoDescription}</p>
<a href="#pricing" className={`${primaryCtaClass} mt-6`}>
{copy.hero.startTrial}
</a>
</div>
</div>
</Section>
)
}
function Testimonials({ copy }: { copy: LandingCopy }) {
return (
<Section id="testimonials" title={copy.testimonialsTitle} compact>
<ul className="grid gap-4 md:grid-cols-2">
{copy.testimonials.map(item => (
<li key={item.name} className={softCardClass}>
<blockquote className="text-base leading-7 text-slate-700 dark:text-zinc-300">“{item.quote}”</blockquote>
<p className="mt-4 text-sm font-semibold text-slate-900 dark:text-zinc-100">{item.name}</p>
<p className="text-sm text-slate-600 dark:text-zinc-400">{item.role}</p>
</li>
))}
</ul>
</Section>
)
}
function Pricing({ copy, lang }: { copy: LandingCopy, lang: string }) {
return (
<Section id="pricing" title={copy.pricingTitle}>
<p className="mb-6 text-sm text-slate-600 dark:text-zinc-400">{copy.pricingSubtitle}</p>
<InteractivePricingCards lang={lang} plans={copy.plans} />
</Section>
)
}
function FAQ({ copy }: { copy: LandingCopy }) {
return (
<Section id="faq" title={copy.faqTitle}>
<Accordion type="single" collapsible className={`${panelClass} px-5`}>
{copy.faqs.map((faq, index) => (
<AccordionItem key={faq.question} value={`faq-${index}`}>
<AccordionTrigger className="text-left text-base font-medium text-slate-900 dark:text-zinc-100">
{faq.question}
</AccordionTrigger>
<AccordionContent className="text-sm leading-7 text-slate-700 dark:text-zinc-300">
{faq.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</Section>
)
}
function FinalCta({ copy }: { copy: LandingCopy }) {
return (
<section
id="final-cta"
className="relative overflow-hidden border-y border-slate-200/80 bg-[linear-gradient(150deg,#eff6ff_0%,#ffffff_46%,#ecfdf5_100%)] py-16 dark:border-zinc-800/80 dark:bg-[linear-gradient(150deg,#0f172a_0%,#111827_45%,#0b1322_100%)]"
aria-labelledby="final-cta-title"
>
<div className="pointer-events-none absolute left-1/2 top-0 h-48 w-48 -translate-x-1/2 rounded-full bg-[#2563EB]/20 blur-3xl motion-safe:animate-pulse dark:bg-[#34D399]/12" aria-hidden />
<div className="mx-auto max-w-6xl px-4 text-center sm:px-6 lg:px-8">
<h2 id="final-cta-title" className="text-balance text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-zinc-100">
{copy.finalCta.title}
</h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-7 text-slate-700 dark:text-zinc-300">
{copy.finalCta.description}
</p>
<div className="mt-8 flex flex-wrap justify-center gap-3">
<a href="#top" className={primaryCtaClass}>
{copy.finalCta.primary}
</a>
<a href={`mailto:${copy.footer.contactEmail}`} className="inline-flex min-h-11 items-center justify-center rounded-xl border border-slate-300/80 bg-white/80 px-5 py-2.5 text-sm font-semibold text-slate-800 shadow-[0_10px_20px_-18px_rgba(15,23,42,0.8)] transition duration-300 hover:-translate-y-0.5 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900/80 dark:text-zinc-100 dark:hover:bg-zinc-900 dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950">
{copy.finalCta.secondary}
</a>
</div>
</div>
</section>
)
}
function Footer({ copy }: { copy: LandingCopy }) {
return (
<footer className="py-10" aria-label="Footer">
<div className="mx-auto flex max-w-6xl flex-col gap-2 px-4 text-sm text-slate-600 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:px-8 dark:text-zinc-400">
<p>
{copy.footer.productName}
{' · '}
{copy.footer.copyright}
</p>
<p>
{copy.footer.contactTitle}
{': '}
<a className="font-medium text-slate-700 transition-colors hover:text-[#2563EB] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 dark:text-zinc-300 dark:hover:text-[#60A5FA] dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950" href={`mailto:${copy.footer.contactEmail}`}>
{copy.footer.contactEmail}
</a>
{' · '}
<a className="font-medium text-slate-700 transition-colors hover:text-[#2563EB] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 dark:text-zinc-300 dark:hover:text-[#60A5FA] dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-950" href={`tel:${copy.footer.contactPhone.replace(/\s+/g, '')}`}>
{copy.footer.contactPhone}
</a>
</p>
</div>
</footer>
)
}
export default function AIDemoLanding() {
const { currentLocale } = useLocale()
const copy = getLandingCopy(currentLocale)
return (
<main className="relative left-1/2 w-dvw -translate-x-1/2 overflow-x-clip bg-white text-slate-900 dark:bg-zinc-950 dark:text-zinc-100">
<TopNav copy={copy} />
<Hero copy={copy} />
<SocialProof copy={copy} />
<Features copy={copy} />
<HowItWorks copy={copy} />
<UseCases copy={copy} />
<DemoPreview copy={copy} lang={currentLocale} />
<Testimonials copy={copy} />
<Pricing copy={copy} lang={currentLocale} />
<FAQ copy={copy} />
<FinalCta copy={copy} />
<Footer copy={copy} />
</main>
)
}
================================================
FILE: src/components/AIDemoLanding/interactions.tsx
================================================
'use client'
import { useMemo, useState } from 'react'
import { CheckCircle2 } from 'lucide-react'
import type { Plan } from '@/i18n/ai-demo'
type BillingCycle = 'monthly' | 'yearly'
type InteractiveDemoProps = {
lang: string
ctaText: string
}
type InteractivePricingProps = {
lang: string
plans: Plan[]
}
type Scenario = {
key: string
label: string
summary: string
metrics: Array<{ label: string, value: string }>
}
function getDemoScenarios(lang: string): Scenario[] {
if (lang === 'zh') {
return [
{
key: 'support',
label: '客服分诊',
summary: '自动分类工单、提取摘要,并分发给对应处理人。',
metrics: [
{ label: '平均首响时间', value: '-38%' },
{ label: '自动分发准确率', value: '96%' },
{ label: '人工预处理耗时', value: '-4.2h/周' },
],
},
{
key: 'reporting',
label: '周报生成',
summary: '定时汇总核心指标,自动输出可执行周报结论。',
metrics: [
{ label: '周报产出时间', value: '-72%' },
{ label: '数据同步时效', value: 'T+0' },
{ label: '跨团队对齐效率', value: '+2.1x' },
],
},
{
key: 'launch',
label: '上线协同',
summary: '自动推进素材、审批与发布清单,降低上线遗漏。',
metrics: [
{ label: '发布延期率', value: '-29%' },
{ label: '清单遗漏项', value: '-64%' },
{ label: '协作往返沟通', value: '-31%' },
],
},
]
}
return [
{
key: 'support',
label: 'Support Triage',
summary: 'Classify tickets, generate summaries, and route to the right owner automatically.',
metrics: [
{ label: 'First response time', value: '-38%' },
{ label: 'Auto-routing accuracy', value: '96%' },
{ label: 'Manual prep time', value: '-4.2h/week' },
],
},
{
key: 'reporting',
label: 'Weekly Reporting',
summary: 'Collect key metrics on schedule and draft action-ready weekly reports.',
metrics: [
{ label: 'Reporting time', value: '-72%' },
{ label: 'Data freshness', value: 'T+0' },
{ label: 'Team alignment speed', value: '+2.1x' },
],
},
{
key: 'launch',
label: 'Launch Ops',
summary: 'Coordinate assets, approvals, and checklists with fewer missed steps.',
metrics: [
{ label: 'Launch delay rate', value: '-29%' },
{ label: 'Checklist misses', value: '-64%' },
{ label: 'Back-and-forth handoffs', value: '-31%' },
],
},
]
}
function parsePriceNumber(price: string): number | null {
const match = price.match(/\d+(?:\.\d+)?/)
if (!match) {
return null
}
return Number(match[0])
}
function formatPlanPrice(price: string, cycle: BillingCycle, lang: string): string {
if (price === 'Custom' || price === '定制') {
return price
}
const value = parsePriceNumber(price)
if (value === null) {
return price
}
const isDollar = price.includes('$')
const isYuan = price.includes('¥')
const prefix = isDollar ? '$' : isYuan ? '¥' : ''
if (cycle === 'monthly') {
return lang === 'zh' ? `${prefix}${value}/月` : `${prefix}${value}/mo`
}
const yearly = Math.round(value * 10)
return lang === 'zh' ? `${prefix}${yearly}/年` : `${prefix}${yearly}/yr`
}
export function InteractiveDemoPanel({ lang, ctaText }: InteractiveDemoProps) {
const scenarios = useMemo(() => getDemoScenarios(lang), [lang])
const [activeKey, setActiveKey] = useState(scenarios[0]?.key ?? 'support')
const activeScenario = scenarios.find(item => item.key === activeKey) ?? scenarios[0]
if (!activeScenario) {
return null
}
return (
<div className="relative overflow-hidden rounded-2xl border border-slate-200/80 bg-white/90 p-4 shadow-[0_14px_40px_-24px_rgba(37,99,235,0.5)] backdrop-blur-sm dark:border-zinc-700/80 dark:bg-zinc-900/80 dark:shadow-[0_16px_44px_-30px_rgba(34,197,94,0.42)] sm:p-5">
<div className="pointer-events-none absolute -right-10 -top-10 h-28 w-28 rounded-full bg-[#2563EB]/12 blur-2xl motion-safe:animate-pulse dark:bg-[#22C55E]/10" aria-hidden />
<div className="mb-4 flex flex-wrap gap-2" role="tablist" aria-label={lang === 'zh' ? '场景切换' : 'Scenario switcher'}>
{scenarios.map(scenario => (
<button
key={scenario.key}
type="button"
role="tab"
aria-selected={activeScenario.key === scenario.key}
className={[
'rounded-xl border px-3 py-1.5 text-sm font-medium transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 motion-reduce:transition-none dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-900',
activeScenario.key === scenario.key
? 'border-[#2563EB]/40 bg-gradient-to-r from-blue-50 to-blue-100/70 text-[#1D4ED8] shadow-[0_10px_22px_-20px_rgba(37,99,235,0.9)] dark:border-[#60A5FA]/40 dark:bg-[#1E3A8A]/25 dark:text-[#93C5FD]'
: 'border-slate-300/80 bg-white/85 text-slate-700 hover:border-slate-400 hover:bg-white dark:border-zinc-700 dark:bg-zinc-900/80 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-900',
].join(' ')}
onClick={() => setActiveKey(scenario.key)}
>
{scenario.label}
</button>
))}
</div>
<div className="rounded-xl border border-slate-200/80 bg-slate-50/80 p-4 dark:border-zinc-700/80 dark:bg-zinc-950/70">
<p className="text-sm text-slate-700 dark:text-zinc-300">{activeScenario.summary}</p>
<ul className="mt-4 space-y-2">
{activeScenario.metrics.map(metric => (
<li key={metric.label} className="flex items-center justify-between rounded-xl border border-slate-200/80 bg-white/90 px-3 py-2 text-sm shadow-[0_10px_20px_-22px_rgba(15,23,42,0.8)] transition duration-300 hover:-translate-y-0.5 hover:shadow-[0_16px_28px_-22px_rgba(37,99,235,0.5)] motion-reduce:transition-none dark:border-zinc-700 dark:bg-zinc-900/85">
<span className="text-slate-600 dark:text-zinc-400">{metric.label}</span>
<span className="font-semibold text-slate-900 dark:text-zinc-100">{metric.value}</span>
</li>
))}
</ul>
<a
href="#pricing"
className="mt-5 inline-flex min-h-11 items-center justify-center rounded-xl bg-gradient-to-r from-[#2563EB] to-[#1D4ED8] px-4 py-2.5 text-sm font-semibold text-white shadow-[0_14px_26px_-18px_rgba(37,99,235,0.95)] transition duration-300 hover:-translate-y-0.5 hover:shadow-[0_20px_32px_-18px_rgba(37,99,235,0.9)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-900"
>
{ctaText}
</a>
</div>
</div>
)
}
export function InteractivePricingCards({ lang, plans }: InteractivePricingProps) {
const [cycle, setCycle] = useState<BillingCycle>('monthly')
return (
<>
<div className="mb-5 inline-flex rounded-xl border border-slate-300/80 bg-white/85 p-1 shadow-[0_10px_18px_-18px_rgba(15,23,42,0.75)] dark:border-zinc-700/80 dark:bg-zinc-900/75" role="group" aria-label={lang === 'zh' ? '计费周期' : 'Billing cycle'}>
<button
type="button"
onClick={() => setCycle('monthly')}
className={[
'rounded-lg px-3 py-1.5 text-sm font-medium transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 motion-reduce:transition-none dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-900',
cycle === 'monthly' ? 'bg-slate-100 text-slate-900 dark:bg-zinc-800 dark:text-zinc-100' : 'text-slate-600 hover:bg-slate-50 dark:text-zinc-400 dark:hover:bg-zinc-800',
].join(' ')}
aria-pressed={cycle === 'monthly'}
>
{lang === 'zh' ? '月付' : 'Monthly'}
</button>
<button
type="button"
onClick={() => setCycle('yearly')}
className={[
'rounded-lg px-3 py-1.5 text-sm font-medium transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 motion-reduce:transition-none dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-900',
cycle === 'yearly' ? 'bg-slate-100 text-slate-900 dark:bg-zinc-800 dark:text-zinc-100' : 'text-slate-600 hover:bg-slate-50 dark:text-zinc-400 dark:hover:bg-zinc-800',
].join(' ')}
aria-pressed={cycle === 'yearly'}
>
{lang === 'zh' ? '年付(约 2 个月优惠)' : 'Yearly (approx. 2 months free)'}
</button>
</div>
<ul className="grid gap-4 lg:grid-cols-3">
{plans.map(plan => (
<li
key={plan.name}
className={[
'relative overflow-hidden rounded-2xl border bg-white/90 p-5 shadow-[0_14px_30px_-24px_rgba(15,23,42,0.75)] transition duration-300 hover:-translate-y-1 hover:shadow-[0_22px_40px_-28px_rgba(37,99,235,0.5)] motion-reduce:transition-none dark:bg-zinc-900/75 dark:shadow-[0_16px_32px_-28px_rgba(0,0,0,0.85)]',
plan.highlight ? 'border-[#2563EB]/45 dark:border-[#60A5FA]/45' : 'border-slate-200/80 dark:border-zinc-700/80',
].join(' ')}
>
{plan.highlight && (
<span className="mb-3 inline-flex rounded-full border border-[#2563EB]/25 bg-[#2563EB]/10 px-2.5 py-1 text-xs font-semibold text-[#1D4ED8] dark:border-[#60A5FA]/30 dark:bg-[#60A5FA]/15 dark:text-[#93C5FD]">
{lang === 'zh' ? '推荐方案' : 'Most Popular'}
</span>
)}
<div className="pointer-events-none absolute -right-12 -top-12 h-32 w-32 rounded-full bg-[#2563EB]/10 blur-3xl dark:bg-[#22C55E]/10" aria-hidden />
<h3 className="text-lg font-semibold text-slate-900 dark:text-zinc-100">{plan.name}</h3>
<p className="mt-1 text-2xl font-semibold text-slate-900 dark:text-zinc-100">{formatPlanPrice(plan.price, cycle, lang)}</p>
<p className="mt-2 text-sm text-slate-700 dark:text-zinc-300">{plan.description}</p>
<ul className="mt-4 space-y-2">
{plan.points.map(point => (
<li key={point} className="flex items-start gap-2 text-sm text-slate-700 dark:text-zinc-300">
<CheckCircle2 className="mt-0.5 h-4 w-4 text-[#22C55E]" aria-hidden />
<span>{point}</span>
</li>
))}
</ul>
<a
href="#final-cta"
className={[
'mt-5 inline-flex min-h-11 items-center justify-center rounded-xl px-4 py-2.5 text-sm font-semibold transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB] focus-visible:ring-offset-2 motion-reduce:transition-none dark:focus-visible:ring-[#60A5FA] dark:focus-visible:ring-offset-zinc-900',
plan.highlight
? 'bg-gradient-to-r from-[#2563EB] to-[#1D4ED8] text-white shadow-[0_14px_26px_-18px_rgba(37,99,235,0.95)] hover:-translate-y-0.5 hover:shadow-[0_20px_32px_-18px_rgba(37,99,235,0.9)]'
: 'border border-slate-300/80 bg-white/80 text-slate-800 hover:-translate-y-0.5 hover:bg-white dark:border-zinc-700 dark:bg-zinc-900/80 dark:text-zinc-200 dark:hover:bg-zinc-900',
].join(' ')}
>
{plan.cta}
</a>
</li>
))}
</ul>
</>
)
}
================================================
FILE: src/components/CustomFooter/index.tsx
================================================
import type { ReactNode } from 'react'
import Link from 'next/link'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import LocaleToggle from '@/widgets/locale-toggle'
import ThemeToggle from '@/widgets/theme-toggle'
const UnderlineLink = ({
link,
label,
underlineByDefault = false,
}: {
label: ReactNode | string
link: string
underlineByDefault?: boolean
}) => {
return (
<Link
href={link}
target="_blank"
className={cn(
'flex items-center rounded-none border border-transparent',
'dark:text-zinc-300',
'duration-200',
'hover:border-b-zinc-600',
'dark:hover:border-b-zinc-300',
underlineByDefault
? `border-b border-b-zinc-400/[0.3] dark:border-b-zinc-500`
: 'hover:border-b',
)}
>
{ label }
</Link>
)
}
export function CustomFooter() {
return (
<div className="w-full flex justify-center items-center">
<div className={cn(
'flex justify-center items-center gap-[2px]',
'max-sm:flex-col max-sm:gap-5 max-sm:pb-10',
'tracking-wide text-[15px] text-center group',
'text-gray-500/[0.8] dark:text-zinc-300/[0.8]',
)}
>
<UnderlineLink
link="https://creativecommons.org/licenses/by-nc-sa/4.0/"
label="CC BY-NC-SA 4.0"
underlineByDefault
/>
<div className="flex items-center gap-[2px]">
<span className="pl-[4px]">
Copyright ©
{' '}
{ new Date().getFullYear() }
</span>
<UnderlineLink
link="https://github.com/pdsuwwz"
label={(
<>
<span className="animate-[heartbeat_1.5s_infinite] mr-[3px]">❤️</span>
{' '}
Wisdom
</>
)}
/>
</div>
<Separator
orientation="vertical"
className="max-sm:hidden h-5 mx-2"
/>
<div className="flex justify-center h-5 items-center space-x-2 text-sm">
<ThemeToggle />
<Separator orientation="vertical" />
<LocaleToggle />
</div>
</div>
</div>
)
}
================================================
FILE: src/components/HomepageHero/Section.tsx
================================================
import type { ReactNode } from 'react'
import { MotionWrapperFadeIn, MotionWrapperFlash } from '@/components/MotionWrapper'
import { cn } from '@/lib/utils'
interface Props {
title?: string
titleProps?: Partial<React.ComponentProps<typeof MotionWrapperFlash>>
description?: string
children?: ReactNode
className?: string
tallPaddingY?: boolean
}
export const Section = (props: Props) => {
const {
className,
titleProps,
title,
description,
children,
tallPaddingY = false,
} = props
return (
<section className={cn(
'flex flex-col items-center justify-center px-6',
className,
)}
>
<MotionWrapperFlash
{
...titleProps
}
>
<h2 className={cn(
'relative',
'text-center font-semibold',
'bg-clip-text text-transparent bg-linear-to-b',
'text-3xl md:text-5xl md:leading-tight pt-5',
'from-slate-700 to-slate-900',
'dark:from-slate-200 dark:to-white',
`${tallPaddingY ? 'pt-20 pb-10' : ''}`,
)}
>
<span>{ title }</span>
</h2>
</MotionWrapperFlash>
<MotionWrapperFadeIn>
{
description
&& (
<h2 className="text-sm md:text-base max-w-4xl my-4 mx-auto text-center font-normal text-zinc-600 dark:text-zinc-400">
{ description }
</h2>
)
}
</MotionWrapperFadeIn>
{children}
</section>
)
}
================================================
FILE: src/components/HomepageHero/Setup.tsx
================================================
'use client'
import clsx from 'clsx'
import Link from 'next/link'
import styles from '@/components/HomepageHero/SetupHero.module.css'
import { MotionWrapperFlash } from '@/components/MotionWrapper/Flash'
import { Button } from '@/components/ui/button'
import { FlipWords } from '@/components/ui/flip-words'
import { LinkPreview } from '@/components/ui/link-preview'
import { useLocale } from '@/hooks'
interface Props {
}
export function SetupHero(props: Props) {
const { t, currentLocale } = useLocale()
return (
<div className={styles.container}>
<div className={styles.glowA} aria-hidden />
<div className={styles.glowB} aria-hidden />
<div className={styles.content}>
<div className={styles.badgeContainer}>
<a
className={styles.badge}
href="https://github.com/pdsuwwz/nextjs-nextra-starter"
target="_blank"
rel="noopener noreferrer"
>
{t('badgeTitle')}
</a>
</div>
<h1 className={styles.headline}>
<MotionWrapperFlash
disabledAnimation={false}
className="flex items-center"
>
<span className="icon-[emojione-v1--lightning-mood]"></span>
</MotionWrapperFlash>
{' '}
Nextra
{' '}
<br className="sm:hidden"></br>
{' '}
Starter
<br className="sm:hidden"></br>
{' '}
Template
</h1>
<Link
href={`/${currentLocale}/upgrade`}
className={clsx([
'text-sm mt-3 inline-flex items-center rounded-xl px-3.5 py-1.5',
'border border-blue-200/80 bg-blue-50/80 text-blue-700 shadow-[0_12px_22px_-18px_rgba(37,99,235,0.8)] backdrop-blur-sm',
'dark:border-cyan-500/25 dark:bg-cyan-500/10 dark:text-cyan-300 dark:shadow-[0_14px_24px_-20px_rgba(34,211,238,0.65)]',
'[&>span]:font-semibold',
'transition duration-300 hover:-translate-y-0.5',
])}
dangerouslySetInnerHTML={{
__html: t('featureSupport', {
feature: `<span>Tailwind CSS v4, Nextra v4</span>`,
}),
}}
/>
<div className={clsx([
styles.subtitle,
'text-neutral-500 dark:text-neutral-300',
])}
>
Template made
{' '}
<FlipWords
words={[
'Fast',
'Simple',
'Modern',
'Flexible',
'Easy',
'Functional',
'Efficient',
'Scalable',
'Reusable',
]}
/>
<br />
With
{' '}
<LinkPreview
url="https://nextjs.org"
>
Next.js
</LinkPreview>
,
{' '}
<LinkPreview
url="https://tailwindcss.com"
>
Tailwind CSS
</LinkPreview>
, and
{' '}
<LinkPreview
url="https://ui.shadcn.com"
>
Shadcn UI
</LinkPreview>
{', '}
<LinkPreview
url="https://ui.aceternity.com"
>
Aceternity UI
</LinkPreview>
</div>
<div className="flex justify-center pt-10">
<div className="max-w-[500px] flex flex-wrap gap-[20px] max-sm:justify-center">
<Button
asChild
size="lg"
className="font-semibold group rounded-xl bg-linear-to-r from-blue-600 to-indigo-600 text-white shadow-[0_16px_30px_-20px_rgba(37,99,235,0.9)] transition duration-300 hover:-translate-y-0.5 hover:from-blue-700 hover:to-indigo-700 hover:text-white dark:from-blue-500 dark:to-cyan-500 dark:hover:from-blue-400 dark:hover:to-cyan-400 dark:text-white dark:hover:text-white max-sm:w-[100%]"
>
<Link
href={`/${currentLocale}/introduction`}
>
{t('getStarted')}
<span className="w-[20px] translate-x-[6px] transition-all group-hover:translate-x-[10px] icon-[mingcute--arrow-right-fill]"></span>
</Link>
</Button>
<Button
asChild
size="lg"
variant="secondary"
className="font-semibold group rounded-xl border border-slate-300/80 bg-white/85 shadow-[0_12px_22px_-18px_rgba(15,23,42,0.7)] transition duration-300 hover:-translate-y-0.5 hover:bg-white dark:border-zinc-700 dark:bg-zinc-900/80 dark:hover:bg-zinc-900 max-sm:w-[100%]"
>
<Link
href="https://github.com/pdsuwwz/nextjs-nextra-starter"
target="_blank"
>
Github
<span className="ml-[6px] icon-[mingcute--github-line]"></span>
</Link>
</Button>
</div>
</div>
</div>
</div>
)
}
================================================
FILE: src/components/HomepageHero/SetupHero.module.css
================================================
@reference "tailwindcss";
.container {
position: relative;
}
.glowA {
position: absolute;
top: 1.5rem;
left: 8%;
width: 18rem;
height: 18rem;
border-radius: 9999px;
background: radial-gradient(circle, rgba(37, 99, 235, 0.18) 0%, rgba(37, 99, 235, 0) 70%);
filter: blur(20px);
pointer-events: none;
}
.glowB {
position: absolute;
right: 5%;
top: 3rem;
width: 20rem;
height: 20rem;
border-radius: 9999px;
background: radial-gradient(circle, rgba(34, 197, 94, 0.14) 0%, rgba(34, 197, 94, 0) 72%);
filter: blur(24px);
pointer-events: none;
:global(.dark) & {
background: radial-gradient(circle, rgba(34, 211, 238, 0.16) 0%, rgba(34, 211, 238, 0) 72%);
}
}
.content {
margin: 0 auto;
position: relative;
z-index: 2;
padding-left: max(env(safe-area-inset-left), 1.5rem);
padding-right: max(env(safe-area-inset-right), 1.5rem);
max-width: 90rem;
text-align: center;
@apply pb-12 md:pb-[100px];
}
.badgeContainer {
@apply mt-8 md:mt-16;
}
.badge {
padding: 0.45rem 0.95rem;
border-radius: 2em;
border: 1px solid hsl(214 32% 75%);
background: rgba(255, 255, 255, 0.72);
color: hsl(218 62% 31%);
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
user-select: none;
transition: all 0.2s ease;
backdrop-filter: blur(8px);
&:hover {
border-color: hsl(214 52% 55%);
box-shadow: 0 14px 24px -20px rgba(37, 99, 235, 0.85);
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
border-color: hsl(214 32% 75%);
}
&:focus-visible {
outline: 2px solid
hsl(var(--nextra-primary-hue) var(--nextra-primary-saturation) 77%);
outline-offset: 2px;
}
:global(.dark) & {
background: rgba(20, 23, 34, 0.72);
color: hsl(210 50% 74%);
border: 1px solid hsl(220 25% 34%);
text-shadow: 0 1px 1px #000;
&:hover {
background: rgba(20, 23, 34, 0.9);
border-color: hsl(197 40% 46%);
box-shadow: 0 14px 24px -20px rgba(34, 211, 238, 0.8);
}
&:active {
border-color: hsl(220 25% 34%);
}
}
}
.headline {
margin-top: 1.5rem;
background-image: linear-gradient(138deg, #0f172a 0%, #1d4ed8 40%, #22c55e 95%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: 3.125rem;
font-size: min(4rem, max(8vw, 2.8rem));
font-weight: 700;
font-feature-settings: initial;
line-height: 1.05;
letter-spacing: -0.12rem;
:global(.dark) & {
background-image: linear-gradient(136deg, #dbeafe 0%, #7dd3fc 42%, #86efac 100%);
}
@apply flex max-lg:flex max-lg:flex-col items-center justify-center;
}
.subtitle {
margin-top: 1.25em;
font-size: 1.2rem;
font-size: min(1.2rem, max(3.2vw, 1.05rem));
font-feature-settings: initial;
font-weight: 450;
line-height: 1.6;
}
.actions {
margin-top: 1.6em;
margin-bottom: 1.4em;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
font-size: min(1.3rem, max(3.5vw, 1.1rem));
font-weight: 500;
}
================================================
FILE: src/components/HomepageHero/index.tsx
================================================
'use client'
import { useMemo } from 'react'
import Marquee from 'react-fast-marquee'
import EntryCard from '@/components/AIDemoLanding/EntryCard'
import { PanelParticles } from '@/components/PanelParticles'
import ScrollProgressBar from '@/components/ScrollProgressBar'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { HoverEffect } from '@/components/ui/card-hover-effect'
import { useLocale } from '@/hooks'
import { cn } from '@/lib/utils'
import { Section } from './Section'
import { SetupHero } from './Setup'
export const StackItem = ({
className,
}: {
className: string
},
) => {
return (
<div className={cn(
'mx-6 size-[50px]',
'text-neutral-800 dark:text-neutral-100',
'transition-all duration-300 transform opacity-75',
'hover:scale-125 hover:opacity-100',
className,
)}
>
</div>
)
}
export default function HomepageHero() {
const { t, currentLocale } = useLocale()
const featureList = t('featureList')
const faqs = t('faqs')
const homeEnhance = t('homeEnhance') as {
quickStatsTitle: string
quickStatsDesc: string
quickStats: Array<{ value: string, label: string }>
useCasesTitle: string
useCases: Array<{ title: string, description: string, tag: string }>
flowTitle: string
flow: Array<{ title: string, description: string }>
ctaTitle: string
ctaDescription: string
ctaPrimary: string
ctaSecondary: string
}
const processedFeatureList = useMemo(() => {
const icons = [
'icon-[material-symbols--rocket-launch-outline]',
'icon-[icon-park-outline--international]',
'icon-[nonicons--typescript-16]',
'icon-[carbon--face-satisfied] hover:icon-[carbon--face-wink]',
'icon-[teenyicons--tailwind-outline]',
'icon-[tabler--calendar-code]',
'icon-[carbon--color-palette]',
'icon-[carbon--ibm-cloud-transit-gateway]',
'icon-[carbon--flash]',
]
return featureList.map((item, index) => {
return {
...item,
icon: <span className={icons[index] || icons[0]}></span>,
}
})
}, [featureList])
return (
<>
<ScrollProgressBar />
<PanelParticles />
<SetupHero />
<EntryCard />
{/* <div className="relative top-[-18px] mb-[-10px] flex justify-center py-[0px] z-2">
<a
href="https://nextjs.org"
target="_blank"
rel="noopener noreferrer"
className="w-[150px] h-[40px] flex flex-col items-center gap-[20px]"
>
<img
className="dark:invert"
src="/next.svg"
style={{ width: '100%', height: 'auto' }}
/>
</a>
</div> */}
<div className="relative z-1 pb-10 md:pb-[100px]">
<Section
title="Tech Stack"
titleProps={{
disabledAnimation: false,
}}
>
<div className="flex justify-center w-full max-w-7xl h-[96px] my-[30px] rounded-2xl bg-transparent px-3">
<Marquee
pauseOnHover
autoFill
gradient
direction="right"
gradientColor="var(--background)"
speed={60}
>
<StackItem className="icon-[akar-icons--nextjs-fill]" />
<StackItem className="icon-[simple-icons--react]" />
<StackItem className="icon-[simple-icons--tailwindcss]" />
<StackItem className="icon-[teenyicons--framer-outline]" />
<StackItem className="icon-[simple-icons--shadcnui]" />
<StackItem className="icon-[simple-icons--typescript]" />
<StackItem className="icon-[fa6-brands--sass]" />
<StackItem className="icon-[teenyicons--eslint-outline]" />
<StackItem className="icon-[simple-icons--postcss]" />
<StackItem className="icon-[simple-icons--nextra]" />
<StackItem className="icon-[line-md--iconify1]" />
</Marquee>
</div>
</Section>
<Section
title="Features"
description={t('featuresDesc')}
>
<div className="flex justify-center w-full max-w-7xl">
<HoverEffect items={processedFeatureList} />
</div>
</Section>
<Section
title={homeEnhance.quickStatsTitle}
description={homeEnhance.quickStatsDesc}
className="pt-8 md:pt-14"
>
<div className="w-full max-w-6xl">
<ul className="flex flex-wrap justify-center gap-x-6 gap-y-4 border-b border-slate-200 pb-9 dark:border-zinc-700">
{homeEnhance.quickStats.map(item => (
<li key={item.label} className="w-full max-w-[240px] text-center sm:w-[calc(50%-0.75rem)] lg:w-[calc(25%-1.125rem)]">
<p className="text-2xl font-semibold tracking-tight text-slate-900 dark:text-zinc-100">{item.value}</p>
<p className="mt-1 text-sm text-slate-600 dark:text-zinc-400">{item.label}</p>
</li>
))}
</ul>
<div className="mt-14 grid items-start gap-10 md:grid-cols-2">
<article className="mx-auto w-full max-w-[28rem]">
<p className="mb-4 text-center text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-zinc-400">{homeEnhance.useCasesTitle}</p>
<ul className="mt-4 space-y-5">
{homeEnhance.useCases.map(item => (
<li key={item.title}>
<span className="inline-flex rounded-full bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 dark:bg-cyan-500/12 dark:text-cyan-300">
{item.tag}
</span>
<h3 className="mt-2 text-base font-semibold text-slate-900 dark:text-zinc-100">{item.title}</h3>
<p className="mt-1 text-sm leading-6 text-slate-700 dark:text-zinc-300">{item.description}</p>
</li>
))}
</ul>
</article>
<article className="mx-auto w-full max-w-[28rem]">
<p className="mb-4 text-center text-xs font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-zinc-400">{homeEnhance.flowTitle}</p>
<ol className="relative mx-auto mt-4 max-w-md space-y-7 before:absolute before:bottom-2 before:left-1/2 before:top-2 before:z-0 before:w-px before:-translate-x-1/2 before:bg-blue-200 before:content-[''] dark:before:bg-cyan-500/40">
{homeEnhance.flow.map((item, index) => (
<li key={item.title} className="relative z-10 text-center">
<span
className="absolute left-1/2 top-0 inline-flex h-7 w-7 -translate-x-1/2 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700 dark:bg-cyan-500/20 dark:text-cyan-300"
aria-hidden
>
{index + 1}
</span>
<h3 className="inline-block bg-background px-3 pt-9 text-base font-semibold text-slate-900 dark:text-zinc-100">{item.title.replace(/^\d+\.\s*/, '')}</h3>
<p className="mx-auto mt-1 max-w-xs bg-background px-2 text-sm leading-6 text-slate-700 dark:text-zinc-300">{item.description}</p>
</li>
))}
</ol>
</article>
</div>
<div className="mt-16 pt-2 text-center">
<p className="text-base font-semibold text-slate-900 dark:text-zinc-100">{homeEnhance.ctaDescription}</p>
<div className="mt-4 flex flex-wrap justify-center gap-2">
<a
href={`/${currentLocale}/ai-demo`}
className="inline-flex min-h-10 items-center rounded-xl bg-linear-to-r from-[#2563EB] to-[#1D4ED8] px-4 py-2 text-sm font-semibold text-white"
>
{homeEnhance.ctaPrimary}
</a>
<a
href={`/${currentLocale}/introduction`}
className="inline-flex min-h-10 items-center rounded-xl border border-slate-300/80 bg-white px-4 py-2 text-sm font-semibold text-slate-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200"
>
{homeEnhance.ctaSecondary}
</a>
</div>
</div>
</div>
</Section>
<Section
title="Frequently Asked Questions"
tallPaddingY
>
<Accordion
type="single"
collapsible
className="w-full max-w-5xl px-5"
>
{
faqs.map((faqItem, index) => (
<AccordionItem
value={faqItem.question}
key={index}
>
<AccordionTrigger>{faqItem.question}</AccordionTrigger>
<AccordionContent>
{faqItem.answer}
</AccordionContent>
</AccordionItem>
))
}
</Accordion>
</Section>
</div>
</>
)
}
================================================
FILE: src/components/MotionWrapper/FadeIn.tsx
================================================
'use client'
import type { Variants } from 'framer-motion'
import { motion, useInView } from 'framer-motion'
import { memo, useRef } from 'react'
interface MotionWrapperFadeInProps {
children: React.ReactNode
className?: string
noVertical?: boolean
delay?: number
viewTriggerOffset?: boolean
}
const fadeUpVariants: Variants = {
initial: (noVertical: boolean) => ({
opacity: 0,
y: noVertical ? 0 : 24,
}),
animate: {
opacity: 1,
y: 0,
},
}
export const MotionWrapperFadeIn = memo(({
children,
className,
noVertical = false,
delay = 0,
viewTriggerOffset = false,
}: MotionWrapperFadeInProps) => {
const ref = useRef(null)
const inView = useInView(ref, {
once: true,
margin: viewTriggerOffset ? '-100px' : '0px',
})
return (
<motion.div
animate={inView ? 'animate' : 'initial'}
className={className}
initial="initial"
ref={ref}
transition={{
duration: 1,
delay,
ease: [0.25, 0.1, 0.25, 1],
}}
variants={fadeUpVariants}
custom={noVertical}
>
{children}
</motion.div>
)
})
================================================
FILE: src/components/MotionWrapper/Flash.tsx
================================================
'use client'
import type { ReactNode } from 'react'
import { motion } from 'framer-motion'
import React from 'react'
interface Props {
className?: string
disabledAnimation?: boolean
disabledHover?: boolean
children: ReactNode
}
export const MotionWrapperFlash: React.FC<Props> = (props) => {
const {
disabledAnimation = true,
disabledHover = false,
children,
className,
} = props
if (disabledAnimation) {
return children
}
return (
<motion.span
className={className}
initial={{ opacity: 0, scale: 0.8, rotate: -20 }}
animate={{ opacity: 1, scale: 1, rotate: 0 }}
whileHover={
!disabledHover
? {
scale: 1.1,
rotate: 10,
transition: { duration: 0.3 },
}
: {}
}
transition={{
duration: 0.6,
ease: [0.2, 0.8, 0.6, 1],
scale: {
type: 'spring',
stiffness: 260,
},
rotate: {
type: 'spring',
stiffness: 150,
},
color: {
duration: 0.3,
},
}}
>
{children}
</motion.span>
)
}
================================================
FILE: src/components/MotionWrapper/index.ts
================================================
export * from './FadeIn'
export * from './Flash'
================================================
FILE: src/components/PanelParticles/index.tsx
================================================
'use client'
import type { ISourceOptions } from '@tsparticles/engine'
import Particles, { initParticlesEngine } from '@tsparticles/react'
import { useTheme } from 'nextra-theme-docs'
import { useEffect, useMemo } from 'react'
import { loadFull } from 'tsparticles'
const PanelParticles = () => {
const { resolvedTheme } = useTheme()
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadFull(engine)
})
}, [])
const options = useMemo<ISourceOptions>(
() => ({
fpsLimit: 120,
interactivity: {
events: {
onHover: {
enable: true,
mode: 'grab',
},
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: resolvedTheme === 'light' ? '#9f9cbf' : '#c1c7d1',
},
links: {
color: {
value: resolvedTheme === 'light' ? '#9f9cbf' : '#c1c7d1',
},
distance: 120,
enable: true,
opacity: resolvedTheme === 'light' ? 0.2 : 0.1,
width: 1,
},
move: {
direction: 'none',
enable: true,
outModes: {
default: 'bounce',
},
random: false,
speed: 1,
straight: false,
},
number: {
density: {
enable: true,
},
value: 60,
},
opacity: {
value: resolvedTheme === 'light' ? 0.2 : 0.15,
},
shape: {
type: 'circle',
},
size: {
value: { min: 1, max: 3 },
},
},
detectRetina: true,
}),
[resolvedTheme],
)
return (
<Particles
className="max-sm:hidden pointer-events-none"
options={options}
/>
)
}
export {
PanelParticles,
}
================================================
FILE: src/components/ScrollProgressBar/index.tsx
================================================
'use client'
import { usePathname } from 'next/navigation'
import { useCallback, useEffect, useRef, useState } from 'react'
interface ScrollProgressBarProps {
height?: number // 进度条高度,默认 3px
colors?: string[] // 渐变色数组,默认彩虹渐变
zIndex?: number // z-index 层级,默认 9999
smoothness?: number // 平滑度(0-1),默认 0.15,越小越平滑但响应稍慢
}
const defaultColors = [
'#00CED1',
'#4072ed',
'#9370DB',
]
export default function ScrollProgressBar({
height = 4,
colors = defaultColors,
zIndex = 9999,
smoothness = 0.15,
}: ScrollProgressBarProps) {
const [progress, setProgress] = useState(0)
const [isVisible, setIsVisible] = useState(false)
const pathname = usePathname()
// 使用 ref 存储动画相关状态,避免重复渲染
const rafIdRef = useRef<number | null>(null)
const currentProgressRef = useRef(0)
const targetProgressRef = useRef(0)
const lastScrollTimeRef = useRef(0)
const isAnimatingRef = useRef(false)
// 计算目标滚动进度
const calculateTargetProgress = useCallback(() => {
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
const scrollTop = window.scrollY || document.documentElement.scrollTop
const scrollableHeight = documentHeight - windowHeight
if (scrollableHeight <= 0) {
return { progress: 0, visible: false }
}
const scrollProgress = (scrollTop / scrollableHeight) * 100
const clampedProgress = Math.min(Math.max(scrollProgress, 0), 100)
return {
progress: clampedProgress,
visible: scrollTop > 0,
}
}, [])
// 平滑动画函数 - 使用 lerp (线性插值) 实现平滑过渡
const animateProgress = useCallback(() => {
const now = performance.now()
const timeSinceLastScroll = now - lastScrollTimeRef.current
// 计算当前进度到目标进度的差值
const diff = targetProgressRef.current - currentProgressRef.current
// 如果差值很小且已经 200ms 没有滚动,停止动画
if (Math.abs(diff) < 0.01 && timeSinceLastScroll > 200) {
currentProgressRef.current = targetProgressRef.current
setProgress(currentProgressRef.current)
isAnimatingRef.current = false
return
}
// 使用 lerp 平滑插值
currentProgressRef.current += diff * smoothness
setProgress(currentProgressRef.current)
// 继续动画
rafIdRef.current = requestAnimationFrame(animateProgress)
}, [smoothness])
// 启动平滑动画
const startAnimation = useCallback(() => {
if (!isAnimatingRef.current) {
isAnimatingRef.current = true
animateProgress()
}
}, [animateProgress])
// 处理滚动事件
const handleScroll = useCallback(() => {
lastScrollTimeRef.current = performance.now()
const { progress: newProgress, visible } = calculateTargetProgress()
targetProgressRef.current = newProgress
setIsVisible(visible)
// 启动或继续动画
startAnimation()
}, [calculateTargetProgress, startAnimation])
// 处理窗口大小变化
const handleResize = useCallback(() => {
const { progress: newProgress, visible } = calculateTargetProgress()
targetProgressRef.current = newProgress
currentProgressRef.current = newProgress
setProgress(newProgress)
setIsVisible(visible)
}, [calculateTargetProgress])
// 监听滚动和窗口变化
useEffect(() => {
// 初始化
const { progress: initialProgress, visible } = calculateTargetProgress()
targetProgressRef.current = initialProgress
currentProgressRef.current = initialProgress
setProgress(initialProgress)
setIsVisible(visible)
// 使用节流优化滚动事件
let scrollTimeout
const throttledScroll = () => {
clearTimeout(scrollTimeout)
handleScroll()
}
window.addEventListener('scroll', throttledScroll, { passive: true })
window.addEventListener('resize', handleResize, { passive: true })
return () => {
window.removeEventListener('scroll', throttledScroll)
window.removeEventListener('resize', handleResize)
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current)
}
}
}, [calculateTargetProgress, handleScroll, handleResize])
// 路由切换时重置
useEffect(() => {
const timer = setTimeout(() => {
const { progress: newProgress, visible } = calculateTargetProgress()
targetProgressRef.current = newProgress
currentProgressRef.current = newProgress
setProgress(newProgress)
setIsVisible(visible)
// 取消正在进行的动画
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current)
isAnimatingRef.current = false
}
}, 100)
return () => clearTimeout(timer)
}, [pathname, calculateTargetProgress])
// 生成渐变色字符串
const gradientColors = colors.join(', ')
return (
<div
className="fixed top-0 left-0 right-0 pointer-events-none"
style={{ zIndex }}
aria-hidden="true"
>
<div
className="origin-left"
style={{
height: `${height}px`,
width: `${progress}%`,
background: `linear-gradient(to right, ${gradientColors})`,
opacity: isVisible ? 1 : 0,
transition: 'opacity 150ms ease-out',
willChange: 'width',
// 使用 transform 代替 width 动画以获得更好的性能
// 但这里我们保持 width 因为需要精确的进度显示
}}
/>
</div>
)
}
================================================
FILE: src/components/ThemeSwitcher/index.tsx
================================================
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'nextra-theme-docs'
import { Button } from '@/components/ui/button'
import { useLocale } from '@/hooks'
export const ThemeSwitcher = () => {
const { setTheme } = useTheme()
const { t } = useLocale()
return (
<div className="flex justify-start max-sm:justify-center gap-6 py-6">
<div className="flex flex-col items-center gap-2">
<Button
variant="secondary"
size="icon"
onClick={() => setTheme('light')}
aria-label={t('themeSwitcher.lightAria')}
>
<Sun className="h-[1.5rem] w-[1.5rem]" />
</Button>
<span className="text-sm text-muted-foreground">{ t('themeSwitcher.light') }</span>
</div>
<div className="flex flex-col items-center gap-2">
<Button
variant="secondary"
size="icon"
onClick={() => setTheme('dark')}
aria-label={t('themeSwitcher.darkAria')}
>
<Moon className="h-[1.5rem] w-[1.5rem]" />
</Button>
<span className="text-sm text-muted-foreground">{ t('themeSwitcher.dark') }</span>
</div>
</div>
)
}
================================================
FILE: src/components/TitleBadge/index.tsx
================================================
'use client'
import type { ReactNode } from 'react'
import clsx from 'clsx'
import { motion } from 'framer-motion'
interface Props {
className?: string
children?: ReactNode
}
export const TitleBadge = ({
className,
children = 'NEW',
}: Props) => {
return (
<motion.span
animate={{ backgroundPosition: ['0% 50%', '100% 50%', '0% 50%'] }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className={clsx(
'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500',
'bg-[length:200%_100%]',
'ml-[6px] py-[4px] px-[4.5px]',
'font-semibold text-[11px] rounded-[6px]',
'text-white',
'leading-[1]',
className,
)}
>
{children}
</motion.span>
)
}
================================================
FILE: src/components/auth/login-form.client.tsx
================================================
'use client'
import dynamic from 'next/dynamic'
const LoginForm = dynamic(() => import('@/components/auth/login-form'), {
ssr: false,
})
export default function LoginFormClient() {
return <LoginForm />
}
================================================
FILE: src/components/auth/login-form.tsx
================================================
'use client'
import { useEffect, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { LoaderOne } from '@/components/ui/loader'
import { Label } from '@/components/ui/label'
import { useLocale } from '@/hooks'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'
const STORAGE_KEY = 'auth:userEmail'
type ErrorType = 'invalidEmail' | 'passwordRequired' | 'storage' | null
export default function LoginForm() {
const { currentLocale, t } = useLocale()
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<ErrorType>(null)
const [googleLoading, setGoogleLoading] = useState(false)
const [submitLoading, setSubmitLoading] = useState(false)
const [pageLoading, setPageLoading] = useState(true)
useEffect(() => {
const timer = window.setTimeout(() => {
setPageLoading(false)
}, 700)
return () => window.clearTimeout(timer)
}, [])
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setError(null)
if (submitLoading || googleLoading) {
return
}
if (!email || !/\S+@\S+\.\S+/.test(email)) {
setError('invalidEmail')
return
}
if (!password) {
setError('passwordRequired')
return
}
try {
setSubmitLoading(true)
window.localStorage.setItem(STORAGE_KEY, email)
toast.success(t('auth.success'))
window.dispatchEvent(new Event('auth:changed'))
window.setTimeout(() => {
router.replace(`/${currentLocale}`)
setSubmitLoading(false)
}, 1200)
} catch {
setSubmitLoading(false)
setError('storage')
}
}
const onGoogleLogin = () => {
if (googleLoading || submitLoading) {
return
}
setError(null)
setGoogleLoading(true)
// TODO: replace with real Google OAuth when backend is available.
window.setTimeout(() => {
try {
const googleEmail = 'jane.doe@gmail.com'
window.localStorage.setItem(STORAGE_KEY, googleEmail)
toast.success(t('auth.success'))
window.dispatchEvent(new Event('auth:changed'))
window.setTimeout(() => {
router.replace(`/${currentLocale}`)
}, 600)
} catch {
setError('storage')
} finally {
setGoogleLoading(false)
}
}, 900)
}
const errorMessage = useMemo(() => {
if (!error) {
return null
}
if (error === 'invalidEmail') {
return t('auth.invalidEmail')
}
if (error === 'passwordRequired') {
return t('auth.passwordRequired')
}
return t('auth.storageError')
}, [error, t])
if (pageLoading) {
return (
<div className="flex min-h-[60vh] w-full items-center justify-center">
<div className="flex flex-col items-center gap-3 text-sm text-muted-foreground">
<LoaderOne />
<span>{t('auth.loading')}</span>
</div>
</div>
)
}
return (
<div className="mx-auto flex w-full flex-col gap-5 px-4 py-6 sm:max-w-md sm:py-10">
<div className="space-y-2 text-center">
<p className="text-base font-medium text-foreground/70">
{t('auth.brand')}
</p>
<h1 className="text-xl font-semibold leading-tight text-foreground sm:text-2xl">
{t('auth.welcome')}
</h1>
</div>
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="email">{t('auth.email')}</Label>
<Input
id="email"
type="email"
value={email}
placeholder={t('auth.emailPlaceholder')}
className="h-11 rounded-full bg-muted/70"
onChange={(event) => {
setEmail(event.target.value)
if (error) {
setError(null)
}
}}
required
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="password">{t('auth.password')}</Label>
<Input
id="password"
type="password"
value={password}
placeholder={t('auth.passwordPlaceholder')}
className="h-11 rounded-full bg-muted/70"
onChange={(event) => {
setPassword(event.target.value)
if (error) {
setError(null)
}
}}
required
/>
</div>
{errorMessage && (
<p className="text-sm text-destructive">{errorMessage}</p>
)}
<Button
type="submit"
className="h-10 w-full rounded-md bg-primary text-primary-foreground hover:bg-primary/90"
disabled={submitLoading || googleLoading}
aria-busy={submitLoading}
>
{submitLoading ? t('auth.googleLoading') : t('auth.submit')}
</Button>
</form>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="h-px flex-1 bg-border" />
<span className="text-center">{t('auth.or')}</span>
<span className="h-px flex-1 bg-border" />
</div>
<Button
type="button"
variant="outline"
className="h-10 w-full rounded-md"
onClick={onGoogleLogin}
disabled={googleLoading || submitLoading}
aria-busy={googleLoading}
>
{googleLoading ? t('auth.googleLoading') : t('auth.google')}
</Button>
<Button
type="button"
variant="ghost"
className="h-9 w-full rounded-md text-muted-foreground"
onClick={() => {
router.push(`/${currentLocale}`)
}}
>
{t('auth.backHome')}
</Button>
</div>
)
}
================================================
FILE: src/components/ui/accordion.tsx
================================================
'use client'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import * as React from 'react'
import { cn } from '@/lib/utils'
const Accordion = AccordionPrimitive.Root
const AccordionItem = ({
className,
...props
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>) => {
const itemRef = React.useRef<React.ComponentRef<typeof AccordionPrimitive.Item>>(null)
return (
<AccordionPrimitive.Item
ref={itemRef}
className={cn(
'border-b',
className,
)}
{...props}
/>
)
}
AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = ({
className,
children,
...props
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>) => {
const itemRef = React.useRef<React.ComponentRef<typeof AccordionPrimitive.Trigger>>(null)
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={itemRef}
className={cn(
'group flex flex-1 items-center justify-between py-7 font-medium transition-all hover:underline',
'text-[18px] font-bold',
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = ({
className,
children,
...props
}: React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>) => {
const itemRef = React.useRef<React.ComponentRef<typeof AccordionPrimitive.Content>>(null)
return (
<AccordionPrimitive.Content
ref={itemRef}
className={cn(
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
'text-[16px]',
)}
{...props}
>
<div className={cn('pb-6 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
================================================
FILE: src/components/ui/alert.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }
================================================
FILE: src/components/ui/button.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
================================================
FILE: src/components/ui/card-hover-effect.tsx
================================================
'use client'
import type { ReactNode } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
import { cn } from '@/lib/utils'
export const Card = ({
className,
children,
}: {
className?: string
children: React.ReactNode
}) => {
return (
<div
className={cn(
'relative rounded-2xl h-full w-full p-4 overflow-hidden',
'border duration-200',
'bg-neutral-50 dark:bg-neutral-800',
'border-neutral-200/[0.5] dark:border-white/[0.1]',
'group-hover:border-neutral-300/[0.6] dark:group-hover:border-primary/[0.8]',
className,
)}
>
<div className="relative">
<div className="p-4">{children}</div>
</div>
</div>
)
}
export const CardIcon = ({
className,
children,
}: {
className?: string
children?: React.ReactNode
}) => {
return (
<div className={cn(
'flex justify-center items-center',
'rounded-[6px]',
'text-zinc-600 dark:text-zinc-200',
'size-[48px] mb-[20px] bg-red-200',
'text-[24px]',
'bg-[#e3e3e5] dark:bg-[#1e1e20]',
'transition-all duration-300 dark:group-hover:text-primary',
className,
)}
>
{children}
</div>
)
}
export const CardTitle = ({
className,
children,
}: {
className?: string
children: React.ReactNode
}) => {
return (
<h4 className={cn(
'text-zinc-600 dark:text-zinc-200',
'font-bold tracking-wide mt-4',
className,
)}
>
{children}
</h4>
)
}
export const CardDescription = ({
className,
children,
}: {
className?: string
children: React.ReactNode
}) => {
return (
<p
className={cn(
'mt-8 tracking-wide leading-relaxed text-sm',
'text-zinc-500 dark:text-zinc-300/[0.8]',
className,
)}
>
{children}
</p>
)
}
export const HoverEffect = ({
items,
className,
}: {
items: {
title: string
description: string
link?: string
icon: ReactNode
}[]
className?: string
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
return (
<div
className={cn(
'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 py-[10px]',
className,
)}
>
{items.map((item, idx) => (
<div
key={idx}
className="relative group block p-2 h-full w-full"
onMouseEnter={() => setHoveredIndex(idx)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === idx && (
<motion.span
className="z-[-1] absolute inset-0 h-full w-full bg-neutral-200/[0.3] dark:bg-neutral-500/[0.5] block rounded-3xl"
layoutId="hoverBackground"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.5 },
}}
exit={{
opacity: 0,
transition: { duration: 0.3, delay: 0.2 },
}}
/>
)}
</AnimatePresence>
<Card>
<CardIcon>{item.icon}</CardIcon>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</Card>
</div>
))}
</div>
)
}
================================================
FILE: src/components/ui/card.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
================================================
FILE: src/components/ui/flip-words.tsx
================================================
'use client'
import type { TargetAndTransition } from 'framer-motion'
import { AnimatePresence, motion } from 'framer-motion'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useBreakpoint } from '@/hooks'
import { cn } from '@/lib/utils'
export const FlipWords = ({
words,
duration = 3000,
className,
}: {
words: string[]
duration?: number
className?: string
}) => {
const [currentWord, setCurrentWord] = useState(words[0])
const [isAnimating, setIsAnimating] = useState<boolean>(false)
// thanks for the fix Julian - https://github.com/Julian-AT
const startAnimation = useCallback(() => {
const word = words[words.indexOf(currentWord) + 1] || words[0]
setCurrentWord(word)
setIsAnimating(true)
}, [currentWord, words])
useEffect(() => {
if (!isAnimating) {
setTimeout(() => {
startAnimation()
}, duration)
}
}, [isAnimating, duration, startAnimation])
const { isMd } = useBreakpoint()
const motionExit = useMemo<TargetAndTransition>(() => {
if (isMd) {
return {
opacity: 0,
filter: 'blur(0px)',
position: 'absolute',
}
}
return {
opacity: 0,
y: -40,
x: 40,
filter: 'blur(8px)',
scale: 2,
position: 'absolute',
}
}, [isMd])
return (
<AnimatePresence
onExitComplete={() => {
setIsAnimating(false)
}}
>
<motion.div
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
type: 'spring',
stiffness: 100,
damping: 10,
}}
exit={motionExit}
className={cn(
'inline-block relative font-bold text-neutral-700 dark:text-neutral-200',
className,
)}
key={currentWord}
>
{currentWord.split('').map((letter, index) => (
<motion.span
key={currentWord + index}
initial={{ opacity: 0, y: 10, filter: 'blur(8px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
transition={{
delay: index * 0.08,
duration: 0.4,
}}
className="inline-block"
>
{letter}
</motion.span>
))}
</motion.div>
</AnimatePresence>
)
}
================================================
FILE: src/components/ui/hover-card.tsx
================================================
'use client'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import * as React from 'react'
import { cn } from '@/lib/utils'
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = ({ className, align = 'center', sideOffset = 4, ...props }: React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>) => {
const itemRef = React.useRef<React.ComponentRef<typeof HoverCardPrimitive.Content>>(null)
return (
<HoverCardPrimitive.Content
ref={itemRef}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
)
}
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardContent, HoverCardTrigger }
================================================
FILE: src/components/ui/input.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
================================================
FILE: src/components/ui/label.tsx
================================================
"use client"
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
================================================
FILE: src/components/ui/link-preview.tsx
================================================
'use client'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
import {
AnimatePresence,
motion,
useMotionValue,
useSpring,
} from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { encode } from 'qss'
import React from 'react'
import { cn } from '@/lib/utils'
type LinkPreviewProps = {
children: React.ReactNode
url: string
className?: string
width?: number
height?: number
quality?: number
} & (
| { isStatic: true, imageSrc: string }
| { isStatic?: false, imageSrc?: never }
)
export const LinkPreview = ({
children,
url,
className,
width = 200,
height = 125,
quality = 50,
isStatic = false,
imageSrc = '',
}: LinkPreviewProps) => {
let src
if (!isStatic) {
const params = encode({
url,
screenshot: true,
meta: false,
embed: 'screenshot.url',
colorScheme: 'dark',
'viewport.isMobile': true,
'viewport.deviceScaleFactor': 1,
'viewport.width': width * 3,
'viewport.height': height * 3,
})
src = `https://api.microlink.io/?${params}`
}
else {
src = imageSrc
}
const [isOpen, setOpen] = React.useState(false)
const [isMounted, setIsMounted] = React.useState(false)
React.useEffect(() => {
setIsMounted(true)
}, [])
const springConfig = { stiffness: 100, damping: 15 }
const x = useMotionValue(0)
const translateX = useSpring(x, springConfig)
const handleMouseMove = (event: any) => {
const targetRect = event.target.getBoundingClientRect()
const eventOffsetX = event.clientX - targetRect.left
const offsetFromCenter = (eventOffsetX - targetRect.width / 2) / 2 // Reduce the effect to make it subtle
x.set(offsetFromCenter)
}
return (
<>
{isMounted
? (
<div className="hidden">
<Image
src={src}
width={width}
height={height}
quality={quality}
priority
alt="hidden image"
/>
</div>
)
: null}
<HoverCardPrimitive.Root
openDelay={50}
closeDelay={100}
onOpenChange={(open) => {
setOpen(open)
}}
>
<HoverCardPrimitive.Trigger
onMouseMove={handleMouseMove}
className={cn('font-bold bg-clip-text bg-linear-to-br', className)}
href={url}
target="_blank"
>
{children}
</HoverCardPrimitive.Trigger>
<HoverCardPrimitive.Content
className="aa [transform-origin:var(--radix-hover-card-content-transform-origin)]"
side="top"
align="center"
sideOffset={10}
>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.6 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
transition: {
type: 'spring',
stiffness: 260,
damping: 20,
},
}}
exit={{ opacity: 0, y: 20, scale: 0.6 }}
className="shadow-xl rounded-xl"
style={{
x: translateX,
}}
>
<Link
href={url}
target="_blank"
className="block p-1 bg-white border-2 border-transparent shadow-sm rounded-xl hover:border-neutral-200 dark:hover:border-neutral-800"
style={{ fontSize: 0 }}
>
<Image
src={isStatic ? imageSrc : src}
width={width}
height={height}
quality={quality}
priority
className="rounded-lg"
alt="preview image"
/>
</Link>
</motion.div>
)}
</AnimatePresence>
</HoverCardPrimitive.Content>
</HoverCardPrimitive.Root>
</>
)
}
================================================
FILE: src/components/ui/loader.tsx
================================================
"use client";
import { motion } from "motion/react";
import React from "react";
export const LoaderOne = () => {
const transition = (x: number) => {
return {
duration: 1,
repeat: Infinity,
repeatType: "loop" as const,
delay: x * 0.2,
ease: "easeInOut" as const,
};
};
return (
<div className="flex items-center gap-2">
<motion.div
initial={{
y: 0,
}}
animate={{
y: [0, 10, 0],
}}
transition={transition(0)}
className="h-4 w-4 rounded-full border border-neutral-300 bg-gradient-to-b from-neutral-400 to-neutral-300"
/>
<motion.div
initial={{
y: 0,
}}
animate={{
y: [0, 10, 0],
}}
transition={transition(1)}
className="h-4 w-4 rounded-full border border-neutral-300 bg-gradient-to-b from-neutral-400 to-neutral-300"
/>
<motion.div
initial={{
y: 0,
}}
animate={{
y: [0, 10, 0],
}}
transition={transition(2)}
className="h-4 w-4 rounded-full border border-neutral-300 bg-gradient-to-b from-neutral-400 to-neutral-300"
/>
</div>
);
};
export const LoaderTwo = () => {
const transition = (x: number) => {
return {
duration: 2,
repeat: Infinity,
repeatType: "loop" as const,
delay: x * 0.2,
ease: "easeInOut" as const,
};
};
return (
<div className="flex items-center">
<motion.div
transition={transition(0)}
initial={{
x: 0,
}}
animate={{
x: [0, 20, 0],
}}
className="h-4 w-4 rounded-full bg-neutral-200 shadow-md dark:bg-neutral-500"
/>
<motion.div
initial={{
x: 0,
}}
animate={{
x: [0, 20, 0],
}}
transition={transition(0.4)}
className="h-4 w-4 -translate-x-2 rounded-full bg-neutral-200 shadow-md dark:bg-neutral-500"
/>
<motion.div
initial={{
x: 0,
}}
animate={{
x: [0, 20, 0],
}}
transition={transition(0.8)}
className="h-4 w-4 -translate-x-4 rounded-full bg-neutral-200 shadow-md dark:bg-neutral-500"
/>
</div>
);
};
export const LoaderThree = () => {
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
className="h-20 w-20 stroke-neutral-500 [--fill-final:var(--color-yellow-300)] [--fill-initial:var(--color-neutral-50)] dark:stroke-neutral-100 dark:[--fill-final:var(--color-yellow-500)] dark:[--fill-initial:var(--color-neutral-800)]"
>
<motion.path stroke="none" d="M0 0h24v24H0z" fill="none" />
<motion.path
initial={{ pathLength: 0, fill: "var(--fill-initial)" }}
animate={{ pathLength: 1, fill: "var(--fill-final)" }}
transition={{
duration: 2,
ease: "easeInOut" as const,
repeat: Infinity,
repeatType: "reverse",
}}
d="M13 3l0 7l6 0l-8 11l0 -7l-6 0l8 -11"
/>
</motion.svg>
);
};
export const LoaderFour = ({ text = "Loading..." }: { text?: string }) => {
return (
<div className="relative font-bold text-black [perspective:1000px] dark:text-white">
<motion.span
animate={{
skewX: [0, -40, 0],
scaleX: [1, 2, 1],
}}
transition={{
duration: 0.05,
repeat: Infinity,
repeatType: "reverse",
repeatDelay: 2,
ease: "linear" as const,
times: [0, 0.2, 0.5, 0.8, 1],
}}
className="relative z-20 inline-block"
>
{text}
</motion.span>
<motion.span
className="absolute inset-0 text-[#00e571]/50 blur-[0.5px] dark:text-[#00e571]"
animate={{
x: [-2, 4, -3, 1.5, -2],
y: [-2, 4, -3, 1.5, -2],
opacity: [0.3, 0.9, 0.4, 0.8, 0.3],
}}
transition={{
duration: 0.5,
repeat: Infinity,
repeatType: "reverse",
ease: "linear" as const,
times: [0, 0.2, 0.5, 0.8, 1],
}}
>
{text}
</motion.span>
<motion.span
className="absolute inset-0 text-[#8b00ff]/50 dark:text-[#8b00ff]"
animate={{
x: [0, 1, -1.5, 1.5, -1, 0],
y: [0, -1, 1.5, -0.5, 0],
opacity: [0.4, 0.8, 0.3, 0.9, 0.4],
}}
transition={{
duration: 0.8,
repeat: Infinity,
repeatType: "reverse",
ease: "linear" as const,
times: [0, 0.3, 0.6, 0.8, 1],
}}
>
{text}
</motion.span>
</div>
);
};
export const LoaderFive = ({ text }: { text: string }) => {
return (
<div className="font-sans font-bold [--shadow-color:var(--color-neutral-500)] dark:[--shadow-color:var(--color-neutral-100)]">
{text.split("").map((char, i) => (
<motion.span
key={i}
className="inline-block"
initial={{ scale: 1, opacity: 0.5 }}
animate={{
scale: [1, 1.1, 1],
textShadow: [
"0 0 0 var(--shadow-color)",
"0 0 1px var(--shadow-color)",
"0 0 0 var(--shadow-color)",
],
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 0.5,
repeat: Infinity,
repeatType: "loop",
delay: i * 0.05,
ease: "easeInOut" as const,
repeatDelay: 2,
}}
>
{char === " " ? "\u00A0" : char}
</motion.span>
))}
</div>
);
};
================================================
FILE: src/components/ui/separator.tsx
================================================
'use client'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import * as React from 'react'
import { cn } from '@/lib/utils'
const Separator = (
{
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>,
) => {
const itemRef = React.useRef<React.ComponentRef<typeof SeparatorPrimitive.Root>>(null)
return (
<SeparatorPrimitive.Root
ref={itemRef}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className,
)}
{...props}
/>
)
}
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
================================================
FILE: src/components/ui/sonner.tsx
================================================
"use client"
import type React from "react"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
================================================
FILE: src/components/ui/toggle.tsx
================================================
'use client'
import type { VariantProps } from 'class-variance-authority'
import * as TogglePrimitive from '@radix-ui/react-toggle'
import { cva } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '@/lib/utils'
const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-3',
sm: 'h-9 px-2.5',
lg: 'h-11 px-5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
const Toggle = ({
className,
variant,
size,
...props
}: React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>) => {
const itemRef = React.useRef<React.ComponentRef<typeof TogglePrimitive.Root>>(null)
return (
<TogglePrimitive.Root
ref={itemRef}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }
================================================
FILE: src/content/en/_meta.tsx
================================================
import type { MetaRecord } from 'nextra'
import { TitleBadge } from '@/components/TitleBadge'
export default {
index: {
type: 'page',
display: 'hidden',
theme: {
copyPage: false,
timestamp: false,
layout: 'full',
toc: false,
},
},
introduction: {
type: 'page',
title: 'This is Introduction',
theme: {
copyPage: false,
navbar: true,
toc: false,
},
},
login: {
type: 'page',
title: 'Login',
display: 'hidden',
theme: {
navbar: false,
footer: false,
toc: false,
layout: 'full',
timestamp: false,
},
},
'ai-demo': {
type: 'page',
display: 'hidden',
theme: {
copyPage: false,
toc: false,
timestamp: false,
layout: 'full',
},
},
docs: {
title: '📦 Some Examples',
type: 'page',
},
upgrade: {
title: (
<span className="flex items-center leading-[1]">
What's New
<TitleBadge />
</span>
),
type: 'page',
},
} satisfies MetaRecord
================================================
FILE: src/content/en/ai-demo.mdx
================================================
---
title: "PulseOps AI Workflow Assistant | Landing Page Demo"
description: "PulseOps landing page demo for an AI workflow assistant, showcasing positioning, features, and conversion structure for lean teams."
---
import AIDemoLanding from '@/components/AIDemoLanding'
<AIDemoLanding />
================================================
FILE: src/content/en/docs/_meta.tsx
================================================
import type { MetaRecord } from 'nextra'
export default {
// ...
} satisfies MetaRecord
================================================
FILE: src/content/en/docs/examples/test-tailwind.mdx
================================================
# Tailwind CSS Example
## Card Component
Here's an example of a classic card component. It uses Tailwind CSS's utility classes to quickly build responsive layouts, including shadows, rounded corners, and some padding, making it look more elegant and modern.
<div className="my-5 max-w-md mx-auto bg-foreground dark:bg-neutral-600 rounded-xl shadow-md border p-6">
<h3 className="text-lg font-semibold text-zinc-200 mb-2">Classic Card</h3>
<div className="text-zinc-200 mb-4">
Using Tailwind CSS, you can quickly build responsive card components with great default styles and extensibility.
</div>
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">
Action Button
</button>
</div>
## Button State Demo
Here are some examples of different button states, including default, disabled, and success/warning buttons. All buttons use Tailwind CSS's interaction state classes, such as `hover` and `disabled`, to achieve different visual effects.
<div className="my-5 flex gap-4 flex-wrap">
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">Default</button>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded cursor-not-allowed" disabled>Disabled</button>
<button className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition">Success</button>
<button className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition">Warning</button>
</div>
## Alert (Prompt)
Alert boxes are used to notify users of important information or warnings. By using Tailwind CSS's background colors and border styles, you can easily create different types of alert boxes.
<div className="my-5 max-w-md mx-auto">
<div className="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4">
<p className="font-bold">Information Prompt:</p>
<p>This is an information prompt box, suitable for showing normal notification information.</p>
</div>
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mt-4">
<p className="font-bold">Warning Prompt:</p>
<p>This is a warning prompt box, suitable for showing important warning information.</p>
</div>
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mt-4">
<p className="font-bold">Error Prompt:</p>
<p>This is an error prompt box, suitable for showing error information or requiring correction.</p>
</div>
</div>
## Tags (Labels)
Tag components can be used to display the classification or tags of content, helping users quickly understand the type or theme of the content.
<div className="my-5 mx-auto">
<div className="space-x-2">
<span className="inline-block bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-1 rounded-full">Tag 1</span>
<span className="inline-block bg-green-100 text-green-800 text-xs font-semibold px-2 py-1 rounded-full">Tag 2</span>
<span className="inline-block bg-yellow-100 text-yellow-800 text-xs font-semibold px-2 py-1 rounded-full">Tag 3</span>
</div>
</div>
## Responsive Layout Grid
Responsive layout grids can automatically adjust the display based on different screen sizes. By using Tailwind CSS's `grid` class, you can easily create flexible layouts that adapt to desktop, tablet, and mobile devices.
<div className="my-5 mx-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-accent p-4 rounded shadow text-center">Block 1</div>
<div className="bg-accent p-4 rounded shadow text-center">Block 2</div>
<div className="bg-accent p-4 rounded shadow text-center">Block 3</div>
<div className="bg-accent p-4 rounded shadow text-center">Block 4</div>
<div className="bg-accent p-4 rounded shadow text-center">Block 5</div>
<div className="bg-accent p-4 rounded shadow text-center">Block 6</div>
<div className="bg-accent p-4 rounded shadow text-center">Block 7</div>
</div>
## Typography (Prose)
Tailwind CSS's Typography plugin (`@tailwindcss/typography`) provides excellent typographic styles, especially for Markdown content. Using the `prose` class, you can make text, lists, quotes, and code blocks look beautiful and elegant.
<div className="my-5 mx-auto prose dark:prose-invert max-w-3xl">
<h3>Supported Typography Content</h3>
<div>
Using Tailwind's Typography plugin, you can make originally unstyled Markdown content look great with default typography.
</div>
<ol>
<li>List item example 1</li>
<li>List item example 2</li>
<li>List item example 3</li>
</ol>
<blockquote>
Do what you should do, and let time take care of the rest.
</blockquote>
<div>
You can insert an inline code snippet:
<br />
e.g. <code>npm install tailwindcss</code>
</div>
<div>👉 More content can be found at the <a href="https://tailwindcss.com/docs" target="_blank">Tailwind official documentation</a>.</div>
</div>
================================================
FILE: src/content/en/docs/examples/theme-update.mdx
================================================
# Dark Mode
import { ThemeSwitcher } from '@/components/ThemeSwitcher'
<ThemeSwitcher />
================================================
FILE: src/content/en/docs/i18n.mdx
================================================
import { FileTree } from 'nextra/components'
# i18n Support
This project provides two approaches for internationalization:
1. Nextra's built-in folder structure internationalization
2. Custom i18n implementation for components and client-side content
## Nextra's Built-in Internationalization Support
Nextra supports multilingual content through folder structure. In the project root directory, you can create folders for different languages, such as:
<FileTree>
<FileTree.Folder name="src" defaultOpen>
<FileTree.Folder name="content" defaultOpen>
<FileTree.Folder name="en" defaultOpen>
<FileTree.File name="_meta.tsx" active />
<FileTree.File name="index.mdx" />
<FileTree.File name="introduction.mdx" />
</FileTree.Folder>
<FileTree.Folder name="zh" defaultOpen>
<FileTree.File name="_meta.tsx" active />
<FileTree.File name="index.mdx" />
<FileTree.File name="introduction.mdx" />
</FileTree.Folder>
</FileTree.Folder>
</FileTree.Folder>
</FileTree>
You can configure the language switcher via the `_meta.tsx` file:
```tsx
import type { MetaRecord } from 'nextra'
export default {
index: {
type: 'page',
display: 'hidden',
theme: {
timestamp: false,
layout: 'full',
toc: false,
},
},
introduction: {
type: 'page',
theme: {
navbar: true,
toc: false,
},
},
} satisfies MetaRecord
```
## Custom i18n Implementation
For components and client-side content, we've implemented a type-safe internationalization solution.
### Directory Structure
<FileTree>
<FileTree.Folder name="src" defaultOpen>
<FileTree.Folder name="i18n" defaultOpen>
<FileTree.File name="index.ts" comment="Core functionality and type definitions" />
<FileTree.File name="en.ts" comment="English JSON language pack" />
<FileTree.File name="zh.ts" comment="Chinese JSON language pack" />
</FileTree.Folder>
<FileTree.Folder name="hooks" defaultOpen>
<FileTree.File name="useLocale.ts" comment="Pre-packaged common hooks" />
</FileTree.Folder>
</FileTree.Folder>
</FileTree>
### Language File Example
The language file structure is as follows (using [`en.ts`](https://github.com/pdsuwwz/nextjs-nextra-starter/blob/main/src/i18n/en.ts) as an example):
```typescript
export default {
systemTitle: '🚀 My Nextra Starter',
banner: {
title: '👋 Hey there! Welcome to the Next.js Starter.',
more: 'Check it out',
},
badgeTitle: 'Lightweight & Easy 🎉',
featureSupport: `🔥 Now with {{feature}} support!`,
lastUpdated: 'Last updated on:',
getStarted: 'Get Started',
// ...
}
```
### Usage
#### 1. In Components
```tsx
import { useLocale } from '@/hooks'
function MyComponent() {
const { t, currentLocale } = useLocale()
return (
<div>
<h1>{t('home.systemTitle')}</h1>
{/* Using variable interpolation */}
<div dangerouslySetInnerHTML={{
__html: t('home.featureSupport', {
feature: '<span>Tailwind CSS v4, Nextra v4</span>',
}),
}} />
</div>
)
}
```
#### 2. Dynamic Language Switching
Switch languages via URL paths (e.g., [`/en/introduction`](/en/introduction) and [`/zh/introduction`](/zh/introduction)). The current language is automatically obtained from the URL parameter.
## Type Safety
Our i18n implementation provides complete type safety support:
1. **Auto-completion:** The editor automatically suggests all available translation keys
2. **Type checking:** Using incorrect keys triggers TypeScript errors
3. **Nested key support:** Supports dot notation access like `home.title`
4. **Variable interpolation:** Can use `{{variable}}` syntax in translations
## Advanced Features
### Nested Value Retrieval
The `getNestedValue` function can retrieve nested values from an object based on dot notation paths:
```typescript
const value = getNestedValue(i18nConfig[currentLocale], 'home.title')
```
### String Interpolation
The `interpolateString` function supports inserting variables into translation strings:
```typescript
const result = interpolateString(
'Supports {{feature}}',
{ feature: 'Tailwind CSS v4' }
) // "Supports Tailwind CSS v4"
```
### Custom Hooks
The `useLocale` hook encapsulates language detection and translation functionality, providing:
- `currentLocale`: Current language code
- `t`: Translation function, supports variable interpolation
## Best Practices
1. **Organize translation files:** Use nested objects to group related translations
2. **Avoid hardcoded strings:** Always use the `t()` function instead of hardcoded text
3. **Set default language:** Ensure there's a fallback mechanism for the default language
4. **Maintain key consistency:** All language files should contain the same keys
5. **Use TypeScript:** Leverage the type system to ensure translation completeness and correctness
## Practical Example
Here's an example of using i18n in the `SetupHero` component:
```tsx
'use client'
import { useLocale } from '@/hooks'
export function SetupHero() {
const { t, currentLocale } = useLocale()
return (
<div>
<a href="https://github.com/pdsuwwz/nextjs-nextra-starter">
{t('badgeTitle')}
</a>
<Link href={`/${currentLocale}/upgrade`}>
<span dangerouslySetInnerHTML={{
__html: t('featureSupport', {
feature: `<span>Tailwind CSS v4, Nextra v4</span>`,
}),
}} />
</Link>
<Button asChild>
<Link href={`/${currentLocale}/introduction`}>
{t('getStarted')}
</Link>
</Button>
</div>
)
}
```
## Further Extensions
- **Language detection:** Add automatic language detection based on browser or user preferences
- **Number and date formatting:** Integrate the `Intl` API for localized formatting
- **Pluralization:** Add support for different language pluralization rules
- **Translation management interface:** Create translation management tools for content editors
================================================
FILE: src/content/en/docs/index.mdx
================================================
---
title: "Test Overview"
---
<h1>This is the overview content</h1>
================================================
FILE: src/content/en/index.mdx
================================================
---
title: "Home Page"
---
import HomepageHero from "@/components/HomepageHero";
<HomepageHero />
================================================
FILE: src/content/en/introduction.mdx
================================================
---
title: "Introduction"
---
import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
# Introduction
A free, open-source, and powerful Template with Next.js + Nextra + TypeScript + Tailwind CSS.
```js
console.log('hello, world')
```
## Installation & Running
* Install dependencies
```bash
pnpm install
```
* Local development
```bash
pnpm run dev
```
<Alert>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components and dependencies to your app using the cli.
</AlertDescription>
</Alert>
---
<HoverCard>
<HoverCardTrigger>
<Button>
😄 Hover Me
</Button>
</HoverCardTrigger>
<HoverCardContent>
The React Framework – created and maintained by @vercel.
</HoverCardContent>
</HoverCard>
================================================
FILE: src/content/en/login.mdx
================================================
---
title: Login
---
import LoginForm from '@/components/auth/login-form.client'
<div data-pagefind-ignore="all">
<LoginForm />
</div>
================================================
FILE: src/content/en/upgrade.mdx
================================================
---
title: "What's New"
---
import { Callout } from 'nextra/components'
# 🔥 Major Framework Updates
We've completed significant core framework upgrades to bring you a better development experience!
<Callout emoji={<span className='icon-[ri--alarm-warning-fill]'></span>} type="info">
Note
This is the latest release, and while it has been tested, there may still be some issues that have not been discovered. If you encounter any anomalies or instability, please provide [Feedback](https://github.com/pdsuwwz/nextjs-nextra-starter/issues), and I will fix them as soon as possible.
</Callout>
### Nextra v4 Updates
Nextra v4 is now released with major improvements and new features:
- 🚀 **App Router Support**: Fully transitioned to Next.js App Router, discontinuing Pages Router, and supporting the latest Metadata API.
- ⚡ **Faster Page Loading**: Optimized bundle size and performance for a more responsive website.
- 🔍 **New Search Engine**: Rust-powered Pagefind delivers faster and more accurate search results.
- 🏎️ **Turbopack Support**: Improved development experience, addressing long-standing community requests.
- 🌍 **RSC + i18n**: Enhanced internationalization, making multilingual website development easier.
- 🎨 **Optimized Theme Styles**: Improved UI design for better visual consistency.
- 📱 **Better Mobile Adaptation**: Optimized display and interaction for smaller screens.
- 📚 **More Powerful Page Collection**: The new "Page Map" now collects `md/mdx` pages and `jsx/tsx` pages from the `app/` directory.
- 🛠️ **Remote MDX Rendering**: Supports rendering MDX content from any source while generating proper sidebar navigation.
- ✨ **Enhanced Table of Contents (TOC)**: Displays Markdown headings accurately, supporting JSX, code blocks, math formulas, and more.
For more details, check out the [Nextra v4 Official Migration Guide](https://the-guild.dev/blog/nextra-4)
### Tailwind CSS v4 Updates
Tailwind CSS v4 has been officially released, introducing several significant improvements and new features:
- 🚀 **New High-Performance Engine**: Full builds are up to 5x faster, and incremental builds are over 100x faster, measured in microseconds.
- 💎 **Designed for the Modern Web**: Built on cutting-edge CSS features like cascade layers, registered custom properties with `@property`, and `color-mix()`.
- 🎯 **Simplified Installation**: Fewer dependencies, zero configuration, and just a single line of code in your CSS file.
- 📦 **First-Party Vite Plugin**: Tight integration for maximum performance and minimum configuration.
- 🔍 **Automatic Content Detection**: All of your template files are discovered automatically, with no configuration required.
- 📂 **Built-In Import Support**: No additional tooling necessary to bundle multiple CSS files.
- 🛠️ **CSS-First Configuration**: A reimagined developer experience where you customize and extend the framework directly in CSS instead of a JavaScript configuration file.
For more details, please refer to the [Tailwind CSS v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) and [Tailwind CSS Blog](https://tailwindcss.com/blog/tailwindcss-v4)
### Migration Tips
For a smooth upgrade process, we recommend:
1. Test the upgrade in a development environment first
2. Check custom component compatibility
3. Monitor console for any warnings
4. Follow the official migration guides step by step
### Documentation & Support
- 📘 [Nextra Documentation](https://nextra.site)
- 🎨 [Tailwind CSS Documentation](https://tailwindcss.com)
- 🐞 Found an issue? Open an [Issue](https://github.com/pdsuwwz/nextjs-nextra-starter/issues)
### Roadmap
We'll continue optimizing framework performance and plan to add more useful features. Stay tuned!
If you find this project useful or like the work I’ve done, please consider clicking the [⭐️ Star](https://github.com/pdsuwwz/nextjs-nextra-starter) button to show your support! Each star motivates me to keep improving. Thank you! 😊
================================================
FILE: src/content/zh/_meta.tsx
================================================
import type { MetaRecord } from 'nextra'
import { TitleBadge } from '@/components/TitleBadge'
export default {
index: {
type: 'page',
display: 'hidden',
theme: {
copyPage: false,
timestamp: false,
layout: 'full',
toc: false,
},
},
introduction: {
type: 'page',
theme: {
copyPage: false,
navbar: true,
toc: false,
},
},
docs: {
title: '📦 示例代码',
type: 'page',
},
'ai-demo': {
type: 'page',
display: 'hidden',
theme: {
copyPage: false,
toc: false,
timestamp: false,
layout: 'full',
},
},
login: {
type: 'page',
title: '登录',
display: 'hidden',
theme: {
navbar: false,
footer: false,
toc: false,
layout: 'full',
timestamp: false,
},
},
upgrade: {
title: (
<span className="flex items-center leading-[1]">
新变化
<TitleBadge />
</span>
),
type: 'page',
},
} satisfies MetaRecord
================================================
FILE: src/content/zh/ai-demo.mdx
================================================
---
title: "PulseOps AI 自动化工作流助手|落地页示例"
description: "PulseOps 落地页示例,展示 AI 自动化工作流助手在中小团队中的产品定位、功能与转化结构。"
---
import AIDemoLanding from '@/components/AIDemoLanding'
<AIDemoLanding />
================================================
FILE: src/content/zh/docs/_meta.tsx
================================================
import type { MetaRecord } from 'nextra'
export default {
// ...
} satisfies MetaRecord
================================================
FILE: src/content/zh/docs/examples/test-tailwind.mdx
================================================
# Tailwind CSS 示例
## 卡片组件
在这里,我们展示了一个经典的卡片组件。它使用了 Tailwind CSS 的工具类来快速构建响应式布局,包含了阴影、圆角和一些内边距等样式,使其看起来更具层次感和现代感。
<div className="my-5 max-w-md mx-auto bg-foreground dark:bg-neutral-600 rounded-xl shadow-md border p-6">
<h3 className="text-lg font-semibold text-zinc-200 mb-2">经典卡片</h3>
<div className="text-zinc-200 mb-4">
使用 Tailwind CSS 可以快速构建响应式卡片组件,具备良好的默认样式和扩展性。
</div>
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">
操作按钮
</button>
</div>
## 按钮状态演示
这里展示了几个不同状态的按钮,包括默认状态、禁用状态和成功/警告按钮。所有按钮都使用了 Tailwind CSS 的交互状态类,例如 `hover` 和 `disabled`,以实现不同的视觉效果。
<div className="my-5 flex gap-4 flex-wrap">
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">默认</button>
<button className="bg-gray-300 text-gray-700 px-4 py-2 rounded cursor-not-allowed" disabled>禁用</button>
<button className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition">成功</button>
<button className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition">警告</button>
</div>
## 提示框(Alert)
提示框是通知用户的重要信息或警告的组件。通过 Tailwind CSS 的背景色和边框样式,可以轻松制作不同类型的提示框。
<div className="my-5 max-w-md mx-auto">
<div className="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4">
<p className="font-bold">信息提示:</p>
<p>这是一个信息提示框,适用于展示普通的通知信息。</p>
</div>
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mt-4">
<p className="font-bold">警告提示:</p>
<p>这是一个警告提示框,适用于展示重要警告信息。</p>
</div>
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mt-4">
<p className="font-bold">错误提示:</p>
<p>这是一个错误提示框,适用于展示错误信息或需要修正的操作。</p>
</div>
</div>
## 标签组件(Tags)
标签组件可以用来展示内容的分类或标签,帮助用户快速了解内容的类型或主题。
<div className="my-5 mx-auto">
<div className="space-x-2">
<span className="inline-block bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-1 rounded-full">标签 1</span>
<span className="inline-block bg-green-100 text-green-800 text-xs font-semibold px-2 py-1 rounded-full">标签 2</span>
<span className="inline-block bg-yellow-100 text-yellow-800 text-xs font-semibold px-2 py-1 rounded-full">标签 3</span>
</div>
</div>
## 响应式布局网格
响应式布局网格可以根据不同屏幕尺寸自动调整显示方式。通过使用 Tailwind CSS 的 `grid` 类,你可以方便地创建灵活的布局,适应桌面、平板和手机等不同设备。
<div className="my-5 mx-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-accent p-4 rounded shadow text-center">区块 1</div>
<div className="bg-accent p-4 rounded shadow text-center">区块 2</div>
<div className="bg-accent p-4 rounded shadow text-center">区块 3</div>
<div className="bg-accent p-4 rounded shadow text-center">区块 4</div>
<div className="bg-accent p-4 rounded shadow text-center">区块 5</div>
<div className="bg-accent p-4 rounded shadow text-center">区块 6</div>
<div className="bg-accent p-4 rounded shadow text-center">区块 7</div>
</div>
## 排版样式(Prose)
Tailwind CSS 的 Typography 插件(`@tailwindcss/typography`)为你提供了精美的排版样式,特别适用于 Markdown 内容。使用 `prose` 类,你可以让文本、列表、引用和代码块等内容呈现出优雅的样式。
<div className="my-5 mx-auto prose dark:prose-invert max-w-3xl">
<h3>支持的排版内容</h3>
<div>
使用 Tailwind 的 Typography 插件,可以让原本没有样式的 Markdown 内容拥有很棒的默认排版。
</div>
<ol>
<li>列表项示例一</li>
<li>列表项示例二</li>
<li>列表项示例三</li>
</ol>
<blockquote>
做你该做的事,其他的交给时间 ~
</blockquote>
<div>
你可以插入一段内联代码:
比如 <code>npm install tailwindcss</code>
</div>
<div>👉 更多内容请访问 <a href="https://tailwindcss.com/docs" target="_blank">Tailwind 官方文档</a>。</div>
</div>
================================================
FILE: src/content/zh/docs/examples/theme-update.mdx
================================================
# 暗黑模式
import { ThemeSwitcher } from '@/components/ThemeSwitcher'
<ThemeSwitcher />
================================================
FILE: src/content/zh/docs/i18n.mdx
================================================
import { FileTree } from 'nextra/components'
# 国际化支持 (i18n)
本项目提供了两种国际化实现方式:
1. Nextra 内置的文件夹结构国际化
2. 自定义 i18n 实现,用于组件和客户端内容
## Nextra 内置的国际化支持
Nextra 通过文件夹结构来支持多语言内容。在项目根目录下,您可以创建不同语言的文件夹,如:
<FileTree>
<FileTree.Folder name="src" defaultOpen>
<FileTree.Folder name="content" defaultOpen>
<FileTree.Folder name="en" defaultOpen>
<FileTree.File name="_meta.tsx" active />
<FileTree.File name="index.mdx" />
<FileTree.File name="introduction.mdx" />
</FileTree.Folder>
<FileTree.Folder name="zh" defaultOpen>
<FileTree.File name="_meta.tsx" active />
<FileTree.File name="index.mdx" />
<FileTree.File name="introduction.mdx" />
</FileTree.Folder>
</FileTree.Folder>
</FileTree.Folder>
</FileTree>
可以通过 `_meta.tsx` 文件配置语言切换器:
```tsx
import type { MetaRecord } from 'nextra'
export default {
index: {
type: 'page',
display: 'hidden',
theme: {
timestamp: false,
layout: 'full',
toc: false,
},
},
introduction: {
type: 'page',
theme: {
navbar: true,
toc: false,
},
},
} satisfies MetaRecord
```
## 自定义 i18n 实现
对于组件和客户端内容,我们实现了类型安全的国际化解决方案。
### 目录结构
<FileTree>
<FileTree.Folder name="src" defaultOpen>
<FileTree.Folder name="i18n" defaultOpen>
<FileTree.File name="index.ts" comment="核心功能和类型定义" />
<FileTree.File name="en.ts" comment="英文 json 语言包" />
<FileTree.File name="zh.ts" comment="中文 json 语言包" />
</FileTree.Folder>
<FileTree.Folder name="hooks" defaultOpen>
<FileTree.File name="useLocale.ts" comment="封装好的通用 Hooks" />
</FileTree.Folder>
</FileTree.Folder>
</FileTree>
### 语言文件示例
语言文件结构如下(以 [`zh.ts`](https://github.com/pdsuwwz/nextjs-nextra-starter/blob/main/src/i18n/zh.ts) 为例):
```typescript
export default {
systemTitle: '🚀 Nextra 启动模板',
banner: {
title: '👋 嘿,欢迎来到 Next.js 起步模板!',
more: '了解详情',
},
badgeTitle: '轻量级、开箱即用 🎉',
featureSupport: `🔥 现在支持 {{feature}}!`,
lastUpdated: '最后更新于:',
getStarted: '开始使用',
// ...
}
```
### 使用方法
#### 1. 在组件中使用
```tsx
import { useLocale } from '@/hooks'
function MyComponent() {
const { t, currentLocale } = useLocale()
return (
<div>
<h1>{t('home.systemTitle')}</h1>
{/* 使用变量插值 */}
<div dangerouslySetInnerHTML={{
__html: t('home.featureSupport', {
feature: '<span>Tailwind CSS v4, Nextra v4</span>',
}),
}} />
</div>
)
}
```
#### 2. 动态切换语言
通过 URL 路径切换语言(例如 [`/en/introduction`](/en/introduction) 和 [`/zh/introduction`](/zh/introduction))。当前语言会从 URL 参数中自动获取。
## 类型安全
我们的 i18n 实现提供了完整的类型安全支持:
1. **自动补全:** 编辑器中会自动提示所有可用的翻译键
2. **类型检查:** 使用错误的键会触发 TypeScript 错误
3. **嵌套键支持:** 支持如 `home.title` 的点表示法访问
4. **变量插值:** 可以在翻译中使用 `{{variable}}` 语法
## 高级功能
### 嵌套值获取
`getNestedValue` 函数可以根据点表示法路径获取对象中的嵌套值:
```typescript
const value = getNestedValue(i18nConfig[currentLocale], 'home.title')
```
### 字符串插值
`interpolateString` 函数支持在翻译字符串中插入变量:
```typescript
const result = interpolateString(
'支持 {{feature}}',
{ feature: 'Tailwind CSS v4' }
) // "支持 Tailwind CSS v4"
```
### 自定义 Hooks
`useLocale` Hooks 封装了语言检测和翻译功能,提供:
- `currentLocale`: 当前语言代码
- `t`: 翻译函数,支持变量插值
## 最佳实践
1. **组织翻译文件:** 使用嵌套对象将相关翻译分组
2. **避免硬编码字符串:** 总是使用 `t()` 函数而不是硬编码文本
3. **设置默认语言:** 确保有默认语言回退机制
4. **保持键的一致性:** 所有语言文件中应包含相同的键
5. **使用 TypeScript:** 利用类型系统确保翻译的完整性和正确性
## 实战示例
以下是 `SetupHero` 组件中使用 i18n 的示例:
```tsx
'use client'
import { useLocale } from '@/hooks'
export function SetupHero() {
const { t, currentLocale } = useLocale()
return (
<div>
<a href="https://github.com/pdsuwwz/nextjs-nextra-starter">
{t('badgeTitle')}
</a>
<Link href={`/${currentLocale}/upgrade`}>
<span dangerouslySetInnerHTML={{
__html: t('featureSupport', {
feature: `<span>Tailwind CSS v4, Nextra v4</span>`,
}),
}} />
</Link>
<Button asChild>
<Link href={`/${currentLocale}/introduction`}>
{t('getStarted')}
</Link>
</Button>
</div>
)
}
```
## 进一步扩展
- **语言检测:** 添加基于浏览器或用户偏好的自动语言检测
- **数字和日期格式化:** 集成 `Intl` API 进行本地化格式化
- **多元复数:** 添加对不同语言复数规则的支持
- **翻译管理界面:** 为内容编辑者创建翻译管理工具
================================================
FILE: src/content/zh/docs/index.mdx
================================================
---
title: "测试概览"
---
<h1>这是概览内容</h1>
================================================
FILE: src/content/zh/index.mdx
================================================
---
title: "首页"
---
import HomepageHero from "@/components/HomepageHero";
<HomepageHero />
================================================
FILE: src/content/zh/introduction.mdx
================================================
---
title: "介绍"
---
import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
# 简单介绍
一个免费、开源且功能强大的模板,采用 Next.js + Nextra + TypeScript + Tailwind CSS
```js
console.log('hello, world')
```
## 安装和运行
* 安装依赖
```bash
pnpm install
```
* 本地开发
```bash
pnpm run dev
```
<Alert>
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
You can add components and dependencies to your app using the cli.
</AlertDescription>
</Alert>
---
<HoverCard>
<HoverCardTrigger>
<Button>
😄 Hover Me
</Button>
</HoverCardTrigger>
<HoverCardContent>
The React Framework – created and maintained by @vercel.
</HoverCardContent>
</HoverCard>
================================================
FILE: src/content/zh/login.mdx
================================================
---
title: 登录
---
import LoginForm from '@/components/auth/login-form.client'
<div data-pagefind-ignore="all">
<LoginForm />
</div>
================================================
FILE: src/content/zh/upgrade.mdx
================================================
---
title: "新变化"
---
import { Callout } from 'nextra/components'
# 🔥 重大框架升级
本次完成了核心框架的重大升级,为您带来更好的开发体验!
<Callout emoji={<span className='icon-[ri--alarm-warning-fill]'></span>} type="info">
注意
本版本为最新发布版本,虽然已进行过测试,但仍可能存在一些未被发现的问题。如遇到任何异常或不稳定的情况,请及时[反馈](https://github.com/pdsuwwz/nextjs-nextra-starter/issues),我会尽快修复。
</Callout>
### Nextra v4 更新要点
Nextra v4 现已发布,带来了诸多重要改进和新特性:
- 🚀 **App Router 支持**:全面切换至 Next.js App Router,淘汰 Pages Router,支持最新的 Metadata API。
- ⚡ **更快的页面加载**:优化后的打包体积与性能,使网站响应更迅速。
- 🔍 **全新搜索引擎**:基于 Rust 的 Pagefind 提供更精准、更高效的搜索体验。
- 🏎️ **Turbopack 支持**:改进开发体验,解决长期的社区需求。
- 🌍 **RSC + i18n**:增强国际化支持,使多语言网站开发更加便捷。
- 🎨 **优化主题样式**:改进 UI 设计,增强视觉一致性。
- 📱 **更好的移动端适配**:针对小屏幕设备优化显示效果和交互体验。
- 📚 **更强大的目录结构**:新的 “Page Map” 支持收集 md/mdx 与 app/ 目录下的 jsx/tsx 页面。
- 🛠️ **远程 MDX 渲染**:支持从任意内容源加载 MDX,并正确生成侧边栏导航。
- ✨ **增强的目录 (TOC)**:精确呈现 Markdown 标题,支持 JSX、代码块、数学公式等内容。
详细信息请参考 [Nextra v4 官方升级指南](https://the-guild.dev/blog/nextra-4)
### Tailwind CSS v4 更新要点
Tailwind CSS v4 已正式发布,带来了多个重要的改进和新特性:
- 🚀 **全新高性能引擎**:完整构建速度提升最多 5 倍,增量构建速度提升超过 100 倍,单位为微秒。
- 💎 **为现代 Web 设计**:基于前沿的 CSS 特性构建,如层叠层(cascade layers)、带有 `@property` 的自定义属性和 `color-mix()`。
- 🎯 **简化安装过程**:更少的依赖,零配置,只需在你的 CSS 文件中添加一行代码。
- 📦 **官方 Vite 插件**:紧密集成,最大化性能并最小化配置。
- 🔍 **自动内容检测**:自动发现所有模板文件,无需配置。
- 📂 **内置导入支持**:无需额外工具即可捆绑多个 CSS 文件。
- 🛠️ **CSS 优先配置**:重新构思的开发者体验,直接在 CSS 中自定义和扩展框架,无需 tailwind.config 配置文件。
更多详情,请参考 [Tailwind CSS v4 升级指南](https://tailwindcss.com/docs/upgrade-guide) 和 [官方博客](https://tailwindcss.com/blog/tailwindcss-v4)。
### 迁移说明
为确保顺利升级,建议:
1. 先在测试环境进行升级验证
2. 检查自定义组件的兼容性
3. 查看控制台是否有警告信息
4. 按照官方迁移指南逐步操作
### 文档与支持
- 📘 [Nextra 官方文档](https://nextra.site)
- 🎨 [Tailwind CSS 官方文档](https://tailwindcss.com)
- 🐞 遇到问题?欢迎提交 [Issue](https://github.com/pdsuwwz/nextjs-nextra-starter/issues)
### 后续计划
将持续优化框架性能,并计划添加更多实用功能。敬请期待!
如果你觉得这个项目有帮助,或喜欢我做的工作,欢迎点击项目右上角的 [⭐️ Star](https://github.com/pdsuwwz/nextjs-nextra-starter) 按钮进行支持!每一个星标都是对我最大的鼓励,帮助我不断改进和前进。非常感激你的支持!😊
================================================
FILE: src/hooks/index.ts
================================================
export * from './useBreakpoint'
export * from './useLocale'
export * from './useServerLocale'
================================================
FILE: src/hooks/useBreakpoint.ts
================================================
'use client'
import { useMediaQuery } from 'react-responsive'
export const useBreakpoint = () => {
// sm 640px @media (max-width: 639px) { ... }
const isSm = useMediaQuery({ query: '(max-width: 639px)' })
// md 768px @media (max-width: 767px) { ... }
const isMd = useMediaQuery({ query: '(max-width: 767px)' })
// lg 1024px @media (max-width: 1023px) { ... }
const isLg = useMediaQuery({ query: '(max-width: 1023px)' })
// xl 1280px @media (max-width: 1279px) { ... }
const isXl = useMediaQuery({ query: '(max-width: 1279px)' })
// 2xl 1536px @media (max-width: 1535px) { ... }
const is2Xl = useMediaQuery({ query: '(max-width: 1535px)' })
return { isSm, isMd, isLg, isXl, is2Xl }
}
================================================
FILE: src/hooks/useLocale.ts
================================================
'use client'
import type { AllLocales, I18nLangKeys, LocaleKeys, PathValue } from '@/i18n'
import { useParams } from 'next/navigation' // 改用 next/navigation
import { useCallback } from 'react'
import { getNestedValue, i18nConfig, interpolateString } from '@/i18n'
// 类型获取给定键的本地化值的类型
type LocalizedValue<T, K extends LocaleKeys> = PathValue<T, K> extends string
? string
: PathValue<T, K>
export const useLocale = () => {
const params = useParams()
// 从 URL 参数中获取当前语言
const currentLocale = (
(params?.lang as I18nLangKeys)
|| 'en'
) as I18nLangKeys
const t = useCallback(
<K extends LocaleKeys>(
key: K,
withData: Record<string, any> = {},
): LocalizedValue<AllLocales, K> => {
const template = getNestedValue(i18nConfig[currentLocale], key)
if (typeof template === 'string') {
return interpolateString(template, withData) as LocalizedValue<AllLocales, K>
}
return template as LocalizedValue<AllLocales, K>
},
[currentLocale],
)
return {
currentLocale,
t,
}
}
================================================
FILE: src/hooks/useServerLocale.ts
================================================
import type { AllLocales, I18nLangKeys, LocaleKeys, PathValue } from '@/i18n'
import { getNestedValue, i18nConfig, interpolateString } from '@/i18n'
// 类型获取给定键的本地化值的类型
type LocalizedValue<T, K extends LocaleKeys> = PathValue<T, K> extends string
? string
: PathValue<T, K>
interface ServerLocaleParams {
params: {
lang?: string
}
}
export async function useServerLocale(lang: I18nLangKeys) {
// 从参数中获取当前语言
const currentLocale = lang
function t<K extends LocaleKeys>(
key: K,
withData: Record<string, any> = {},
): LocalizedValue<AllLocales, K> {
const template = getNestedValue(i18nConfig[currentLocale], key)
if (typeof template === 'string') {
return interpolateString(template, withData) as LocalizedValue<AllLocales, K>
}
return template as LocalizedValue<AllLocales, K>
}
return {
currentLocale,
t,
}
}
================================================
FILE: src/i18n/ai-demo.ts
================================================
import type { I18nLangKeys } from './index'
import { i18nConfig } from './index'
type HeroCopy = {
title: string
subtitle: string
tryDemo: string
bookCall: string
startTrial: string
trust: string
}
type SocialProof = {
logos: string[]
stats: Array<{ label: string, value: string }>
}
type Feature = {
title: string
description: string
}
type Step = {
title: string
description: string
}
type UseCase = {
title: string
description: string
}
type Testimonial = {
quote: string
name: string
role: string
}
export type Plan = {
name: string
price: string
description: string
points: string[]
cta: string
highlight?: boolean
}
type FAQ = {
question: string
answer: string
}
type FinalCta = {
title: string
description: string
primary: string
secondary: string
}
type Footer = {
productName: string
copyright: string
contactTitle: string
contactEmail: string
contactPhone: string
}
export type LandingCopy = {
nav: {
product: string
tryDemo: string
bookCall: string
startTrial: string
}
hero: HeroCopy
socialProofTitle: string
socialProof: SocialProof
featureTitle: string
features: Feature[]
howItWorksTitle: string
steps: Step[]
useCasesTitle: string
useCases: UseCase[]
demoTitle: string
demoDescription: string
testimonialsTitle: string
testimonials: Testimonial[]
pricingTitle: string
pricingSubtitle: string
plans: Plan[]
faqTitle: string
faqs: FAQ[]
finalCta: FinalCta
footer: Footer
}
export function getLandingCopy(lang: string): LandingCopy {
const safeLang: I18nLangKeys = lang === 'zh' ? 'zh' : 'en'
return i18nConfig[safeLang].aiDemo as LandingCopy
}
================================================
FILE: src/i18n/en.ts
================================================
export default {
systemTitle: '🚀 My Nextra Starter',
banner: {
title: '👋 Hey there! Welcome to the Next.js Starter.',
more: 'Check it out',
},
pageTitle: 'On This Page',
backToTop: 'Back to top',
search: {
placeholder: 'Search...',
noResults: 'No results found',
errorText: 'Search error',
loading: 'Loading...',
},
badgeTitle: 'Lightweight & Easy 🎉',
featureSupport: `🔥 Now with {{feature}} support!`,
lastUpdated: 'Last updated on:',
getStarted: 'Get Started',
themeSwitcher: {
light: 'Light Mode',
dark: 'Dark Mode',
lightAria: 'Switch to light mode',
darkAria: 'Switch to dark mode',
},
auth: {
login: 'Login',
logout: 'Logout',
brand: 'Nextra Starter',
welcome: 'Welcome back',
email: 'Email',
emailPlaceholder: 'you@example.com',
password: 'Password',
passwordPlaceholder: '••••••••',
submit: 'Sign in',
or: 'or',
google: 'Continue with Google',
googleLoading: 'Signing in...',
loading: 'Loading...',
backHome: 'Back to home',
success: 'Login successful',
invalidEmail: 'Please enter a valid email address.',
passwordRequired: 'Please enter your password.',
storageError: 'Unable to save login state. Please try again.',
},
featureList: [
{
title: 'Advanced Tech Stack',
description: 'Leveraging efficient React (v19) and support with Next.js, Nextra(v4) and Shadcn UI to build modern applications.',
},
{
title: 'internationalization (i18n)',
description: 'Built-in multi-language support for easy i18n of your application, expanding your user base.',
},
{
title: 'TypeScript Safety',
description: 'Fully integrated with TypeScript, offering static type checking to reduce runtime errors and enhance code reliability and maintainability.',
},
{
title: 'Iconify Icons',
description: 'Integrated with the Iconify icon set, offering a wide range of icons to enhance UI visual presentation.',
},
{
title: 'Tailwind CSS (v4)',
description: 'Atomic CSS integrated with Tailwind CSS, enabling efficient design and responsive UI.',
},
{
title: 'Code Standards',
description: 'Adheres to best practices with code standards and uses ESLint for quality checks and consistency.',
},
{
title: 'Dark Mode',
description: 'Supports dark mode for an enhanced nighttime experience.',
},
{
title: 'Rich Components & Extensible Support',
description: 'Offers a range of built-in components and supports flexible custom extensions.',
},
{
title: 'Lightweight Design',
description: 'Employs a lightweight design approach, streamlining project setup to focus on content creation.',
},
],
featuresDesc: 'Easily build modern applications and kickstart your development process.',
homeEnhance: {
quickStatsTitle: 'Build once, ship repeatedly',
quickStatsDesc: 'A practical base for docs, landing pages, and product frontends that need to look and feel ready.',
quickStats: [
{ value: '15 min', label: 'From clone to first page' },
{ value: 'v16 + v19', label: 'Next.js and React modern stack' },
{ value: 'i18n Ready', label: 'English and Chinese routing out of the box' },
{ value: 'Dark Mode', label: 'Theme system already wired' },
],
useCasesTitle: 'Signals that matter',
useCases: [
{
title: 'AI SaaS Landing',
description: 'Ship a conversion-focused marketing page with reusable sections and clear CTA rhythm.',
tag: 'Growth',
},
{
title: 'Dev Docs Hub',
description: 'Publish multilingual docs with Nextra structure, search, and clean content organization.',
tag: 'Documentation',
},
{
title: 'Blog + Auth Demo',
description: 'Start from a practical baseline that includes login flow and frontend auth examples.',
tag: 'Product',
},
],
flowTitle: 'From setup to shipping',
flow: [
{
title: '1. Initialize',
description: 'Install dependencies and run the dev server.',
},
{
title: '2. Customize',
description: 'Edit sections, i18n copy, and design tokens.',
},
{
title: '3. Launch',
description: 'Deploy to Netlify or Vercel with one command.',
},
],
ctaTitle: 'Starter-ready scenarios',
ctaDescription: 'Use the AI demo page as your reference and turn it into your own product narrative in a few focused iterations.',
ctaPrimary: 'View AI Demo',
ctaSecondary: 'Read Introduction',
},
faqs: [
{
question: 'What frameworks and tech stack does this starter template support?',
answer: 'This starter template supports Next.js and Nextra, with integrated modern development technologies like Tailwind CSS, Framer Motion, and Shadcn UI components.',
},
{
question: 'How do I start developing with this template?',
answer: 'Simply clone our GitHub repository and follow the steps in the documentation to run the installation commands to get started.',
},
{
question: 'What types of projects is this template suitable for?',
answer: 'This template is ideal for building fast and efficient modern web applications, including corporate sites, personal blogs, and e-commerce platforms.',
},
{
question: 'How do I add or modify components in the project?',
answer: 'You can use the provided component library and follow the instructions in the documentation to customize and extend them to suit your specific needs.',
},
{
question: 'Does the template support multiple languages?',
answer: 'Yes, the template includes built-in internationalization (i18n) support, allowing you to easily add and manage multilingual content to expand your app\'s international user base.',
},
{
question: 'How can I get technical support or help?',
answer: 'If you encounter any issues while using the template, please contact us via GitHub @pdsuwwz.',
},
{
question: '🐒 What does the author need most right now?',
answer: 'Stars! ⭐️ Coding till bald, just need Stars to heal my soul... 🥺',
},
],
aiDemo: {
nav: {
product: 'PulseOps',
tryDemo: 'Try Demo',
bookCall: 'Book a Call',
startTrial: 'Start Free Trial',
},
hero: {
title: 'Automate repetitive ops and ship more every week',
subtitle: 'PulseOps helps indie builders and small teams turn SOPs into AI workflows in hours, not weeks.',
tryDemo: 'Try Demo',
bookCall: 'Book a Call',
startTrial: 'Start Free Trial',
trust: 'No credit card required · Setup in 10 minutes',
},
socialProofTitle: 'Social proof',
socialProof: {
logos: ['Marlow Ke', 'Nina Zhou', 'Evan Lin', 'Luca Ren', 'Iris Qiao'],
stats: [
{ value: '41%', label: 'Average time saved on ops tasks' },
{ value: '3.6x', label: 'Faster handoff from idea to execution' },
{ value: '12 hrs', label: 'Recovered weekly focus time per team' },
],
},
featureTitle: 'Core capabilities built for lean teams',
features: [
{
title: 'Workflow Builder',
description: 'Design multi-step automations with plain language prompts and reusable blocks.',
},
{
title: 'AI Task Routing',
description: 'Automatically assign tasks to the right person, tool, or bot based on context.',
},
{
title: 'Knowledge Sync',
description: 'Connect docs, tickets, and chat history to give your automations live context.',
},
{
title: 'Approval Guardrails',
description: 'Set review checkpoints before high-impact actions are executed.',
},
{
title: 'Execution Analytics',
description: 'Track success rates, cycle time, and ROI with clear operational dashboards.',
},
{
title: 'Template Library',
description: 'Launch proven workflow templates for support, growth, and product ops.',
},
],
howItWorksTitle: 'How it works',
steps: [
{
title: '1. Map your process',
description: 'Upload SOPs or describe a process in plain English.',
},
{
title: '2. Generate and customize',
description: 'PulseOps creates a draft automation that your team can refine in minutes.',
},
{
title: '3. Run with control',
description: 'Launch workflows with approvals, alerts, and measurable outcomes.',
},
],
useCasesTitle: 'Common use cases',
useCases: [
{
title: 'Customer support triage',
description: 'Classify, summarize, and route tickets before an agent opens them.',
},
{
title: 'Weekly growth reporting',
description: 'Collect data, generate summaries, and post actionable updates automatically.',
},
{
title: 'Product launch ops',
description: 'Coordinate assets, checklists, and approvals across tools without manual follow-up.',
},
],
demoTitle: 'Product demo preview',
demoDescription: 'A single command center to monitor runs, detect bottlenecks, and trigger next actions.',
testimonialsTitle: 'What customers say',
testimonials: [
{
quote: 'We replaced scattered Zap workflows with one clear system. Operations finally feel predictable.',
name: 'Maya Chen',
role: 'Founder, Marlow Ke Team',
},
{
quote: 'Our four-person team now runs onboarding and support flows with enterprise-level
gitextract_lpxxmf0u/ ├── .gitignore ├── .npmrc ├── .vscode/ │ ├── settings.json │ └── tailwind.json ├── LICENSE ├── README-en.md ├── README.md ├── components.json ├── eslint.config.js ├── next-env.d.ts ├── next-sitemap.config.mjs ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── src/ │ ├── app/ │ │ ├── [lang]/ │ │ │ ├── [[...mdxPath]]/ │ │ │ │ └── page.tsx │ │ │ ├── _components/ │ │ │ │ ├── ThemeProvider.tsx │ │ │ │ └── ThirdPartyScripts.tsx │ │ │ ├── layout.tsx │ │ │ ├── not-found.ts │ │ │ └── styles/ │ │ │ ├── index.css │ │ │ └── overrides.css │ │ └── _dictionaries/ │ │ └── get-dictionary.ts │ ├── components/ │ │ ├── AIDemoLanding/ │ │ │ ├── EntryCard.tsx │ │ │ ├── index.tsx │ │ │ └── interactions.tsx │ │ ├── CustomFooter/ │ │ │ └── index.tsx │ │ ├── HomepageHero/ │ │ │ ├── Section.tsx │ │ │ ├── Setup.tsx │ │ │ ├── SetupHero.module.css │ │ │ └── index.tsx │ │ ├── MotionWrapper/ │ │ │ ├── FadeIn.tsx │ │ │ ├── Flash.tsx │ │ │ └── index.ts │ │ ├── PanelParticles/ │ │ │ └── index.tsx │ │ ├── ScrollProgressBar/ │ │ │ └── index.tsx │ │ ├── ThemeSwitcher/ │ │ │ └── index.tsx │ │ ├── TitleBadge/ │ │ │ └── index.tsx │ │ ├── auth/ │ │ │ ├── login-form.client.tsx │ │ │ └── login-form.tsx │ │ └── ui/ │ │ ├── accordion.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card-hover-effect.tsx │ │ ├── card.tsx │ │ ├── flip-words.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── link-preview.tsx │ │ ├── loader.tsx │ │ ├── separator.tsx │ │ ├── sonner.tsx │ │ └── toggle.tsx │ ├── content/ │ │ ├── en/ │ │ │ ├── _meta.tsx │ │ │ ├── ai-demo.mdx │ │ │ ├── docs/ │ │ │ │ ├── _meta.tsx │ │ │ │ ├── examples/ │ │ │ │ │ ├── test-tailwind.mdx │ │ │ │ │ └── theme-update.mdx │ │ │ │ ├── i18n.mdx │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ ├── introduction.mdx │ │ │ ├── login.mdx │ │ │ └── upgrade.mdx │ │ └── zh/ │ │ ├── _meta.tsx │ │ ├── ai-demo.mdx │ │ ├── docs/ │ │ │ ├── _meta.tsx │ │ │ ├── examples/ │ │ │ │ ├── test-tailwind.mdx │ │ │ │ └── theme-update.mdx │ │ │ ├── i18n.mdx │ │ │ └── index.mdx │ │ ├── index.mdx │ │ ├── introduction.mdx │ │ ├── login.mdx │ │ └── upgrade.mdx │ ├── hooks/ │ │ ├── index.ts │ │ ├── useBreakpoint.ts │ │ ├── useLocale.ts │ │ └── useServerLocale.ts │ ├── i18n/ │ │ ├── ai-demo.ts │ │ ├── en.ts │ │ ├── index.ts │ │ └── zh.ts │ ├── lib/ │ │ └── utils.ts │ ├── mdx-components.ts │ ├── proxy.ts │ └── widgets/ │ ├── auth-button.tsx │ ├── locale-toggle.tsx │ ├── mobile-menu-auth.tsx │ ├── navbar-extras.tsx │ └── theme-toggle.tsx └── tsconfig.json
SYMBOL INDEX (101 symbols across 34 files)
FILE: eslint.config.js
constant OFF (line 3) | const OFF = 0
constant WARN (line 4) | const WARN = 1
constant ERROR (line 5) | const ERROR = 2
FILE: src/app/[lang]/[[...mdxPath]]/page.tsx
function generateMetadata (line 6) | async function generateMetadata(props: PageProps) {
type PageProps (line 12) | type PageProps = Readonly<{
function Page (line 20) | async function Page(props: PageProps) {
FILE: src/app/[lang]/_components/ThemeProvider.tsx
function ThemeProvider (line 6) | function ThemeProvider({
FILE: src/app/[lang]/_components/ThirdPartyScripts.tsx
constant GA_ID (line 5) | const GA_ID = 'G-VCR6017LB8'
constant BAIDU_SRC (line 6) | const BAIDU_SRC = 'https://hm.baidu.com/hm.js?d5ad5e04e6af914c0176792656...
function ThirdPartyScripts (line 8) | function ThirdPartyScripts() {
FILE: src/app/[lang]/layout.tsx
function RootLayout (line 72) | async function RootLayout({ children, params }: LayoutProps<'/[lang]'>) {
FILE: src/components/AIDemoLanding/EntryCard.tsx
function EntryCard (line 6) | function EntryCard() {
FILE: src/components/AIDemoLanding/index.tsx
type SectionProps (line 10) | type SectionProps = {
function Section (line 21) | function Section({ id, title, children, compact }: SectionProps) {
function FeatureIcon (line 34) | function FeatureIcon({ index }: { index: number }) {
function TopNav (line 40) | function TopNav({ copy }: { copy: LandingCopy }) {
function Hero (line 70) | function Hero({ copy }: { copy: LandingCopy }) {
function SocialProof (line 142) | function SocialProof({ copy }: { copy: LandingCopy }) {
function Features (line 166) | function Features({ copy }: { copy: LandingCopy }) {
function HowItWorks (line 185) | function HowItWorks({ copy }: { copy: LandingCopy }) {
function UseCases (line 203) | function UseCases({ copy }: { copy: LandingCopy }) {
function DemoPreview (line 218) | function DemoPreview({ copy, lang }: { copy: LandingCopy, lang: string }) {
function Testimonials (line 234) | function Testimonials({ copy }: { copy: LandingCopy }) {
function Pricing (line 250) | function Pricing({ copy, lang }: { copy: LandingCopy, lang: string }) {
function FAQ (line 259) | function FAQ({ copy }: { copy: LandingCopy }) {
function FinalCta (line 278) | function FinalCta({ copy }: { copy: LandingCopy }) {
function Footer (line 306) | function Footer({ copy }: { copy: LandingCopy }) {
function AIDemoLanding (line 331) | function AIDemoLanding() {
FILE: src/components/AIDemoLanding/interactions.tsx
type BillingCycle (line 7) | type BillingCycle = 'monthly' | 'yearly'
type InteractiveDemoProps (line 9) | type InteractiveDemoProps = {
type InteractivePricingProps (line 14) | type InteractivePricingProps = {
type Scenario (line 19) | type Scenario = {
function getDemoScenarios (line 26) | function getDemoScenarios(lang: string): Scenario[] {
function parsePriceNumber (line 96) | function parsePriceNumber(price: string): number | null {
function formatPlanPrice (line 104) | function formatPlanPrice(price: string, cycle: BillingCycle, lang: strin...
function InteractiveDemoPanel (line 126) | function InteractiveDemoPanel({ lang, ctaText }: InteractiveDemoProps) {
function InteractivePricingCards (line 179) | function InteractivePricingCards({ lang, plans }: InteractivePricingProp...
FILE: src/components/CustomFooter/index.tsx
function CustomFooter (line 37) | function CustomFooter() {
FILE: src/components/HomepageHero/Section.tsx
type Props (line 5) | interface Props {
FILE: src/components/HomepageHero/Setup.tsx
type Props (line 12) | interface Props {
function SetupHero (line 14) | function SetupHero(props: Props) {
FILE: src/components/HomepageHero/index.tsx
function HomepageHero (line 35) | function HomepageHero() {
FILE: src/components/MotionWrapper/FadeIn.tsx
type MotionWrapperFadeInProps (line 7) | interface MotionWrapperFadeInProps {
FILE: src/components/MotionWrapper/Flash.tsx
type Props (line 7) | interface Props {
FILE: src/components/ScrollProgressBar/index.tsx
type ScrollProgressBarProps (line 6) | interface ScrollProgressBarProps {
function ScrollProgressBar (line 20) | function ScrollProgressBar({
FILE: src/components/TitleBadge/index.tsx
type Props (line 6) | interface Props {
FILE: src/components/auth/login-form.client.tsx
function LoginFormClient (line 9) | function LoginFormClient() {
FILE: src/components/auth/login-form.tsx
constant STORAGE_KEY (line 12) | const STORAGE_KEY = 'auth:userEmail'
type ErrorType (line 14) | type ErrorType = 'invalidEmail' | 'passwordRequired' | 'storage' | null
function LoginForm (line 16) | function LoginForm() {
FILE: src/components/ui/alert.tsx
function Alert (line 22) | function Alert({
function AlertTitle (line 37) | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
function AlertDescription (line 50) | function AlertDescription({
FILE: src/components/ui/button.tsx
function Button (line 41) | function Button({
FILE: src/components/ui/card.tsx
function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<"...
function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardContent (line 64) | function CardContent({ className, ...props }: React.ComponentProps<"div"...
function CardFooter (line 74) | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
FILE: src/components/ui/input.tsx
function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...
FILE: src/components/ui/label.tsx
function Label (line 8) | function Label({
FILE: src/components/ui/link-preview.tsx
type LinkPreviewProps (line 15) | type LinkPreviewProps = {
FILE: src/hooks/useLocale.ts
type LocalizedValue (line 9) | type LocalizedValue<T, K extends LocaleKeys> = PathValue<T, K> extends s...
FILE: src/hooks/useServerLocale.ts
type LocalizedValue (line 5) | type LocalizedValue<T, K extends LocaleKeys> = PathValue<T, K> extends s...
type ServerLocaleParams (line 9) | interface ServerLocaleParams {
function useServerLocale (line 15) | async function useServerLocale(lang: I18nLangKeys) {
FILE: src/i18n/ai-demo.ts
type HeroCopy (line 4) | type HeroCopy = {
type SocialProof (line 13) | type SocialProof = {
type Feature (line 18) | type Feature = {
type Step (line 23) | type Step = {
type UseCase (line 28) | type UseCase = {
type Testimonial (line 33) | type Testimonial = {
type Plan (line 39) | type Plan = {
type FAQ (line 48) | type FAQ = {
type FinalCta (line 53) | type FinalCta = {
type Footer (line 60) | type Footer = {
type LandingCopy (line 68) | type LandingCopy = {
function getLandingCopy (line 97) | function getLandingCopy(lang: string): LandingCopy {
FILE: src/i18n/index.ts
type I18nLangKeys (line 9) | type I18nLangKeys = keyof typeof i18nConfig
type I18nLangAsyncProps (line 10) | interface I18nLangAsyncProps {
type AllLocales (line 15) | type AllLocales = typeof i18nConfig[I18nLangKeys]
type DeepKeys (line 18) | type DeepKeys<T> = {
type NestedKeyOf (line 25) | type NestedKeyOf<ObjectType extends object> = {
type LocaleKeys (line 32) | type LocaleKeys = NestedKeyOf<AllLocales>
type DeepObject (line 35) | type DeepObject = Record<string, any>
type PathValue (line 38) | type PathValue<T, P extends string>
function getNestedValue (line 48) | function getNestedValue<T extends DeepObject, K extends string>(obj: T, ...
function interpolateString (line 54) | function interpolateString(template: string, context: Record<string, any...
FILE: src/lib/utils.ts
function cn (line 5) | function cn(...inputs: ClassValue[]) {
FILE: src/widgets/auth-button.tsx
constant STORAGE_KEY (line 9) | const STORAGE_KEY = 'auth:userEmail'
type AuthButtonProps (line 11) | type AuthButtonProps = {
function AuthButton (line 16) | function AuthButton({ className, showOnMobile }: AuthButtonProps) {
FILE: src/widgets/locale-toggle.tsx
constant ONE_YEAR (line 10) | const ONE_YEAR = 365 * 24 * 60 * 60 * 1000
function LocaleToggle (line 15) | function LocaleToggle({
FILE: src/widgets/mobile-menu-auth.tsx
constant CONTAINER_ATTR (line 7) | const CONTAINER_ATTR = 'data-mobile-auth'
function MobileMenuAuth (line 9) | function MobileMenuAuth() {
FILE: src/widgets/navbar-extras.tsx
function NavbarExtras (line 8) | function NavbarExtras() {
FILE: src/widgets/theme-toggle.tsx
function ThemeToggle (line 11) | function ThemeToggle({
Condensed preview — 92 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (237K chars).
[
{
"path": ".gitignore",
"chars": 485,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": ".npmrc",
"chars": 55,
"preview": "enable-pre-post-scripts=true\nstore-dir=.pnpm-store/v10\n"
},
{
"path": ".vscode/settings.json",
"chars": 795,
"preview": "{\n \"css.customData\": [\".vscode/tailwind.json\"],\n \"editor.formatOnSave\": false,\n \"editor.codeActionsOnSave\": {\n \"so"
},
{
"path": ".vscode/tailwind.json",
"chars": 2756,
"preview": "{\n \"version\": 1.1,\n \"atDirectives\": [\n {\n \"name\": \"@tailwind\",\n \"description\": \"Use the `@tailwind` direc"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2020-PRESENT Wisdom\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README-en.md",
"chars": 10099,
"preview": "<p style=\"text-align:center;\" align=\"center\"><a href=\"https://github.com/pdsuwwz/nextjs-nextra-starter\"><picture align=\""
},
{
"path": "README.md",
"chars": 6919,
"preview": "<p style=\"text-align:center;\" align=\"center\"><a href=\"https://github.com/pdsuwwz/nextjs-nextra-starter\"><picture align=\""
},
{
"path": "components.json",
"chars": 534,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "eslint.config.js",
"chars": 1663,
"preview": "import antfu from '@antfu/eslint-config'\n\nconst OFF = 0\nconst WARN = 1\nconst ERROR = 2\n\nexport default antfu({\n ignores"
},
{
"path": "next-env.d.ts",
"chars": 247,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n//"
},
{
"path": "next-sitemap.config.mjs",
"chars": 150,
"preview": "/** @type {import('next-sitemap').IConfig} */\nexport default {\n siteUrl: process.env.SITE_URL || 'https://example.com',"
},
{
"path": "next.config.ts",
"chars": 448,
"preview": "import createWithNextra from 'nextra'\n\nconst withNextra = createWithNextra({\n defaultShowCopyCode: true,\n unstable_sho"
},
{
"path": "package.json",
"chars": 2706,
"preview": "{\n \"name\": \"nextjs-nextra-starter\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"description\": \"Next.js Nextra (v4) Tai"
},
{
"path": "postcss.config.mjs",
"chars": 133,
"preview": "/** @type {import('postcss').Postcss} */\nconst config = {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n\nexport de"
},
{
"path": "src/app/[lang]/[[...mdxPath]]/page.tsx",
"chars": 888,
"preview": "import { generateStaticParamsFor, importPage } from 'nextra/pages'\nimport { useMDXComponents } from '@/mdx-components'\n\n"
},
{
"path": "src/app/[lang]/_components/ThemeProvider.tsx",
"chars": 295,
"preview": "'use client'\n\nimport { ThemeProvider as NextThemesProvider } from 'next-themes'\nimport * as React from 'react'\n\nexport f"
},
{
"path": "src/app/[lang]/_components/ThirdPartyScripts.tsx",
"chars": 1094,
"preview": "'use client'\n\nimport { useEffect } from 'react'\n\nconst GA_ID = 'G-VCR6017LB8'\nconst BAIDU_SRC = 'https://hm.baidu.com/hm"
},
{
"path": "src/app/[lang]/layout.tsx",
"chars": 4593,
"preview": "import type { Metadata } from 'next'\n\n\nimport type { I18nLangAsyncProps, I18nLangKeys } from '@/i18n'\nimport ThirdPartyS"
},
{
"path": "src/app/[lang]/not-found.ts",
"chars": 60,
"preview": "export { NotFoundPage as default } from 'nextra-theme-docs'\n"
},
{
"path": "src/app/[lang]/styles/index.css",
"chars": 6054,
"preview": "@import 'tailwindcss';\n@import 'nextra-theme-docs/style.css';\n\n@plugin 'tailwindcss-animate';\n@plugin \"@iconify/tailwind"
},
{
"path": "src/app/[lang]/styles/overrides.css",
"chars": 315,
"preview": "body {\n font-family: 'sans-serif', 'Arial', 'Microsoft Yahei';\n text-rendering: optimizeLegibility;\n overflow-x: hidd"
},
{
"path": "src/app/_dictionaries/get-dictionary.ts",
"chars": 656,
"preview": "import type Zh from '@/i18n/zh'\nimport 'server-only'\n\n// We enumerate all dictionaries here for better linting and TypeS"
},
{
"path": "src/components/AIDemoLanding/EntryCard.tsx",
"chars": 3007,
"preview": "'use client'\n\nimport { ArrowUpRight } from 'lucide-react'\nimport { useLocale } from '@/hooks'\n\nexport default function E"
},
{
"path": "src/components/AIDemoLanding/index.tsx",
"chars": 19282,
"preview": "'use client'\n\nimport type { ReactNode } from 'react'\nimport { Bot, CheckCircle2, FileText, GaugeCircle, Layers, ShieldCh"
},
{
"path": "src/components/AIDemoLanding/interactions.tsx",
"chars": 11659,
"preview": "'use client'\n\nimport { useMemo, useState } from 'react'\nimport { CheckCircle2 } from 'lucide-react'\nimport type { Plan }"
},
{
"path": "src/components/CustomFooter/index.tsx",
"chars": 2235,
"preview": "import type { ReactNode } from 'react'\nimport Link from 'next/link'\nimport { Separator } from '@/components/ui/separator"
},
{
"path": "src/components/HomepageHero/Section.tsx",
"chars": 1505,
"preview": "import type { ReactNode } from 'react'\nimport { MotionWrapperFadeIn, MotionWrapperFlash } from '@/components/MotionWrapp"
},
{
"path": "src/components/HomepageHero/Setup.tsx",
"chars": 5012,
"preview": "'use client'\n\nimport clsx from 'clsx'\nimport Link from 'next/link'\nimport styles from '@/components/HomepageHero/SetupHe"
},
{
"path": "src/components/HomepageHero/SetupHero.module.css",
"chars": 3067,
"preview": "@reference \"tailwindcss\";\n\n.container {\n position: relative;\n}\n\n.glowA {\n position: absolute;\n top: 1.5rem;\n left: 8"
},
{
"path": "src/components/HomepageHero/index.tsx",
"chars": 9318,
"preview": "'use client'\n\nimport { useMemo } from 'react'\nimport Marquee from 'react-fast-marquee'\nimport EntryCard from '@/componen"
},
{
"path": "src/components/MotionWrapper/FadeIn.tsx",
"chars": 1126,
"preview": "'use client'\n\nimport type { Variants } from 'framer-motion'\nimport { motion, useInView } from 'framer-motion'\nimport { m"
},
{
"path": "src/components/MotionWrapper/Flash.tsx",
"chars": 1167,
"preview": "'use client'\n\nimport type { ReactNode } from 'react'\nimport { motion } from 'framer-motion'\nimport React from 'react'\n\ni"
},
{
"path": "src/components/MotionWrapper/index.ts",
"chars": 49,
"preview": "export * from './FadeIn'\nexport * from './Flash'\n"
},
{
"path": "src/components/PanelParticles/index.tsx",
"chars": 1965,
"preview": "'use client'\n\nimport type { ISourceOptions } from '@tsparticles/engine'\nimport Particles, { initParticlesEngine } from '"
},
{
"path": "src/components/ScrollProgressBar/index.tsx",
"chars": 5142,
"preview": "'use client'\n\nimport { usePathname } from 'next/navigation'\nimport { useCallback, useEffect, useRef, useState } from 're"
},
{
"path": "src/components/ThemeSwitcher/index.tsx",
"chars": 1198,
"preview": "'use client'\n\nimport { Moon, Sun } from 'lucide-react'\nimport { useTheme } from 'nextra-theme-docs'\nimport { Button } fr"
},
{
"path": "src/components/TitleBadge/index.tsx",
"chars": 765,
"preview": "'use client'\nimport type { ReactNode } from 'react'\nimport clsx from 'clsx'\nimport { motion } from 'framer-motion'\n\ninte"
},
{
"path": "src/components/auth/login-form.client.tsx",
"chars": 211,
"preview": "'use client'\n\nimport dynamic from 'next/dynamic'\n\nconst LoginForm = dynamic(() => import('@/components/auth/login-form')"
},
{
"path": "src/components/auth/login-form.tsx",
"chars": 5899,
"preview": "'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimpor"
},
{
"path": "src/components/ui/accordion.tsx",
"chars": 2259,
"preview": "'use client'\n\nimport * as AccordionPrimitive from '@radix-ui/react-accordion'\n\nimport { ChevronDown } from 'lucide-react"
},
{
"path": "src/components/ui/alert.tsx",
"chars": 1614,
"preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
},
{
"path": "src/components/ui/button.tsx",
"chars": 2392,
"preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"r"
},
{
"path": "src/components/ui/card-hover-effect.tsx",
"chars": 3382,
"preview": "'use client'\n\nimport type { ReactNode } from 'react'\nimport { AnimatePresence, motion } from 'framer-motion'\nimport { us"
},
{
"path": "src/components/ui/card.tsx",
"chars": 1987,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Card({ className, ...props }: React.Component"
},
{
"path": "src/components/ui/flip-words.tsx",
"chars": 2401,
"preview": "'use client'\n\nimport type { TargetAndTransition } from 'framer-motion'\nimport { AnimatePresence, motion } from 'framer-m"
},
{
"path": "src/components/ui/hover-card.tsx",
"chars": 1251,
"preview": "'use client'\n\nimport * as HoverCardPrimitive from '@radix-ui/react-hover-card'\nimport * as React from 'react'\n\nimport { "
},
{
"path": "src/components/ui/input.tsx",
"chars": 962,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.Co"
},
{
"path": "src/components/ui/label.tsx",
"chars": 606,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { Label as LabelPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/l"
},
{
"path": "src/components/ui/link-preview.tsx",
"chars": 4151,
"preview": "'use client'\nimport * as HoverCardPrimitive from '@radix-ui/react-hover-card'\nimport {\n AnimatePresence,\n motion,\n us"
},
{
"path": "src/components/ui/loader.tsx",
"chars": 5852,
"preview": "\"use client\";\nimport { motion } from \"motion/react\";\nimport React from \"react\";\n\nexport const LoaderOne = () => {\n cons"
},
{
"path": "src/components/ui/separator.tsx",
"chars": 807,
"preview": "'use client'\n\n\nimport * as SeparatorPrimitive from '@radix-ui/react-separator'\nimport * as React from 'react'\n\nimport { "
},
{
"path": "src/components/ui/sonner.tsx",
"chars": 1065,
"preview": "\"use client\"\n\nimport type React from \"react\"\nimport {\n CircleCheckIcon,\n InfoIcon,\n Loader2Icon,\n OctagonXIcon,\n Tr"
},
{
"path": "src/components/ui/toggle.tsx",
"chars": 1538,
"preview": "'use client'\n\nimport type { VariantProps } from 'class-variance-authority'\nimport * as TogglePrimitive from '@radix-ui/r"
},
{
"path": "src/content/en/_meta.tsx",
"chars": 1057,
"preview": "import type { MetaRecord } from 'nextra'\nimport { TitleBadge } from '@/components/TitleBadge'\n\nexport default {\n index:"
},
{
"path": "src/content/en/ai-demo.mdx",
"chars": 290,
"preview": "---\ntitle: \"PulseOps AI Workflow Assistant | Landing Page Demo\"\ndescription: \"PulseOps landing page demo for an AI workf"
},
{
"path": "src/content/en/docs/_meta.tsx",
"chars": 91,
"preview": "import type { MetaRecord } from 'nextra'\n\nexport default {\n // ...\n} satisfies MetaRecord\n"
},
{
"path": "src/content/en/docs/examples/test-tailwind.mdx",
"chars": 4912,
"preview": "# Tailwind CSS Example\n\n## Card Component\n\nHere's an example of a classic card component. It uses Tailwind CSS's utility"
},
{
"path": "src/content/en/docs/examples/theme-update.mdx",
"chars": 91,
"preview": "# Dark Mode\n\nimport { ThemeSwitcher } from '@/components/ThemeSwitcher'\n\n<ThemeSwitcher />\n"
},
{
"path": "src/content/en/docs/i18n.mdx",
"chars": 6044,
"preview": "import { FileTree } from 'nextra/components'\n\n# i18n Support\n\nThis project provides two approaches for internationalizat"
},
{
"path": "src/content/en/docs/index.mdx",
"chars": 70,
"preview": "---\ntitle: \"Test Overview\"\n---\n\n<h1>This is the overview content</h1>\n"
},
{
"path": "src/content/en/index.mdx",
"chars": 101,
"preview": "---\ntitle: \"Home Page\"\n---\n\n\nimport HomepageHero from \"@/components/HomepageHero\";\n\n<HomepageHero />\n"
},
{
"path": "src/content/en/introduction.mdx",
"chars": 923,
"preview": "---\ntitle: \"Introduction\"\n---\n\nimport { Button } from \"@/components/ui/button\"\nimport { Alert, AlertDescription, AlertTi"
},
{
"path": "src/content/en/login.mdx",
"chars": 139,
"preview": "---\ntitle: Login\n---\n\nimport LoginForm from '@/components/auth/login-form.client'\n\n<div data-pagefind-ignore=\"all\">\n <L"
},
{
"path": "src/content/en/upgrade.mdx",
"chars": 3988,
"preview": "---\ntitle: \"What's New\"\n---\n\nimport { Callout } from 'nextra/components'\n\n# 🔥 Major Framework Updates\n\nWe've completed s"
},
{
"path": "src/content/zh/_meta.tsx",
"chars": 1003,
"preview": "import type { MetaRecord } from 'nextra'\nimport { TitleBadge } from '@/components/TitleBadge'\n\nexport default {\n index:"
},
{
"path": "src/content/zh/ai-demo.mdx",
"chars": 184,
"preview": "---\ntitle: \"PulseOps AI 自动化工作流助手|落地页示例\"\ndescription: \"PulseOps 落地页示例,展示 AI 自动化工作流助手在中小团队中的产品定位、功能与转化结构。\"\n---\n\nimport AID"
},
{
"path": "src/content/zh/docs/_meta.tsx",
"chars": 91,
"preview": "import type { MetaRecord } from 'nextra'\n\nexport default {\n // ...\n} satisfies MetaRecord\n"
},
{
"path": "src/content/zh/docs/examples/test-tailwind.mdx",
"chars": 3524,
"preview": "# Tailwind CSS 示例\n\n## 卡片组件\n\n在这里,我们展示了一个经典的卡片组件。它使用了 Tailwind CSS 的工具类来快速构建响应式布局,包含了阴影、圆角和一些内边距等样式,使其看起来更具层次感和现代感。\n\n<div "
},
{
"path": "src/content/zh/docs/examples/theme-update.mdx",
"chars": 86,
"preview": "# 暗黑模式\n\nimport { ThemeSwitcher } from '@/components/ThemeSwitcher'\n\n<ThemeSwitcher />\n"
},
{
"path": "src/content/zh/docs/i18n.mdx",
"chars": 4246,
"preview": "import { FileTree } from 'nextra/components'\n\n# 国际化支持 (i18n)\n\n本项目提供了两种国际化实现方式:\n1. Nextra 内置的文件夹结构国际化\n2. 自定义 i18n 实现,用于组件"
},
{
"path": "src/content/zh/docs/index.mdx",
"chars": 39,
"preview": "---\ntitle: \"测试概览\"\n---\n\n<h1>这是概览内容</h1>\n"
},
{
"path": "src/content/zh/index.mdx",
"chars": 94,
"preview": "---\ntitle: \"首页\"\n---\n\n\nimport HomepageHero from \"@/components/HomepageHero\";\n\n<HomepageHero />\n"
},
{
"path": "src/content/zh/introduction.mdx",
"chars": 829,
"preview": "---\ntitle: \"介绍\"\n---\n\nimport { Button } from \"@/components/ui/button\"\nimport { Alert, AlertDescription, AlertTitle } from"
},
{
"path": "src/content/zh/login.mdx",
"chars": 136,
"preview": "---\ntitle: 登录\n---\n\nimport LoginForm from '@/components/auth/login-form.client'\n\n<div data-pagefind-ignore=\"all\">\n <Logi"
},
{
"path": "src/content/zh/upgrade.mdx",
"chars": 1960,
"preview": "---\ntitle: \"新变化\"\n---\n\nimport { Callout } from 'nextra/components'\n\n# 🔥 重大框架升级\n\n本次完成了核心框架的重大升级,为您带来更好的开发体验!\n\n<Callout emo"
},
{
"path": "src/hooks/index.ts",
"chars": 94,
"preview": "export * from './useBreakpoint'\nexport * from './useLocale'\nexport * from './useServerLocale'\n"
},
{
"path": "src/hooks/useBreakpoint.ts",
"chars": 712,
"preview": "'use client'\n\nimport { useMediaQuery } from 'react-responsive'\n\nexport const useBreakpoint = () => {\n // sm 640px @medi"
},
{
"path": "src/hooks/useLocale.ts",
"chars": 1063,
"preview": "'use client'\n\nimport type { AllLocales, I18nLangKeys, LocaleKeys, PathValue } from '@/i18n'\nimport { useParams } from 'n"
},
{
"path": "src/hooks/useServerLocale.ts",
"chars": 879,
"preview": "import type { AllLocales, I18nLangKeys, LocaleKeys, PathValue } from '@/i18n'\nimport { getNestedValue, i18nConfig, inter"
},
{
"path": "src/i18n/ai-demo.ts",
"chars": 1702,
"preview": "import type { I18nLangKeys } from './index'\nimport { i18nConfig } from './index'\n\ntype HeroCopy = {\n title: string\n su"
},
{
"path": "src/i18n/en.ts",
"chars": 12149,
"preview": "export default {\n systemTitle: '🚀 My Nextra Starter',\n banner: {\n title: '👋 Hey there! Welcome to the Next.js Start"
},
{
"path": "src/i18n/index.ts",
"chars": 1568,
"preview": "import en from './en'\nimport zh from './zh'\n\nexport const i18nConfig = Object.freeze({\n en,\n zh,\n})\n\nexport type I18nL"
},
{
"path": "src/i18n/zh.ts",
"chars": 7666,
"preview": "export default {\n systemTitle: '🚀 Nextra 启动模板',\n banner: {\n title: '👋 嘿,欢迎来到 Next.js 起步模板!',\n more: '了解详情',\n },"
},
{
"path": "src/lib/utils.ts",
"chars": 188,
"preview": "import type { ClassValue } from 'clsx'\nimport { clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport func"
},
{
"path": "src/mdx-components.ts",
"chars": 323,
"preview": "import { useMDXComponents as getDocsMDXComponents } from 'nextra-theme-docs'\nimport { Pre, withIcons } from 'nextra/comp"
},
{
"path": "src/proxy.ts",
"chars": 525,
"preview": "export { proxy } from 'nextra/locales'\n\nexport const config = {\n matcher: [\n /*\n * Match all request paths excep"
},
{
"path": "src/widgets/auth-button.tsx",
"chars": 2886,
"preview": "'use client'\n\nimport Link from 'next/link'\nimport { useEffect, useState } from 'react'\nimport { Button } from '@/compone"
},
{
"path": "src/widgets/locale-toggle.tsx",
"chars": 2405,
"preview": "'use client'\n\nimport clsx from 'clsx'\nimport { addBasePath } from 'next/dist/client/add-base-path'\nimport { usePathname,"
},
{
"path": "src/widgets/mobile-menu-auth.tsx",
"chars": 1381,
"preview": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport AuthButton fro"
},
{
"path": "src/widgets/navbar-extras.tsx",
"chars": 434,
"preview": "'use client'\n\nimport AuthButton from '@/widgets/auth-button'\nimport LocaleToggle from '@/widgets/locale-toggle'\nimport M"
},
{
"path": "src/widgets/theme-toggle.tsx",
"chars": 755,
"preview": "'use client'\n\nimport clsx from 'clsx'\nimport { useTheme } from 'nextra-theme-docs'\nimport { useCallback } from 'react'\ni"
},
{
"path": "tsconfig.json",
"chars": 784,
"preview": "{\n \"compilerOptions\": {\n \"incremental\": true,\n \"target\": \"ES2017\",\n \"jsx\": \"react-jsx\",\n \"lib\": [\n \"do"
}
]
About this extraction
This page contains the full source code of the pdsuwwz/nextjs-nextra-starter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 92 files (205.4 KB), approximately 61.9k tokens, and a symbol index with 101 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.