init
This commit is contained in:
42
.env.example
Normal file
42
.env.example
Normal file
@@ -0,0 +1,42 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Site info
|
||||
# -----------------------------------------------------------------------------
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_LOCALE_DETECTION=false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Discord
|
||||
# -----------------------------------------------------------------------------
|
||||
NEXT_PUBLIC_DISCORD_INVITE_URL="https://discord.com/invite/R7bUxWKRqZ"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Analytics
|
||||
# Google Analytics: https://analytics.google.com/analytics/web/
|
||||
# Baidu Tongji: https://tongji.baidu.com/
|
||||
# Plausible: https://plausible.io/
|
||||
# -----------------------------------------------------------------------------
|
||||
NEXT_PUBLIC_GOOGLE_ID=
|
||||
NEXT_PUBLIC_BAIDU_TONGJI=
|
||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=
|
||||
NEXT_PUBLIC_PLAUSIBLE_SRC=
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# Ads
|
||||
# Google Adsense: https://www.google.com/adsense/
|
||||
#------------------------------------------------------------------------
|
||||
NEXT_PUBLIC_GOOGLE_ADSENSE_ID=
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# Resend: https://resend.com/
|
||||
#------------------------------------------------------------------------
|
||||
RESEND_API_KEY=
|
||||
ADMIN_EMAIL=
|
||||
RESEND_AUDIENCE_ID=
|
||||
|
||||
#------------------------------------------------------------------------
|
||||
# Upstash: https://upstash.com/
|
||||
#------------------------------------------------------------------------
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
UPSTASH_REDIS_NEWSLETTER_RATE_LIMIT_KEY=newsletter-rate-limit
|
||||
DAY_MAX_SUBMISSIONS=
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
133
.gitignore
vendored
Normal file
133
.gitignore
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
### Node template
|
||||
.idea
|
||||
.DS_Store
|
||||
dist
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.temp
|
||||
yarn.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.local
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the assets line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# assets
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
/.vuepress/dist/
|
||||
|
||||
# sitemap
|
||||
*/sitemap*.xml
|
||||
*/robots.txt
|
||||
4
.npmrc
Normal file
4
.npmrc
Normal file
@@ -0,0 +1,4 @@
|
||||
# if use pnpm
|
||||
enable-pre-post-scripts=true
|
||||
public-hoist-pattern[]=*@nextui-org/*
|
||||
registry=https://registry.npmmirror.com/
|
||||
18
.vscode/settings.json
vendored
Normal file
18
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"css.validate": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"headwind.runOnSave": false,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"commentTranslate.source": "Bing",
|
||||
"cSpell.words": [
|
||||
"contentlayer",
|
||||
"lemonsqueezy"
|
||||
]
|
||||
}
|
||||
293
README.md
Normal file
293
README.md
Normal file
@@ -0,0 +1,293 @@
|
||||
---
|
||||
Ship your SaaS faster with Nexty
|
||||
|
||||
Nexty.dev is a comprehensive Next.js SaaS boilerplate that provides everything developers need to rapidly build and launch modern AI web applications
|
||||
|
||||
Try [Nexty.dev today](https://nexty.dev?utm_source=github-nextjs-starter)
|
||||
---
|
||||
|
||||
[<img src="/public/try-nexty.webp">](https://nexty.dev?utm_source=github-nextjs-starter)
|
||||
|
||||
|
||||
🌍 *[English](README.md) ∙ [简体中文](README_zh.md) ∙ [日本語](README_ja.md)*
|
||||
|
||||
# Next Forge - Multilingual Next.js 16 Starter
|
||||
|
||||
A feature-rich Next.js 16 multilingual starter template to help you quickly build globally-ready websites.
|
||||
|
||||
- [👉 Source Code](https://github.com/weijunext/nextjs-starter)
|
||||
- [👉 Live Demo](https://nextforge.dev/)
|
||||
|
||||
**🚀 Looking for a full-featured SaaS Starter Template? [Check out the complete version](https://nexty.dev)**
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🌐 Built-in i18n support (English, Chinese, Japanese)
|
||||
- 🎨 Modern UI design with Tailwind CSS
|
||||
- 🌙 Dark/Light theme toggle
|
||||
- 📱 Responsive layout
|
||||
- 📝 MDX blog system
|
||||
- 🔍 SEO optimization
|
||||
- 📊 Integrated analytics tools
|
||||
- Google Analytics
|
||||
- Baidu Analytics
|
||||
- Google Adsense
|
||||
- Vercel Analytics
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20.9 or higher
|
||||
- pnpm 9.0 or higher (recommended)
|
||||
|
||||
> **Note**: The project has configured `packageManager` field, we recommend using pnpm for the best experience.
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/weijunext/nextjs-starter.git
|
||||
cd nextjs-starter
|
||||
```
|
||||
|
||||
2. Enable Corepack (recommended):
|
||||
```bash
|
||||
corepack enable
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
# or use other package managers
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
|
||||
4. Copy environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
5. Start the development server:
|
||||
```bash
|
||||
pnpm dev
|
||||
# or npm run dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000 to view your application.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
1. Basic Setup
|
||||
- Edit `config/site.ts` for website information
|
||||
- Update icons and logo in `public/`
|
||||
- Configure `app/sitemap.ts` for sitemap
|
||||
- Update `app/robots.ts` for robots.txt
|
||||
|
||||
2. i18n Setup
|
||||
- Add/modify language files in `i18n/messages/`
|
||||
- Configure supported languages in `i18n/routing.ts`
|
||||
- Set up i18n routing in `middleware.ts`
|
||||
- Create pages under `app/[locale]/`
|
||||
- Use the `Link` component from `i18n/routing.ts` instead of Next.js default
|
||||
|
||||
## 📝 Content Management
|
||||
|
||||
### Blog Posts
|
||||
Create MDX files in `blog/[locale]` with the following format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Post Title
|
||||
description: Post Description
|
||||
image: /image.png
|
||||
slug: /url-path
|
||||
tags: tag1,tag2
|
||||
date: 2025-02-20
|
||||
visible: published
|
||||
pin: true
|
||||
---
|
||||
|
||||
Post content...
|
||||
```
|
||||
|
||||
Reference `types/blog.ts` for supported fields.
|
||||
|
||||
### Static Pages
|
||||
Manage static page content in `content/[page]/[locale].mdx`.
|
||||
|
||||
## 🔍 SEO Optimization
|
||||
|
||||
Built-in comprehensive SEO features:
|
||||
- Server-side rendering and static generation
|
||||
- Automatic sitemap.xml generation
|
||||
- robots.txt configuration
|
||||
- Optimized metadata
|
||||
- Open Graph support
|
||||
- Multilingual SEO support
|
||||
|
||||
## 📊 Analytics
|
||||
|
||||
Enable analytics by adding IDs in `.env`:
|
||||
```
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS=
|
||||
NEXT_PUBLIC_BAIDU_TONGJI=
|
||||
NEXT_PUBLIC_GOOGLE_ADSENSE=
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
nextjs-starter/
|
||||
├── app/ # App directory
|
||||
│ ├── [locale]/ # Internationalized routes
|
||||
│ │ ├── about/ # About page
|
||||
│ │ ├── blog/ # Blog pages
|
||||
│ │ └── ... # Other pages
|
||||
│ ├── api/ # API routes
|
||||
│ └── globals/ # Global components
|
||||
├── blog/ # Blog content (MDX)
|
||||
│ ├── en/ # English blog
|
||||
│ ├── ja/ # Japanese blog
|
||||
│ └── zh/ # Chinese blog
|
||||
├── components/ # Reusable components
|
||||
│ ├── ui/ # Base UI components
|
||||
│ ├── header/ # Header components
|
||||
│ ├── footer/ # Footer components
|
||||
│ └── ... # Other components
|
||||
├── config/ # Configuration files
|
||||
├── content/ # Static content (MDX)
|
||||
├── i18n/ # Internationalization
|
||||
│ ├── messages/ # Translation files
|
||||
│ ├── routing.ts # Routing configuration
|
||||
│ └── request.ts # Request configuration
|
||||
├── lib/ # Utility functions
|
||||
├── public/ # Static assets
|
||||
└── types/ # Type definitions
|
||||
```
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Framework**: Next.js 16 (App Router)
|
||||
- **Language**: TypeScript
|
||||
- **Styling**: Tailwind CSS + Shadcn/ui
|
||||
- **Internationalization**: next-intl
|
||||
- **Content**: MDX
|
||||
- **State Management**: Zustand
|
||||
- **Deployment**: Vercel
|
||||
- **Package Manager**: pnpm (recommended)
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### One-Click Deploy
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/weijunext/nextjs-starter&project-name=&repository-name=nextjs-starter&demo-title=NextjsStarter&demo-description=Nextjs%2015%20starter.&demo-url=https://nextforge.dev&demo-image=https://nextforge.dev/og.png)
|
||||
|
||||
### Manual Deployment to Vercel
|
||||
|
||||
1. Push your code to GitHub
|
||||
2. Import project in Vercel
|
||||
3. Configure environment variables
|
||||
4. Deploy
|
||||
|
||||
### Other Platforms
|
||||
|
||||
```bash
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 💡 Development Best Practices
|
||||
|
||||
### Package Manager
|
||||
|
||||
- Project configured with `packageManager: "pnpm@10.12.4"`
|
||||
- Enable Corepack: `corepack enable`
|
||||
- Team members should use the same pnpm version
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint code
|
||||
pnpm lint
|
||||
|
||||
# Type checking
|
||||
pnpm type-check
|
||||
```
|
||||
|
||||
### Internationalization Development
|
||||
|
||||
1. Adding new language support:
|
||||
- Add new language files in `i18n/messages/`
|
||||
- Update `i18n/routing.ts` configuration
|
||||
- Create corresponding language directories in `blog/` and `content/`
|
||||
|
||||
2. Using translations:
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function MyComponent() {
|
||||
const t = useTranslations('namespace');
|
||||
return <h1>{t('title')}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Package manager version mismatch**
|
||||
```bash
|
||||
# Remove node_modules and lockfile
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
# Reinstall
|
||||
pnpm install
|
||||
```
|
||||
|
||||
**2. MDX files not displaying**
|
||||
- Check file path is correct
|
||||
- Verify frontmatter format
|
||||
- Ensure `visible` field is set to `published`
|
||||
|
||||
**3. Internationalization routing issues**
|
||||
- Use `Link` component from `i18n/routing.ts`
|
||||
- Check `middleware.ts` configuration
|
||||
|
||||
**4. Styles not working**
|
||||
- Verify Tailwind CSS class names are correct
|
||||
- Try restarting development server
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Ensure `.env` file contains necessary configuration:
|
||||
```bash
|
||||
# Copy example config
|
||||
cp .env.example .env
|
||||
# Modify as needed
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Issues and Pull Requests are welcome!
|
||||
|
||||
## About the Author
|
||||
|
||||
Next.js full-stack specialist providing expert services in project development, performance optimization, and SEO improvement.
|
||||
|
||||
For consulting and training opportunities, reach out at weijunext@gmail.com
|
||||
|
||||
- [Github](https://github.com/weijunext)
|
||||
- [Bento](https://bento.me/weijunext)
|
||||
- [Twitter/X](https://twitter.com/judewei_dev)
|
||||
|
||||
<a href="https://www.buymeacoffee.com/weijunext" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
[](https://ko-fi.com/G2G6TWWMG)
|
||||
290
README_ja.md
Normal file
290
README_ja.md
Normal file
@@ -0,0 +1,290 @@
|
||||
---
|
||||
Nexty で SaaS を迅速に立ち上げよう - ビジュアル料金ダッシュボード、AI プレイグラウンド、エンタープライズレベル CMS を備えた唯一の Next.js ボイラープレート。多言語対応、認証、決済、メール機能、SEO 最適化も完備。
|
||||
|
||||
[Nexty.dev を今すぐ試す](https://nexty.dev?utm_source=github-nextjs-starter)
|
||||
---
|
||||
|
||||
[<img src="/public/try-nexty.webp">](https://nexty.dev?utm_source=github-nextjs-starter)
|
||||
|
||||
🌍 *[English](README.md) ∙ [简体中文](README_zh.md) ∙ [日本語](README_ja.md)*
|
||||
|
||||
# Next Forge - 多言語対応 Next.js 16 スターター
|
||||
|
||||
グローバル対応のウェブサイトを素早く構築するための、機能豊富なNext.js 15多言語スターターテンプレートです。
|
||||
|
||||
- [👉 ソースコード](https://github.com/weijunext/nextjs-starter)
|
||||
- [👉 デモサイト](https://nextforge.dev/)
|
||||
|
||||
**🚀 多機能で使いやすいフルスタックの起動テンプレートをお探しですか? ぜひ、当社の[アドバンス版](https://nexty.dev)をお試しください。**
|
||||
|
||||
## ✨ 主な機能
|
||||
|
||||
- 🌐 多言語対応(英語・中国語・日本語)
|
||||
- 🎨 Tailwind CSSによるモダンなUI
|
||||
- 🌙 ダーク/ライトテーマ切り替え
|
||||
- 📱 レスポンシブデザイン
|
||||
- 📝 MDXブログシステム
|
||||
- 🔍 SEO最適化
|
||||
- 📊 アナリティクスツール統合
|
||||
- Google Analytics
|
||||
- Baidu Analytics
|
||||
- Google Adsense
|
||||
- Vercel Analytics
|
||||
|
||||
## 🚀 クイックスタート
|
||||
|
||||
### 必要な環境
|
||||
|
||||
- Node.js 20.9 以上
|
||||
- pnpm 9.0 以上(推奨)
|
||||
|
||||
> **注意**: プロジェクトには `packageManager` フィールドが設定されており、最適な体験のために pnpm の使用を推奨しています。
|
||||
|
||||
### インストール手順
|
||||
|
||||
1. リポジトリのクローン:
|
||||
```bash
|
||||
git clone https://github.com/weijunext/nextjs-starter.git
|
||||
cd nextjs-starter
|
||||
```
|
||||
|
||||
2. Corepack の有効化(推奨):
|
||||
```bash
|
||||
corepack enable
|
||||
```
|
||||
|
||||
3. 依存関係のインストール:
|
||||
```bash
|
||||
pnpm install
|
||||
# または他のパッケージマネージャーを使用
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
|
||||
4. 環境変数の設定:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
5. 開発サーバーの起動:
|
||||
```bash
|
||||
pnpm dev
|
||||
# または npm run dev
|
||||
```
|
||||
|
||||
http://localhost:3000 にアクセスして確認できます。
|
||||
|
||||
## ⚙️ 設定方法
|
||||
|
||||
1. 基本設定
|
||||
- `config/site.ts`でウェブサイト情報を編集
|
||||
- `public/`内のアイコンとロゴを更新
|
||||
- `app/sitemap.ts`でサイトマップを設定
|
||||
- `app/robots.ts`でrobots.txtを更新
|
||||
|
||||
2. 多言語設定
|
||||
- `i18n/messages/`内の言語ファイルを追加/編集
|
||||
- `i18n/routing.ts`でサポートする言語を設定
|
||||
- `middleware.ts`で多言語ルーティングを設定
|
||||
- `app/[locale]/`配下にページを作成
|
||||
- Next.jsデフォルトの代わりに`i18n/routing.ts`の`Link`コンポーネントを使用
|
||||
|
||||
## 📝 コンテンツ管理
|
||||
|
||||
### ブログ投稿
|
||||
`blog/[locale]`にMDXファイルを以下のフォーマットで作成:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: 投稿タイトル
|
||||
description: 投稿の説明
|
||||
image: /image.png
|
||||
slug: /url-path
|
||||
tags: tag1,tag2
|
||||
date: 2025-02-20
|
||||
visible: published
|
||||
pin: true
|
||||
---
|
||||
|
||||
投稿内容...
|
||||
```
|
||||
|
||||
対応フィールドについては`types/blog.ts`を参照してください。
|
||||
|
||||
### 静的ページ
|
||||
`content/[page]/[locale].mdx`で静的ページのコンテンツを管理します。
|
||||
|
||||
## 🔍 SEO最適化
|
||||
|
||||
包括的なSEO機能を搭載:
|
||||
- サーバーサイドレンダリングと静的生成
|
||||
- sitemap.xml自動生成
|
||||
- robots.txt設定
|
||||
- 最適化されたメタデータ
|
||||
- OGP対応
|
||||
- 多言語SEOサポート
|
||||
|
||||
## 📊 アナリティクス
|
||||
|
||||
`.env`にIDを追加して有効化:
|
||||
```
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS=
|
||||
NEXT_PUBLIC_BAIDU_TONGJI=
|
||||
NEXT_PUBLIC_GOOGLE_ADSENSE=
|
||||
```
|
||||
|
||||
## 📁 プロジェクト構成
|
||||
|
||||
```
|
||||
nextjs-starter/
|
||||
├── app/ # アプリディレクトリ
|
||||
│ ├── [locale]/ # 多言語ルート
|
||||
│ │ ├── about/ # Aboutページ
|
||||
│ │ ├── blog/ # ブログページ
|
||||
│ │ └── ... # その他のページ
|
||||
│ ├── api/ # APIルート
|
||||
│ └── globals/ # グローバルコンポーネント
|
||||
├── blog/ # ブログコンテンツ(MDX)
|
||||
│ ├── en/ # 英語ブログ
|
||||
│ ├── ja/ # 日本語ブログ
|
||||
│ └── zh/ # 中国語ブログ
|
||||
├── components/ # 再利用可能なコンポーネント
|
||||
│ ├── ui/ # ベースUIコンポーネント
|
||||
│ ├── header/ # ヘッダーコンポーネント
|
||||
│ ├── footer/ # フッターコンポーネント
|
||||
│ └── ... # その他のコンポーネント
|
||||
├── config/ # 設定ファイル
|
||||
├── content/ # 静的コンテンツ(MDX)
|
||||
├── i18n/ # 国際化設定
|
||||
│ ├── messages/ # 翻訳ファイル
|
||||
│ ├── routing.ts # ルーティング設定
|
||||
│ └── request.ts # リクエスト設定
|
||||
├── lib/ # ユーティリティ関数
|
||||
├── public/ # 静的アセット
|
||||
└── types/ # 型定義
|
||||
```
|
||||
|
||||
## 🛠️ 技術スタック
|
||||
|
||||
- **フレームワーク**: Next.js 16 (App Router)
|
||||
- **言語**: TypeScript
|
||||
- **スタイリング**: Tailwind CSS + Shadcn/ui
|
||||
- **国際化**: next-intl
|
||||
- **コンテンツ**: MDX
|
||||
- **状態管理**: Zustand
|
||||
- **デプロイ**: Vercel
|
||||
- **パッケージマネージャー**: pnpm(推奨)
|
||||
|
||||
## 🚀 デプロイ
|
||||
|
||||
### ワンクリックデプロイ
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/weijunext/nextjs-starter&project-name=&repository-name=nextjs-starter&demo-title=NextjsStarter&demo-description=Nextjs%2015%20starter.&demo-url=https://nextforge.dev&demo-image=https://nextforge.dev/og.png)
|
||||
|
||||
### Vercelへの手動デプロイ
|
||||
|
||||
1. GitHubにコードをプッシュ
|
||||
2. Vercelでプロジェクトをインポート
|
||||
3. 環境変数を設定
|
||||
4. デプロイ
|
||||
|
||||
### その他のプラットフォーム
|
||||
|
||||
```bash
|
||||
# プロダクション用ビルド
|
||||
pnpm build
|
||||
|
||||
# プロダクションサーバーを起動
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 💡 開発のベストプラクティス
|
||||
|
||||
### パッケージマネージャー使用
|
||||
|
||||
- プロジェクトは `packageManager: "pnpm@10.12.4"` で設定済み
|
||||
- Corepack を有効化: `corepack enable`
|
||||
- チームメンバーは同じ pnpm バージョンを使用すべき
|
||||
|
||||
### コード品質
|
||||
|
||||
```bash
|
||||
# コードリント
|
||||
pnpm lint
|
||||
|
||||
# 型チェック
|
||||
pnpm type-check
|
||||
```
|
||||
|
||||
### 多言語開発
|
||||
|
||||
1. 新しい言語サポートの追加:
|
||||
- `i18n/messages/` に新しい言語ファイルを追加
|
||||
- `i18n/routing.ts` 設定を更新
|
||||
- `blog/` と `content/` に対応する言語ディレクトリを作成
|
||||
|
||||
2. 翻訳の使用:
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function MyComponent() {
|
||||
const t = useTranslations('namespace');
|
||||
return <h1>{t('title')}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 トラブルシューティング
|
||||
|
||||
### よくある問題
|
||||
|
||||
**1. パッケージマネージャーのバージョン不一致**
|
||||
```bash
|
||||
# node_modules と lockfile を削除
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
# 再インストール
|
||||
pnpm install
|
||||
```
|
||||
|
||||
**2. MDXファイルが表示されない**
|
||||
- ファイルパスが正しいか確認
|
||||
- frontmatter のフォーマットが正しいか確認
|
||||
- `visible` フィールドが `published` に設定されているか確認
|
||||
|
||||
**3. 多言語ルーティングの問題**
|
||||
- `i18n/routing.ts` の `Link` コンポーネントを使用
|
||||
- `middleware.ts` の設定を確認
|
||||
|
||||
**4. スタイルが効かない**
|
||||
- Tailwind CSS のクラス名が正しいか確認
|
||||
- 開発サーバーの再起動を試す
|
||||
|
||||
### 環境変数の問題
|
||||
|
||||
`.env` ファイルに必要な設定が含まれていることを確認:
|
||||
```bash
|
||||
# サンプル設定をコピー
|
||||
cp .env.example .env
|
||||
# 必要に応じて設定を変更
|
||||
```
|
||||
|
||||
## 📄 ライセンス
|
||||
|
||||
MIT
|
||||
|
||||
## 🤝 コントリビューション
|
||||
|
||||
Issue、PRは大歓迎です!
|
||||
|
||||
## 作者について
|
||||
|
||||
Next.jsのフルスタックスペシャリストとして、プロジェクト開発、パフォーマンス最適化、SEO改善のエキスパートサービスを提供しています。
|
||||
|
||||
コンサルティングやトレーニングについては、 weijunext@gmail.com までご連絡ください。
|
||||
|
||||
- [Github](https://github.com/weijunext)
|
||||
- [Bento](https://bento.me/weijunext)
|
||||
- [Twitter/X](https://twitter.com/judewei_dev)
|
||||
|
||||
<a href="https://www.buymeacoffee.com/weijunext" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
[](https://ko-fi.com/G2G6TWWMG)
|
||||
297
README_zh.md
Normal file
297
README_zh.md
Normal file
@@ -0,0 +1,297 @@
|
||||
---
|
||||
使用 Nexty 加速 SaaS 开发 - 唯一一个集成可视化定价面板、AI 开发环境和企业级 CMS 的 Next.js 模板,还包含多语言支持、身份验证、支付系统、电子邮件功能和 SEO 优化。
|
||||
|
||||
立即体验 [Nexty.dev](https://nexty.dev/?utm_source=github-nextjs-starter)
|
||||
---
|
||||
|
||||
[<img src="/public/try-nexty.webp">](https://nexty.dev/?utm_source=github-nextjs-starter)
|
||||
|
||||
🌍 *[English](README.md) ∙ [简体中文](README_zh.md) ∙ [日本语](README_ja.md)*
|
||||
|
||||
# Next Forge - 多语言 Next.js 16 启动模板
|
||||
|
||||
一个轻量的 Next.js 16 多语言启动模板,帮助你快速构建面向全球的网站。
|
||||
|
||||
- [👉 源码地址](https://github.com/weijunext/nextjs-starter)
|
||||
- [👉 在线预览](https://nextforge.dev/)
|
||||
|
||||
**🚀 如果你正在寻找功能完备的全栈启动模板,请了解我们的[高级版](https://nexty.dev/?utm_source=github-nextjs-starter)**
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 🌐 内置多语言支持 (中文、英文、日语)
|
||||
- 🎨 基于 Tailwind CSS 的现代 UI 设计
|
||||
- 🌙 深色/浅色主题切换
|
||||
- 📱 响应式布局
|
||||
- 📝 MDX 博客系统
|
||||
- 🔍 SEO 优化
|
||||
- 📊 集成多个统计分析工具
|
||||
- Google Analytics
|
||||
- Baidu Analytics
|
||||
- Google Adsense
|
||||
- Vercel Analytics
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 20.9 或更高版本
|
||||
- pnpm 9.0 或更高版本(推荐)
|
||||
|
||||
> **注意**: 项目已配置 `packageManager` 字段,推荐使用 pnpm 以获得最佳体验。
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆项目:
|
||||
```bash
|
||||
git clone https://github.com/weijunext/nextjs-starter.git
|
||||
cd nextjs-starter
|
||||
```
|
||||
|
||||
2. 启用 Corepack (推荐):
|
||||
```bash
|
||||
corepack enable
|
||||
```
|
||||
|
||||
3. 安装依赖:
|
||||
```bash
|
||||
pnpm install
|
||||
# 或使用其他包管理器
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
|
||||
4. 复制环境变量文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
5. 启动开发服务器:
|
||||
```bash
|
||||
pnpm dev
|
||||
# 或 npm run dev
|
||||
```
|
||||
|
||||
访问 http://localhost:3000 查看你的应用。
|
||||
|
||||
## ⚙️ 配置
|
||||
|
||||
1. 基础配置
|
||||
- 修改 `config/site.ts` 配置网站信息
|
||||
- 修改 `public/` 下的图标和 logo
|
||||
- 更新 `app/sitemap.ts` 配置站点地图
|
||||
- 更新 `app/robots.ts` 配置 robots.txt
|
||||
|
||||
2. 多语言配置
|
||||
- 在 `i18n/messages/` 下添加或修改语言文件
|
||||
- 在 `i18n/routing.ts` 中配置支持的语言
|
||||
- 在 `middleware.ts` 中配置多语言路由
|
||||
- 在 `app/[locale]/` 目录下创建页面
|
||||
- 多语言页面使用 `i18n/routing.ts` 导出的 `Link` 组件替代 next.js 的
|
||||
|
||||
## 📝 内容管理
|
||||
|
||||
### 博客文章
|
||||
在 `blog/[locale]` 目录下创建 MDX 文件,支持以下格式:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: 文章标题
|
||||
description: 文章描述
|
||||
image: /image.png
|
||||
slug: /url-path
|
||||
tags: tag1,tag2
|
||||
date: 2025-02-20
|
||||
visible: published
|
||||
pin: true
|
||||
---
|
||||
|
||||
文章内容...
|
||||
```
|
||||
|
||||
可参考类型定义 `types/blog.ts` 确认支持的字段。
|
||||
|
||||
### 静态页面
|
||||
在 `content/[page]/[locale].mdx` 下管理静态页面内容。
|
||||
|
||||
## 🔍 SEO 优化
|
||||
|
||||
模板内置了完整的 SEO 优化方案:
|
||||
- 服务端渲染和静态生成
|
||||
- 自动生成 sitemap.xml
|
||||
- 配置 robots.txt
|
||||
- 优化的 metadata
|
||||
- 支持 Open Graph
|
||||
- 多语言 SEO 支持
|
||||
|
||||
## 📊 统计分析
|
||||
|
||||
在 `.env` 文件中配置相应的 ID 即可启用:
|
||||
```
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS=
|
||||
NEXT_PUBLIC_BAIDU_TONGJI=
|
||||
NEXT_PUBLIC_GOOGLE_ADSENSE=
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
nextjs-starter/
|
||||
├── app/ # 应用路由目录
|
||||
│ ├── [locale]/ # 多语言路由
|
||||
│ │ ├── about/ # 关于页面
|
||||
│ │ ├── blog/ # 博客页面
|
||||
│ │ └── ... # 其他页面
|
||||
│ ├── api/ # API 路由
|
||||
│ └── globals/ # 全局组件
|
||||
├── blog/ # 博客内容 (MDX)
|
||||
│ ├── en/ # 英文博客
|
||||
│ ├── ja/ # 日文博客
|
||||
│ └── zh/ # 中文博客
|
||||
├── components/ # 可复用组件
|
||||
│ ├── ui/ # 基础 UI 组件
|
||||
│ ├── header/ # 头部组件
|
||||
│ ├── footer/ # 底部组件
|
||||
│ └── ... # 其他组件
|
||||
├── config/ # 配置文件
|
||||
├── content/ # 静态内容 (MDX)
|
||||
├── i18n/ # 国际化配置
|
||||
│ ├── messages/ # 翻译文件
|
||||
│ ├── routing.ts # 路由配置
|
||||
│ └── request.ts # 请求配置
|
||||
├── lib/ # 工具函数
|
||||
├── public/ # 静态资源
|
||||
└── types/ # 类型定义
|
||||
```
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **框架**: Next.js 16 (App Router)
|
||||
- **语言**: TypeScript
|
||||
- **样式**: Tailwind CSS + Shadcn/ui
|
||||
- **国际化**: next-intl
|
||||
- **内容**: MDX
|
||||
- **状态管理**: Zustand
|
||||
- **部署**: Vercel
|
||||
- **包管理器**: pnpm (推荐)
|
||||
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
### 一键部署
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/weijunext/nextjs-starter&project-name=&repository-name=nextjs-starter&demo-title=NextjsStarter&demo-description=Nextjs%2015%20starter.&demo-url=https://nextforge.dev&demo-image=https://nextforge.dev/og.png)
|
||||
|
||||
### 手动部署到 Vercel
|
||||
|
||||
1. 推送代码到 GitHub
|
||||
2. 在 Vercel 中导入项目
|
||||
3. 配置环境变量
|
||||
4. 部署
|
||||
|
||||
### 其他平台部署
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
|
||||
# 启动生产服务器
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 💡 开发最佳实践
|
||||
|
||||
### 包管理器使用
|
||||
|
||||
- 项目已配置 `packageManager: "pnpm@10.12.4"`
|
||||
- 建议启用 Corepack: `corepack enable`
|
||||
- 团队成员应使用相同版本的 pnpm
|
||||
|
||||
### 代码规范
|
||||
|
||||
```bash
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
|
||||
# 类型检查
|
||||
pnpm type-check
|
||||
```
|
||||
|
||||
### 多语言开发
|
||||
|
||||
1. 新增语言支持:
|
||||
- 在 `i18n/messages/` 添加新的语言文件
|
||||
- 更新 `i18n/routing.ts` 配置
|
||||
- 在 `blog/` 和 `content/` 下创建对应语言目录
|
||||
|
||||
2. 使用翻译:
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function MyComponent() {
|
||||
const t = useTranslations('namespace');
|
||||
return <h1>{t('title')}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**1. 包管理器版本不一致**
|
||||
```bash
|
||||
# 删除 node_modules 和 lockfile
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
# 重新安装
|
||||
pnpm install
|
||||
```
|
||||
|
||||
**2. MDX 文件不显示**
|
||||
- 检查文件路径是否正确
|
||||
- 确认 frontmatter 格式正确
|
||||
- 检查 `visible` 字段是否设置为 `published`
|
||||
|
||||
**3. 多语言路由问题**
|
||||
- 确保使用 `i18n/routing.ts` 中的 `Link` 组件
|
||||
- 检查 `middleware.ts` 配置
|
||||
|
||||
**4. 样式不生效**
|
||||
- 确认 Tailwind CSS 类名拼写正确
|
||||
- 检查是否需要重启开发服务器
|
||||
|
||||
### 环境变量问题
|
||||
|
||||
确保 `.env` 文件包含必要的配置:
|
||||
```bash
|
||||
# 复制示例配置
|
||||
cp .env.example .env
|
||||
# 根据需要修改配置
|
||||
```
|
||||
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 关于作者
|
||||
|
||||
专注于 Next.js 全栈开发,欢迎探讨开发、咨询与培训等合作机会,联系微信:bigye_chengpu
|
||||
|
||||
- [Github](https://github.com/weijunext)
|
||||
- [Twitter/X](https://twitter.com/weijunext)
|
||||
- [博客 - J实验室](https://weijunext.com)
|
||||
- [Medium](https://medium.com/@weijunext)
|
||||
- [掘金](https://juejin.cn/user/26044008768029)
|
||||
- [知乎](https://www.zhihu.com/people/mo-mo-mo-89-12-11)
|
||||
|
||||
<a href="https://www.buymeacoffee.com/weijunext" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
[](https://ko-fi.com/G2G6TWWMG)
|
||||
|
||||
<img src="./public/zs.jpeg" alt="赞赏作者" style="height: 200px; width: 200px">
|
||||
|
||||
|
||||
125
actions/newsletter.ts
Normal file
125
actions/newsletter.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { normalizeEmail, validateEmail } from '@/lib/email';
|
||||
import { Ratelimit } from '@upstash/ratelimit';
|
||||
import { Redis } from '@upstash/redis';
|
||||
import { headers } from 'next/headers';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
// initialize resend
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
// Resend Audience ID
|
||||
const AUDIENCE_ID = process.env.RESEND_AUDIENCE_ID!;
|
||||
|
||||
// initialize Redis
|
||||
const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_REST_URL!,
|
||||
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
||||
});
|
||||
|
||||
const REDIS_RATE_LIMIT_KEY = process.env.UPSTASH_REDIS_NEWSLETTER_RATE_LIMIT_KEY!;
|
||||
const DAY_MAX_SUBMISSIONS = parseInt(process.env.DAY_MAX_SUBMISSIONS || '10');
|
||||
|
||||
// create rate limiter
|
||||
const limiter = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(DAY_MAX_SUBMISSIONS, '1d'),
|
||||
prefix: REDIS_RATE_LIMIT_KEY,
|
||||
});
|
||||
|
||||
// Shared rate limit check
|
||||
async function checkRateLimit() {
|
||||
const headersList = await headers();
|
||||
const ip = headersList.get('x-real-ip') ||
|
||||
headersList.get('x-forwarded-for') ||
|
||||
'unknown';
|
||||
|
||||
const { success } = await limiter.limit(ip);
|
||||
if (!success) {
|
||||
throw new Error('Too many submissions, please try again later');
|
||||
}
|
||||
}
|
||||
|
||||
export async function subscribeToNewsletter(email: string) {
|
||||
try {
|
||||
await checkRateLimit();
|
||||
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
const { isValid, error } = validateEmail(normalizedEmail);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error(error || 'Invalid email address');
|
||||
}
|
||||
|
||||
// Check if already subscribed
|
||||
// const list = await resend.contacts.list({ audienceId: AUDIENCE_ID });
|
||||
// const user = list.data?.data.find((item) => item.email === normalizedEmail);
|
||||
// if (user) {
|
||||
// return { success: true, alreadySubscribed: true };
|
||||
// }
|
||||
|
||||
// Add to audience
|
||||
await resend.contacts.create({
|
||||
audienceId: AUDIENCE_ID,
|
||||
email: normalizedEmail,
|
||||
});
|
||||
|
||||
// Send welcome email
|
||||
const unsubscribeToken = Buffer.from(normalizedEmail).toString('base64');
|
||||
const unsubscribeLink = `${process.env.NEXT_PUBLIC_SITE_URL}/unsubscribe?token=${unsubscribeToken}`;
|
||||
|
||||
await resend.emails.send({
|
||||
from: 'NextForge <' + process.env.ADMIN_EMAIL! + '>',
|
||||
to: normalizedEmail,
|
||||
subject: 'Welcome to Next Forge',
|
||||
html: `
|
||||
<h2>Welcome to Next Forge</h2>
|
||||
<p>Thank you for subscribing to the newsletter. You will receive the latest updates and news.</p>
|
||||
<p style="margin-top: 20px; font-size: 12px; color: #666;">
|
||||
If you wish to unsubscribe, please <a href="${unsubscribeLink}">click here</a>
|
||||
</p>
|
||||
`,
|
||||
headers: {
|
||||
"List-Unsubscribe": `<${unsubscribeLink}>`,
|
||||
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Newsletter subscription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unsubscribeFromNewsletter(token: string) {
|
||||
try {
|
||||
await checkRateLimit();
|
||||
|
||||
const email = Buffer.from(token, 'base64').toString();
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
const { isValid, error } = validateEmail(normalizedEmail);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error(error || 'Invalid email address');
|
||||
}
|
||||
|
||||
// Check if subscribed
|
||||
const list = await resend.contacts.list({ audienceId: AUDIENCE_ID });
|
||||
const user = list.data?.data.find((item) => item.email === normalizedEmail);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('This email is not subscribed to our notifications');
|
||||
}
|
||||
|
||||
// Remove from audience
|
||||
await resend.contacts.remove({
|
||||
audienceId: AUDIENCE_ID,
|
||||
email: normalizedEmail,
|
||||
});
|
||||
|
||||
return { success: true, email: normalizedEmail };
|
||||
} catch (error) {
|
||||
console.error('Newsletter unsubscribe failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
33
app/BaiDuAnalytics.tsx
Normal file
33
app/BaiDuAnalytics.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
const BaiDuAnalytics = () => {
|
||||
return (
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_BAIDU_TONGJI ? (
|
||||
<>
|
||||
<Script
|
||||
id="baidu-tongji"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?${process.env.NEXT_PUBLIC_BAIDU_TONGJI}";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaiDuAnalytics;
|
||||
24
app/GoogleAdsense.tsx
Normal file
24
app/GoogleAdsense.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
const GoogleAdsense = () => {
|
||||
return (
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_ID ? (
|
||||
<>
|
||||
<Script
|
||||
async
|
||||
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-${process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_ID}`}
|
||||
crossOrigin="anonymous"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleAdsense;
|
||||
37
app/GoogleAnalytics.tsx
Normal file
37
app/GoogleAnalytics.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
import * as gtag from "../gtag.js";
|
||||
|
||||
const GoogleAnalytics = () => {
|
||||
return (
|
||||
<>
|
||||
{gtag.GA_TRACKING_ID ? (
|
||||
<>
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
|
||||
/>
|
||||
<Script
|
||||
id="gtag-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gtag.GA_TRACKING_ID}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleAnalytics;
|
||||
39
app/PlausibleAnalytics.tsx
Normal file
39
app/PlausibleAnalytics.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import Script from "next/script";
|
||||
|
||||
// 你可以将域名放在环境变量中,这里先直接使用你提供的域名
|
||||
const PLAUSIBLE_DOMAIN = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN;
|
||||
const PLAUSIBLE_SRC = process.env.NEXT_PUBLIC_PLAUSIBLE_SRC;
|
||||
|
||||
const PlausibleAnalytics = () => {
|
||||
return (
|
||||
<>
|
||||
{PLAUSIBLE_DOMAIN ? (
|
||||
<>
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
data-domain={PLAUSIBLE_DOMAIN}
|
||||
src={PLAUSIBLE_SRC}
|
||||
defer
|
||||
/>
|
||||
<Script
|
||||
id="plausible-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.plausible = window.plausible || function() {
|
||||
(window.plausible.q = window.plausible.q || []).push(arguments)
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlausibleAnalytics;
|
||||
78
app/[locale]/about/page.tsx
Normal file
78
app/[locale]/about/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import MDXComponents from "@/components/mdx/MDXComponents";
|
||||
import { Locale, LOCALES } from "@/i18n/routing";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import fs from "fs/promises";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { MDXRemote } from "next-mdx-remote-client/rsc";
|
||||
import path from "path";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
const options = {
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [],
|
||||
},
|
||||
};
|
||||
|
||||
async function getMDXContent(locale: string) {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"content",
|
||||
"about",
|
||||
`${locale}.mdx`
|
||||
);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(`Error reading MDX file: ${error}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
type Params = Promise<{
|
||||
locale: string;
|
||||
}>;
|
||||
|
||||
type MetadataProps = {
|
||||
params: Params;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "About" });
|
||||
|
||||
return constructMetadata({
|
||||
page: "About",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
locale: locale as Locale,
|
||||
path: `/about`,
|
||||
canonicalUrl: `/about`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AboutPage({ params }: { params: Params }) {
|
||||
const { locale } = await params;
|
||||
const content = await getMDXContent(locale);
|
||||
|
||||
return (
|
||||
<article className="w-full md:w-3/5 px-2 md:px-12">
|
||||
<MDXRemote
|
||||
source={content}
|
||||
components={MDXComponents}
|
||||
options={options}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return LOCALES.map((locale) => ({
|
||||
locale,
|
||||
}));
|
||||
}
|
||||
31
app/[locale]/blog/BlogCard.tsx
Normal file
31
app/[locale]/blog/BlogCard.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Link as I18nLink } from "@/i18n/routing";
|
||||
import { BlogPost } from "@/types/blog";
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
|
||||
export function BlogCard({ post, locale }: { post: BlogPost; locale: string }) {
|
||||
return (
|
||||
<I18nLink
|
||||
href={`/blog${post.slug}`}
|
||||
prefetch={false}
|
||||
className="bg-transparent rounded-lg hover:underline"
|
||||
>
|
||||
<div className="relative rounded shadow-md pt-[56.25%]">
|
||||
<Image
|
||||
src={post.image || "/placeholder.svg"}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover shadow-sm w-full rounded hover:shadow-lg transition-shadow duration-200 h-[200p]"
|
||||
/>
|
||||
</div>
|
||||
<div className="py-3 flex-1 flex flex-col">
|
||||
<h2 className="text-lg font-500 line-clamp-2 flex-grow">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mt-2">
|
||||
{dayjs(post.date).format("YYYY-MM-DD")}
|
||||
</p>
|
||||
</div>
|
||||
</I18nLink>
|
||||
);
|
||||
}
|
||||
103
app/[locale]/blog/[slug]/page.tsx
Normal file
103
app/[locale]/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Callout } from "@/components/mdx/Callout";
|
||||
import MDXComponents from "@/components/mdx/MDXComponents";
|
||||
import { Locale, LOCALES } from "@/i18n/routing";
|
||||
import { getPosts } from "@/lib/getBlogs";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { BlogPost } from "@/types/blog";
|
||||
import { Metadata } from "next";
|
||||
import { MDXRemote } from "next-mdx-remote-client/rsc";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
type Params = Promise<{
|
||||
locale: string;
|
||||
slug: string;
|
||||
}>;
|
||||
|
||||
type MetadataProps = {
|
||||
params: Params;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const { locale, slug } = await params;
|
||||
let { posts }: { posts: BlogPost[] } = await getPosts(locale);
|
||||
const post = posts.find((post) => post.slug === "/" + slug);
|
||||
|
||||
if (!post) {
|
||||
return constructMetadata({
|
||||
title: "404",
|
||||
description: "Page not found",
|
||||
noIndex: true,
|
||||
locale: locale as Locale,
|
||||
path: `/blog/${slug}`,
|
||||
canonicalUrl: `/blog/${slug}`,
|
||||
});
|
||||
}
|
||||
|
||||
return constructMetadata({
|
||||
page: "blog",
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
images: post.image ? [post.image] : [],
|
||||
locale: locale as Locale,
|
||||
path: `/blog/${slug}`,
|
||||
canonicalUrl: `/blog/${slug}`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function BlogPage({ params }: { params: Params }) {
|
||||
const { locale, slug } = await params;
|
||||
let { posts }: { posts: BlogPost[] } = await getPosts(locale);
|
||||
|
||||
const post = posts.find((item) => item.slug === "/" + slug);
|
||||
|
||||
if (!post) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full md:w-3/5 px-2 md:px-12">
|
||||
<h1 className="break-words text-4xl font-bold mt-6 mb-4">{post.title}</h1>
|
||||
{post.image && (
|
||||
<img src={post.image} alt={post.title} className="rounded-sm" />
|
||||
)}
|
||||
{post.tags && post.tags.split(",").length ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags.split(",").map((tag) => {
|
||||
return (
|
||||
<div
|
||||
key={tag}
|
||||
className={`rounded-md bg-gray-200 hover:!no-underline dark:bg-[#24272E] flex px-2.5 py-1.5 text-sm font-medium transition-colors hover:text-black hover:dark:bg-[#15AFD04C] hover:dark:text-[#82E9FF] text-gray-500 dark:text-[#7F818C] outline-none focus-visible:ring transition`}
|
||||
>
|
||||
{tag.trim()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{post.description && <Callout>{post.description}</Callout>}
|
||||
<MDXRemote source={post?.content || ""} components={MDXComponents} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
let posts = (await getPosts()).posts;
|
||||
|
||||
// Filter out posts without a slug
|
||||
posts = posts.filter((post) => post.slug);
|
||||
|
||||
return LOCALES.flatMap((locale) =>
|
||||
posts.map((post) => {
|
||||
const slugPart = post.slug.replace(/^\//, "").replace(/^blog\//, "");
|
||||
|
||||
return {
|
||||
locale,
|
||||
slug: slugPart,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
51
app/[locale]/blog/page.tsx
Normal file
51
app/[locale]/blog/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BlogCard } from "@/app/[locale]/blog/BlogCard";
|
||||
import { Locale, LOCALES } from "@/i18n/routing";
|
||||
import { getPosts } from "@/lib/getBlogs";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type Params = Promise<{ locale: string }>;
|
||||
|
||||
type MetadataProps = {
|
||||
params: Params;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "Blog" });
|
||||
|
||||
return constructMetadata({
|
||||
page: "Blog",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
locale: locale as Locale,
|
||||
path: `/blog`,
|
||||
canonicalUrl: `/blog`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: Params }) {
|
||||
const { locale } = await params;
|
||||
const { posts } = await getPosts(locale);
|
||||
|
||||
const t = await getTranslations("Blog");
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-4xl font-bold mb-8 text-center">{t("title")}</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{posts.map((post) => (
|
||||
<BlogCard key={post.slug} locale={locale} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return LOCALES.map((locale) => ({ locale }));
|
||||
}
|
||||
2
app/[locale]/globals.css
Normal file
2
app/[locale]/globals.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import url(../../styles/globals.css);
|
||||
@import url(../../styles/loading.css);
|
||||
110
app/[locale]/layout.tsx
Normal file
110
app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import BaiDuAnalytics from "@/app/BaiDuAnalytics";
|
||||
import GoogleAdsense from "@/app/GoogleAdsense";
|
||||
import GoogleAnalytics from "@/app/GoogleAnalytics";
|
||||
import PlausibleAnalytics from "@/app/PlausibleAnalytics";
|
||||
import Footer from "@/components/footer/Footer";
|
||||
import Header from "@/components/header/Header";
|
||||
import { LanguageDetectionAlert } from "@/components/LanguageDetectionAlert";
|
||||
import { TailwindIndicator } from "@/components/TailwindIndicator";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { DEFAULT_LOCALE, Locale, routing } from "@/i18n/routing";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { cn } from "@/lib/utils";
|
||||
import "@/styles/globals.css";
|
||||
import "@/styles/loading.css";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { Metadata, Viewport } from "next";
|
||||
import { hasLocale, NextIntlClientProvider } from "next-intl";
|
||||
import {
|
||||
getMessages,
|
||||
getTranslations,
|
||||
setRequestLocale,
|
||||
} from "next-intl/server";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { notFound } from "next/navigation";
|
||||
// import './globals.css';
|
||||
|
||||
type MetadataProps = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "Home" });
|
||||
|
||||
return constructMetadata({
|
||||
page: "Home",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
locale: locale as Locale,
|
||||
path: `/`,
|
||||
canonicalUrl: `/`,
|
||||
});
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: siteConfig.themeColors,
|
||||
};
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
// Ensure that the incoming `locale` is valid
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang={locale || DEFAULT_LOCALE} suppressHydrationWarning>
|
||||
<head />
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background flex flex-col font-sans antialiased"
|
||||
)}
|
||||
>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={siteConfig.defaultNextTheme}
|
||||
enableSystem
|
||||
>
|
||||
{messages.LanguageDetection && <LanguageDetectionAlert />}
|
||||
{messages.Header && <Header />}
|
||||
|
||||
<main className="flex-1 flex flex-col items-center">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{messages.Footer && <Footer />}
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
<TailwindIndicator />
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<Analytics />
|
||||
<BaiDuAnalytics />
|
||||
<GoogleAnalytics />
|
||||
<GoogleAdsense />
|
||||
<PlausibleAnalytics />
|
||||
</>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
15
app/[locale]/page.tsx
Normal file
15
app/[locale]/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import HomeComponent from "@/components/home";
|
||||
|
||||
// export const dynamic = "force-static";
|
||||
|
||||
export default function Home() {
|
||||
return <HomeComponent />;
|
||||
}
|
||||
|
||||
// export async function generateStaticParams() {
|
||||
// return [
|
||||
// { locale: 'en' },
|
||||
// { locale: 'zh' },
|
||||
// { locale: 'ja' },
|
||||
// ]
|
||||
// }
|
||||
78
app/[locale]/privacy-policy/page.tsx
Normal file
78
app/[locale]/privacy-policy/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import MDXComponents from "@/components/mdx/MDXComponents";
|
||||
import { Locale, LOCALES } from "@/i18n/routing";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import fs from "fs/promises";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { MDXRemote } from "next-mdx-remote-client/rsc";
|
||||
import path from "path";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
const options = {
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [],
|
||||
},
|
||||
};
|
||||
|
||||
async function getMDXContent(locale: string) {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"content",
|
||||
"privacy-policy",
|
||||
`${locale}.mdx`
|
||||
);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(`Error reading MDX file: ${error}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
type Params = Promise<{
|
||||
locale: string;
|
||||
}>;
|
||||
|
||||
type MetadataProps = {
|
||||
params: Params;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "PrivacyPolicy" });
|
||||
|
||||
return constructMetadata({
|
||||
page: "PrivacyPolicy",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
locale: locale as Locale,
|
||||
path: `/privacy-policy`,
|
||||
canonicalUrl: `/privacy-policy`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AboutPage({ params }: { params: Params }) {
|
||||
const { locale } = await params;
|
||||
const content = await getMDXContent(locale);
|
||||
|
||||
return (
|
||||
<article className="w-full md:w-3/5 px-2 md:px-12">
|
||||
<MDXRemote
|
||||
source={content}
|
||||
components={MDXComponents}
|
||||
options={options}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return LOCALES.map((locale) => ({
|
||||
locale,
|
||||
}));
|
||||
}
|
||||
78
app/[locale]/terms-of-service/page.tsx
Normal file
78
app/[locale]/terms-of-service/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import MDXComponents from "@/components/mdx/MDXComponents";
|
||||
import { Locale, LOCALES } from "@/i18n/routing";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import fs from "fs/promises";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { MDXRemote } from "next-mdx-remote-client/rsc";
|
||||
import path from "path";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
const options = {
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [],
|
||||
},
|
||||
};
|
||||
|
||||
async function getMDXContent(locale: string) {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"content",
|
||||
"terms-of-service",
|
||||
`${locale}.mdx`
|
||||
);
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(`Error reading MDX file: ${error}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
type Params = Promise<{
|
||||
locale: string;
|
||||
}>;
|
||||
|
||||
type MetadataProps = {
|
||||
params: Params;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "TermsOfService" });
|
||||
|
||||
return constructMetadata({
|
||||
page: "TermsOfService",
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
locale: locale as Locale,
|
||||
path: `/terms-of-service`,
|
||||
canonicalUrl: `/terms-of-service`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AboutPage({ params }: { params: Params }) {
|
||||
const { locale } = await params;
|
||||
const content = await getMDXContent(locale);
|
||||
|
||||
return (
|
||||
<article className="w-full md:w-3/5 px-2 md:px-12">
|
||||
<MDXRemote
|
||||
source={content}
|
||||
components={MDXComponents}
|
||||
options={options}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return LOCALES.map((locale) => ({
|
||||
locale,
|
||||
}));
|
||||
}
|
||||
31
app/robots.ts
Normal file
31
app/robots.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
const siteUrl = siteConfig.url
|
||||
|
||||
// export const dynamic = "force-static"
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: [
|
||||
'/private/',
|
||||
'/api/',
|
||||
'/auth/',
|
||||
'/dashboard/',
|
||||
'/_next/',
|
||||
'/assets/',
|
||||
'/error',
|
||||
'/*/404',
|
||||
'/*/500',
|
||||
'/*/403',
|
||||
'/*/401',
|
||||
'/*/400',
|
||||
'/cdn-cgi/',
|
||||
],
|
||||
},
|
||||
sitemap: `${siteUrl}/sitemap.xml`
|
||||
}
|
||||
}
|
||||
48
app/sitemap.ts
Normal file
48
app/sitemap.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing'
|
||||
import { getPosts } from '@/lib/getBlogs'
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
const siteUrl = siteConfig.url
|
||||
|
||||
type ChangeFrequency = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' | undefined
|
||||
|
||||
// export const dynamic = "force-static"
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
// Static pages
|
||||
const staticPages = [
|
||||
'',
|
||||
'/blog',
|
||||
'/about',
|
||||
'/privacy-policy',
|
||||
'/terms-of-service',
|
||||
]
|
||||
|
||||
// Generate multilingual pages
|
||||
const pages = LOCALES.flatMap(locale => {
|
||||
return staticPages.map(page => ({
|
||||
url: `${siteUrl}${locale === DEFAULT_LOCALE ? '' : `/${locale}`}${page}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily' as ChangeFrequency,
|
||||
priority: page === '' ? 1.0 : 0.8,
|
||||
}))
|
||||
})
|
||||
|
||||
const blogPosts = await Promise.all(
|
||||
LOCALES.map(async (locale) => {
|
||||
const { posts } = await getPosts(locale)
|
||||
return posts.map(post => ({
|
||||
url: `${siteUrl}${locale === DEFAULT_LOCALE ? '' : `/${locale}`}/blog${post.slug}`,
|
||||
lastModified: post.metadata.updatedAt || post.date,
|
||||
changeFrequency: 'daily' as const,
|
||||
priority: 0.7,
|
||||
}))
|
||||
})
|
||||
).then(results => results.flat())
|
||||
|
||||
return [
|
||||
...pages,
|
||||
...blogPosts,
|
||||
]
|
||||
}
|
||||
18
blogs/en/1.demo.mdx
Normal file
18
blogs/en/1.demo.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: demo
|
||||
description: it is a description
|
||||
slug: /demo
|
||||
tags: nextjs,i18n,mdx,starter,robots,sitemap
|
||||
date: 2025-02-16
|
||||
visible: published
|
||||
# visible: draft/invisible/published (published is default)
|
||||
pin: pin
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
demo
|
||||
|
||||
## How to use
|
||||
|
||||
demo
|
||||
18
blogs/ja/1.demo.mdx
Normal file
18
blogs/ja/1.demo.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: デモ
|
||||
description: これはデモプロジェクトの説明です
|
||||
slug: /demo
|
||||
tags: nextjs,i18n,mdx,starter,robots,sitemap
|
||||
date: 2025-02-16
|
||||
visible: published
|
||||
# visible: draft/invisible/published、デフォルトはpublished
|
||||
# pin: pin
|
||||
---
|
||||
|
||||
## はじめに
|
||||
|
||||
デモ
|
||||
|
||||
## 使い方
|
||||
|
||||
デモ
|
||||
18
blogs/zh/1.demo.mdx
Normal file
18
blogs/zh/1.demo.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: 示例
|
||||
description: 这是一个描述
|
||||
slug: /demo
|
||||
tags: nextjs,i18n,mdx,starter,robots,sitemap
|
||||
date: 2025-02-16
|
||||
visible: published
|
||||
# visible: draft/invisible/published,默认为published
|
||||
pin: true
|
||||
---
|
||||
|
||||
## 简介
|
||||
|
||||
示例
|
||||
|
||||
## 如何使用
|
||||
|
||||
示例
|
||||
18
blogs/zh/2.demo2.mdx
Normal file
18
blogs/zh/2.demo2.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: 示例2
|
||||
description: 这是一个描述2
|
||||
slug: /demo2
|
||||
tags: nextjs,i18n,mdx,starter,robots,sitemap
|
||||
date: 2025-02-17
|
||||
visible: published
|
||||
# visible: draft/invisible/published,默认为published
|
||||
# pin: true
|
||||
---
|
||||
|
||||
## 简介
|
||||
|
||||
示例2
|
||||
|
||||
## 如何使用
|
||||
|
||||
示例2
|
||||
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
44
components/BuiltWithButton.tsx
Normal file
44
components/BuiltWithButton.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
// Nexty.dev Affiliate Link: https://affiliates.nexty.dev/
|
||||
// sign up and use your affiliate link on BuiltWithButton to earn money
|
||||
|
||||
export default function BuiltWithButton() {
|
||||
return (
|
||||
<Link
|
||||
href="https://nexty.dev"
|
||||
title="Built with Nexty.dev"
|
||||
prefetch={false}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline", size: "sm" }),
|
||||
"px-4 rounded-md bg-transparent border-gray-500 hover:bg-gray-950 text-white hover:text-gray-100"
|
||||
)}
|
||||
>
|
||||
<span>Built with</span>
|
||||
<span>
|
||||
<LogoNexty className="size-4 rounded-full" />
|
||||
</span>
|
||||
<span className="font-bold text-base-content flex gap-0.5 items-center tracking-tight">
|
||||
Nexty.dev
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoNexty({ className }: { className?: string }) {
|
||||
return (
|
||||
<img
|
||||
src="/logo_nexty.png"
|
||||
alt="Logo"
|
||||
title="Logo"
|
||||
loading="lazy"
|
||||
width={96}
|
||||
height={96}
|
||||
className={cn("size-8 rounded-md", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
144
components/LanguageDetectionAlert.tsx
Normal file
144
components/LanguageDetectionAlert.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Link as I18nLink, LOCALE_NAMES, routing } from "@/i18n/routing";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useLocaleStore } from "@/stores/localeStore";
|
||||
import { ArrowRight, Globe, X } from "lucide-react";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export function LanguageDetectionAlert() {
|
||||
const [countdown, setCountdown] = useState(10); // countdown 10s and dismiss
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const locale = useLocale();
|
||||
const [detectedLocale, setDetectedLocale] = useState<string | null>(null);
|
||||
const {
|
||||
showLanguageAlert,
|
||||
setShowLanguageAlert,
|
||||
dismissLanguageAlert,
|
||||
getLangAlertDismissed,
|
||||
} = useLocaleStore();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(() => {
|
||||
dismissLanguageAlert();
|
||||
}, 300);
|
||||
}, [dismissLanguageAlert]);
|
||||
|
||||
const handleSwitchLanguage = useCallback(() => {
|
||||
dismissLanguageAlert();
|
||||
}, [dismissLanguageAlert]);
|
||||
|
||||
useEffect(() => {
|
||||
const detectedLang = navigator.language; // Get full language code, e.g., zh_HK
|
||||
const storedDismiss = getLangAlertDismissed();
|
||||
|
||||
if (!storedDismiss) {
|
||||
let supportedLang = routing.locales.find((l) => l === detectedLang);
|
||||
|
||||
if (!supportedLang) {
|
||||
const mainLang = detectedLang.split("-")[0];
|
||||
supportedLang = routing.locales.find((l) => l.startsWith(mainLang));
|
||||
}
|
||||
|
||||
if (supportedLang && supportedLang !== locale) {
|
||||
setDetectedLocale(supportedLang);
|
||||
setShowLanguageAlert(true);
|
||||
setTimeout(() => setIsVisible(true), 100);
|
||||
}
|
||||
}
|
||||
}, [locale, getLangAlertDismissed, setShowLanguageAlert]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
if (showLanguageAlert && countdown > 0) {
|
||||
timer = setInterval(() => {
|
||||
setCountdown((prev) => prev - 1);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [showLanguageAlert, countdown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown === 0 && showLanguageAlert) {
|
||||
handleDismiss();
|
||||
}
|
||||
}, [countdown, showLanguageAlert, handleDismiss]);
|
||||
|
||||
if (!showLanguageAlert || !detectedLocale) return null;
|
||||
|
||||
const messages = require(`@/i18n/messages/${detectedLocale}.json`);
|
||||
const alertMessages = messages.LanguageDetection;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-16 right-4 z-50 max-w-sm w-full mx-4 sm:mx-0 sm:w-96",
|
||||
"transform transition-all duration-300 ease-in-out",
|
||||
isVisible
|
||||
? "translate-x-0 translate-y-0 opacity-100"
|
||||
: "translate-x-full opacity-0"
|
||||
)}
|
||||
role="banner"
|
||||
aria-live="polite"
|
||||
aria-label="Language detection alert"
|
||||
>
|
||||
<div className="bg-background/95 backdrop-blur-md border border-border rounded-xl shadow-lg p-4 relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-2 h-6 w-6 opacity-50 hover:opacity-100"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss language suggestion"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="pr-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Globe className="h-4 w-4 text-primary" />
|
||||
<h3 className="font-medium text-sm text-foreground">
|
||||
{alertMessages.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground mb-4 leading-relaxed">
|
||||
{alertMessages.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button asChild onClick={handleSwitchLanguage}>
|
||||
<I18nLink
|
||||
href="/"
|
||||
title={`${alertMessages.switchTo} ${LOCALE_NAMES[detectedLocale]}`}
|
||||
locale={detectedLocale as any}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-lg",
|
||||
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
"text-sm font-medium transition-colors",
|
||||
"group focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
)}
|
||||
aria-label={`${alertMessages.switchTo} ${LOCALE_NAMES[detectedLocale]}`}
|
||||
>
|
||||
<span>
|
||||
{alertMessages.switchTo} {LOCALE_NAMES[detectedLocale]}
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3 transition-transform group-hover:translate-x-0.5" />
|
||||
</I18nLink>
|
||||
</Button>
|
||||
|
||||
<span className="text-xs text-muted-foreground">{countdown}s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-primary/10 to-transparent pointer-events-none opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
components/LocaleSwitcher.tsx
Normal file
71
components/LocaleSwitcher.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Locale,
|
||||
LOCALE_NAMES,
|
||||
routing,
|
||||
usePathname,
|
||||
useRouter,
|
||||
} from "@/i18n/routing";
|
||||
import { useLocaleStore } from "@/stores/localeStore";
|
||||
import { Globe } from "lucide-react";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
|
||||
export default function LocaleSwitcher() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
const locale = useLocale();
|
||||
const { dismissLanguageAlert } = useLocaleStore();
|
||||
const [, startTransition] = useTransition();
|
||||
const [currentLocale, setCurrentLocale] = useState("locale");
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLocale(locale);
|
||||
}, [locale, setCurrentLocale]);
|
||||
|
||||
function onSelectChange(nextLocale: Locale) {
|
||||
setCurrentLocale(nextLocale);
|
||||
dismissLanguageAlert();
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(
|
||||
// @ts-expect-error -- TypeScript will validate that only known `params`
|
||||
// are used in combination with a given `pathname`. Since the two will
|
||||
// always match for the current route, we can skip runtime checks.
|
||||
// { pathname: "/", params: params || {} }, // if your want to redirect to the home page
|
||||
{ pathname, params: params || {} }, // if your want to redirect to the current page
|
||||
{ locale: nextLocale }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
defaultValue={locale}
|
||||
value={currentLocale}
|
||||
onValueChange={onSelectChange}
|
||||
>
|
||||
<SelectTrigger className="w-fit">
|
||||
<Globe className="w-4 h-4 mr-1" />
|
||||
<SelectValue placeholder="Language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{routing.locales.map((cur) => (
|
||||
<SelectItem key={cur} value={cur}>
|
||||
{LOCALE_NAMES[cur]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
14
components/TailwindIndicator.tsx
Normal file
14
components/TailwindIndicator.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export function TailwindIndicator() {
|
||||
if (process.env.NODE_ENV === "production") return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
|
||||
<div className="sm:hidden">xs</div>
|
||||
<div className="hidden sm:block md:hidden">sm</div>
|
||||
<div className="hidden md:block lg:hidden">md</div>
|
||||
<div className="hidden lg:block xl:hidden">lg</div>
|
||||
<div className="hidden xl:block 2xl:hidden">xl</div>
|
||||
<div className="hidden 2xl:block">2xl</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
components/ThemeToggle.tsx
Normal file
39
components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
108
components/WebsiteLogo.tsx
Normal file
108
components/WebsiteLogo.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
import { getDomain } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface IProps {
|
||||
url: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
const WebsiteLogo = ({
|
||||
url,
|
||||
size = 32,
|
||||
className = "",
|
||||
timeout = 1000, // 1 second
|
||||
}: IProps) => {
|
||||
const domain = getDomain(url);
|
||||
const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`);
|
||||
const [fallbackIndex, setFallbackIndex] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const fallbackSources = [
|
||||
`https://${domain}/logo.svg`,
|
||||
`https://${domain}/logo.png`,
|
||||
`https://${domain}/apple-touch-icon.png`,
|
||||
`https://${domain}/apple-touch-icon-precomposed.png`,
|
||||
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
|
||||
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
|
||||
`https://${domain}/favicon.ico`,
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: any;
|
||||
|
||||
if (isLoading) {
|
||||
timeoutId = setTimeout(() => {
|
||||
handleError();
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [imgSrc, isLoading]);
|
||||
|
||||
const handleError = () => {
|
||||
const nextIndex = fallbackIndex + 1;
|
||||
if (nextIndex < fallbackSources.length) {
|
||||
setFallbackIndex(nextIndex);
|
||||
setImgSrc(fallbackSources[nextIndex]);
|
||||
setIsLoading(true);
|
||||
} else {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
setHasError(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative inline-block ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
{/* placeholder */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 animate-pulse">
|
||||
<div className="w-full h-full rounded-md bg-gray-200/60" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt={`${domain} logo`}
|
||||
width={size}
|
||||
height={size}
|
||||
onError={handleError}
|
||||
onLoad={handleLoad}
|
||||
className={`inline-block transition-opacity duration-300 ${
|
||||
isLoading ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
display: hasError ? "none" : "inline-block",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Fallback: Display first letter of domain when all image sources fail */}
|
||||
{hasError && (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center bg-gray-100 rounded-md"
|
||||
style={{ fontSize: `${size * 0.5}px` }}
|
||||
>
|
||||
{domain.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebsiteLogo;
|
||||
20
components/footer/Badges.tsx
Normal file
20
components/footer/Badges.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
export default function Badges() {
|
||||
return (
|
||||
<div className="py-6 flex justify-center w-full flex-wrap gap-2">
|
||||
<a
|
||||
href="https://dofollow.tools"
|
||||
title="Featured on Dofollow.Tools"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="https://dofollow.tools/badge/badge_light.svg"
|
||||
alt="Featured on Dofollow.Tools"
|
||||
width="200"
|
||||
height="54"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
components/footer/Footer.tsx
Normal file
188
components/footer/Footer.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import BuiltWithButton from "@/components/BuiltWithButton";
|
||||
import { TwitterX } from "@/components/social-icons/icons";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { Link as I18nLink } from "@/i18n/routing";
|
||||
import { FooterLink } from "@/types/common";
|
||||
import { GithubIcon, MailIcon } from "lucide-react";
|
||||
import { getMessages, getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { SiBluesky, SiDiscord } from "react-icons/si";
|
||||
|
||||
export default async function Footer() {
|
||||
const messages = await getMessages();
|
||||
|
||||
const t = await getTranslations("Home");
|
||||
const tFooter = await getTranslations("Footer");
|
||||
|
||||
const footerLinks: FooterLink[] = tFooter.raw("Links.groups");
|
||||
footerLinks.forEach((group) => {
|
||||
const pricingLink = group.links.find((link) => link.id === "pricing");
|
||||
if (pricingLink) {
|
||||
pricingLink.href = process.env.NEXT_PUBLIC_PRICING_PATH!;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 text-gray-300">
|
||||
<footer className="py-2 border-t border-gray-700">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 py-12 lg:grid-cols-6">
|
||||
<div className="w-full flex flex-col sm:flex-row lg:flex-col gap-4 col-span-full md:col-span-2">
|
||||
<div className="space-y-4 flex-1">
|
||||
<div className="items-center space-x-2 flex">
|
||||
<h2 className="highlight-text text-2xl font-bold">
|
||||
{t("title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-sm p4-4 md:pr-12">{t("tagLine")}</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{siteConfig.socialLinks?.github && (
|
||||
<Link
|
||||
href={siteConfig.socialLinks.github}
|
||||
prefetch={false}
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow noopener"
|
||||
aria-label="GitHub"
|
||||
title="View on GitHub"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
)}
|
||||
{siteConfig.socialLinks?.discord && (
|
||||
<Link
|
||||
href={siteConfig.socialLinks.discord}
|
||||
prefetch={false}
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow noopener"
|
||||
aria-label="Discord"
|
||||
title="Join Discord"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<SiDiscord className="w-4 h-4" aria-hidden="true" />
|
||||
</Link>
|
||||
)}
|
||||
{siteConfig.socialLinks?.twitter && (
|
||||
<Link
|
||||
href={siteConfig.socialLinks.twitter}
|
||||
prefetch={false}
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow noopener"
|
||||
aria-label="Twitter"
|
||||
title="View on Twitter"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<TwitterX className="w-4 h-4" aria-hidden="true" />
|
||||
</Link>
|
||||
)}
|
||||
{siteConfig.socialLinks?.bluesky && (
|
||||
<Link
|
||||
href={siteConfig.socialLinks.bluesky}
|
||||
prefetch={false}
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow noopener"
|
||||
aria-label="Blue Sky"
|
||||
title="View on Bluesky"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<SiBluesky className="w-4 h-4" aria-hidden="true" />
|
||||
</Link>
|
||||
)}
|
||||
{siteConfig.socialLinks?.email && (
|
||||
<Link
|
||||
href={`mailto:${siteConfig.socialLinks.email}`}
|
||||
prefetch={false}
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow noopener"
|
||||
aria-label="Email"
|
||||
title="Email"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<MailIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BuiltWithButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{footerLinks.map((section) => (
|
||||
<div key={section.title} className="flex-1">
|
||||
<h3 className="text-white text-lg font-semibold mb-4">
|
||||
{section.title}
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{section.links.map((link) => (
|
||||
<li key={link.href}>
|
||||
{link.href.startsWith("/") && !link.useA ? (
|
||||
<I18nLink
|
||||
href={link.href}
|
||||
title={link.name}
|
||||
prefetch={false}
|
||||
className="hover:text-white transition-colors"
|
||||
target={link.target || ""}
|
||||
rel={link.rel || ""}
|
||||
>
|
||||
{link.name}
|
||||
</I18nLink>
|
||||
) : (
|
||||
<Link
|
||||
href={link.href}
|
||||
title={link.name}
|
||||
prefetch={false}
|
||||
className="hover:text-white transition-colors"
|
||||
target={link.target || ""}
|
||||
rel={link.rel || ""}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* {messages.Footer.Newsletter && (
|
||||
<div className="w-full flex-1">
|
||||
<Newsletter />
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 py-6 flex flex-col md:flex-row justify-between items-center">
|
||||
<p className="text-gray-400 text-sm">
|
||||
{tFooter("Copyright", {
|
||||
year: new Date().getFullYear(),
|
||||
name: siteConfig.name,
|
||||
})}
|
||||
</p>
|
||||
<div className="flex space-x-6 mt-4 md:mt-0">
|
||||
<I18nLink
|
||||
href="/privacy-policy"
|
||||
title={tFooter("PrivacyPolicy")}
|
||||
prefetch={false}
|
||||
className="text-gray-400 hover:text-white text-sm"
|
||||
>
|
||||
{tFooter("PrivacyPolicy")}
|
||||
</I18nLink>
|
||||
<I18nLink
|
||||
href="/terms-of-service"
|
||||
title={tFooter("TermsOfService")}
|
||||
prefetch={false}
|
||||
className="text-gray-400 hover:text-white text-sm"
|
||||
>
|
||||
{tFooter("TermsOfService")}
|
||||
</I18nLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Badges /> */}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
components/footer/Newsletter.tsx
Normal file
93
components/footer/Newsletter.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { normalizeEmail, validateEmail } from "@/lib/email";
|
||||
import { Send } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
export function Newsletter() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [subscribeStatus, setSubscribeStatus] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const t = useTranslations("Footer.Newsletter");
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email) return;
|
||||
|
||||
const normalizedEmailAddress = normalizeEmail(email);
|
||||
const { isValid, error } = validateEmail(normalizedEmailAddress);
|
||||
|
||||
if (!isValid) {
|
||||
setSubscribeStatus("error");
|
||||
setErrorMessage(error || t("defaultErrorMessage"));
|
||||
setTimeout(() => setSubscribeStatus("idle"), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubscribeStatus("loading");
|
||||
|
||||
const response = await fetch("/api/newsletter", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: normalizedEmailAddress }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || t("errorMessage"));
|
||||
}
|
||||
|
||||
setSubscribeStatus("success");
|
||||
setEmail("");
|
||||
setErrorMessage("");
|
||||
setTimeout(() => setSubscribeStatus("idle"), 5000);
|
||||
} catch (error) {
|
||||
setSubscribeStatus("error");
|
||||
setErrorMessage(
|
||||
error instanceof Error ? error.message : t("errorMessage2")
|
||||
);
|
||||
setTimeout(() => setSubscribeStatus("idle"), 5000);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="">
|
||||
<h4 className="mb-3 font-semibold">{t("title")}</h4>
|
||||
<p className="text-sm mb-3">{t("description")}</p>
|
||||
<form onSubmit={handleSubscribe} className="flex flex-col gap-2 max-w-64">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
className="w-full px-3 py-2 bg-gray-200 text-black text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
disabled={subscribeStatus === "loading"}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={subscribeStatus === "loading"}>
|
||||
{subscribeStatus === "loading" ? (
|
||||
t("subscribing")
|
||||
) : (
|
||||
<>
|
||||
{t("subscribe")} <Send className="w-3.5 h-3.5" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{subscribeStatus === "success" && (
|
||||
<p className="text-xs text-green-600 mt-1">{t("subscribed")}</p>
|
||||
)}
|
||||
{subscribeStatus === "error" && (
|
||||
<p className="text-xs text-red-600 mt-1">{errorMessage}</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
components/header/Header.tsx
Normal file
51
components/header/Header.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import HeaderLinks from "@/components/header/HeaderLinks";
|
||||
import MobileMenu from "@/components/header/MobileMenu";
|
||||
import LocaleSwitcher from "@/components/LocaleSwitcher";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { Link as I18nLink } from "@/i18n/routing";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
|
||||
const Header = () => {
|
||||
const t = useTranslations("Home");
|
||||
|
||||
return (
|
||||
<header className="py-2 px-6 backdrop-blur-md sticky top-0 z-50">
|
||||
<nav className="flex justify-between items-center w-full mx-auto">
|
||||
<div className="flex items-center space-x-6 md:space-x-12">
|
||||
<I18nLink
|
||||
href="/"
|
||||
prefetch={false}
|
||||
className="flex items-center space-x-1 font-bold"
|
||||
>
|
||||
<Image
|
||||
alt={siteConfig.name}
|
||||
src="/logo.svg"
|
||||
className="w-6 h-6"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<span className="text-gray-800 dark:text-gray-200">
|
||||
{t("title")}
|
||||
</span>
|
||||
</I18nLink>
|
||||
<HeaderLinks />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2 md:gap-x-4 lg:gap-x-6 flex-1 justify-end">
|
||||
{/* PC */}
|
||||
<div className="hidden md:flex items-center gap-x-4">
|
||||
<LocaleSwitcher />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Mobile */}
|
||||
<MobileMenu />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
42
components/header/HeaderLinks.tsx
Normal file
42
components/header/HeaderLinks.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { Link as I18nLink, usePathname } from "@/i18n/routing";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HeaderLink } from "@/types/common";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const HeaderLinks = () => {
|
||||
const tHeader = useTranslations("Header");
|
||||
const pathname = usePathname();
|
||||
|
||||
const headerLinks: HeaderLink[] = tHeader.raw("links");
|
||||
|
||||
return (
|
||||
<div className="hidden md:flex flex-row items-center gap-x-2 text-sm font-medium text-muted-500">
|
||||
{headerLinks.map((link) => (
|
||||
<I18nLink
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
title={link.name}
|
||||
prefetch={link.target && link.target === "_blank" ? false : true}
|
||||
target={link.target || "_self"}
|
||||
rel={link.rel || undefined}
|
||||
className={cn(
|
||||
"rounded-xl px-4 py-2 flex items-center gap-x-1 hover:bg-accent-foreground/10 hover:text-accent-foreground",
|
||||
pathname === link.href && "font-semibold text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
{link.name}
|
||||
{link.target && link.target === "_blank" && (
|
||||
<span className="text-xs">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</span>
|
||||
)}
|
||||
</I18nLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderLinks;
|
||||
74
components/header/MobileMenu.tsx
Normal file
74
components/header/MobileMenu.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import LocaleSwitcher from "@/components/LocaleSwitcher";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Link as I18nLink } from "@/i18n/routing";
|
||||
import { HeaderLink } from "@/types/common";
|
||||
import { Menu } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function MobileMenu() {
|
||||
const t = useTranslations("Home");
|
||||
const tHeader = useTranslations("Header");
|
||||
|
||||
const headerLinks: HeaderLink[] = tHeader.raw("links");
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 md:hidden">
|
||||
<LocaleSwitcher />
|
||||
<ThemeToggle />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="p-2">
|
||||
<Menu className="h-5 w-5" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel>
|
||||
<I18nLink
|
||||
href="/"
|
||||
title={t("title")}
|
||||
prefetch={true}
|
||||
className="flex items-center space-x-1 font-bold"
|
||||
>
|
||||
<Image
|
||||
alt={t("title")}
|
||||
src="/logo.svg"
|
||||
className="w-6 h-6"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<span className="highlight-text">{t("title")}</span>
|
||||
</I18nLink>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{headerLinks.map((link) => (
|
||||
<DropdownMenuItem key={link.name}>
|
||||
<I18nLink
|
||||
href={link.href}
|
||||
title={link.name}
|
||||
prefetch={
|
||||
link.target && link.target === "_blank" ? false : true
|
||||
}
|
||||
target={link.target || "_self"}
|
||||
rel={link.rel || undefined}
|
||||
>
|
||||
{link.name}
|
||||
</I18nLink>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
components/home/index.tsx
Normal file
76
components/home/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { MousePointerClick } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { SiDiscord } from "react-icons/si";
|
||||
|
||||
export default function HomeComponent() {
|
||||
const t = useTranslations("Home");
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pb-16 pt-24 text-center">
|
||||
<h1 className="mx-auto max-w-4xl font-display text-5xl font-bold tracking-tight text-slate-900 sm:text-7xl dark:text-gray-200">
|
||||
Next.js{" "}
|
||||
<span className="relative whitespace-nowrap text-blue-600">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 418 42"
|
||||
className="absolute left-0 top-2/3 h-[0.58em] w-full fill-blue-300/70"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z"></path>
|
||||
</svg>
|
||||
<span className="relative"> i18n </span>{" "}
|
||||
</span>
|
||||
Starter
|
||||
</h1>
|
||||
<p className="mx-auto mt-6 max-w-2xl text-2xl tracking-tight text-slate-700 dark:text-slate-500">
|
||||
{t("description")}
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex items-center justify-center gap-2">
|
||||
<Button
|
||||
className="h-11 rounded-xl px-8 py-2 bg-white text-indigo-500 hover:text-indigo-600 border-2 border-indigo-500"
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="https://nexty.dev/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Get SaaS Version - Nexty.dev"
|
||||
prefetch={false}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<MousePointerClick className="w-4 h-4 text-indigo-500" />
|
||||
Get SaaS Version
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
className="h-11 rounded-xl px-8 py-2 bg-white text-indigo-500 hover:text-indigo-600 border-2 border-indigo-500"
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={
|
||||
siteConfig.socialLinks?.discord ||
|
||||
"https://discord.com/invite/R7bUxWKRqZ"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
title="Join Discord"
|
||||
prefetch={false}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SiDiscord className="w-4 h-4 text-indigo-500" />
|
||||
Join Discord
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
components/icons/expanding-arrow.tsx
Normal file
36
components/icons/expanding-arrow.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
export default function ExpandingArrow({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className="group relative flex items-center">
|
||||
<svg
|
||||
className={`${
|
||||
className ? className : "h-4 w-4"
|
||||
} absolute transition-all group-hover:translate-x-1 group-hover:opacity-0`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg
|
||||
className={`${
|
||||
className ? className : "h-4 w-4"
|
||||
} absolute opacity-0 transition-all group-hover:translate-x-1 group-hover:opacity-100`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
components/icons/eye.tsx
Normal file
24
components/icons/eye.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function TablerEyeFilled(props: any) {
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
fill="#000000"
|
||||
d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22.379l.045.1l.03.083l.014.055l.014.082l.011.1v.11l-.014.111a.992.992 0 0 1-.026.11l-.039.108l-.036.075l-.016.03c-2.764 4.836-6.3 7.38-10.555 7.499L12 20c-4.396 0-8.037-2.549-10.868-7.504a1 1 0 0 1 0-.992C3.963 6.549 7.604 4 12 4zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0-6z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
4
components/icons/index.tsx
Normal file
4
components/icons/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as LoadingDots } from "./loading-dots";
|
||||
export { default as LoadingCircle } from "./loading-circle";
|
||||
export { default as LoadingSpinner } from "./loading-spinner";
|
||||
export { default as ExpandingArrow } from "./expanding-arrow";
|
||||
20
components/icons/loading-circle.tsx
Normal file
20
components/icons/loading-circle.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function LoadingCircle() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-4 w-4 animate-spin fill-gray-600 text-gray-200"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
40
components/icons/loading-dots.module.css
Normal file
40
components/icons/loading-dots.module.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading .spacer {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.loading span {
|
||||
animation-name: blink;
|
||||
animation-duration: 1.4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-fill-mode: both;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.loading span:nth-of-type(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.loading span:nth-of-type(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
13
components/icons/loading-dots.tsx
Normal file
13
components/icons/loading-dots.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import styles from "./loading-dots.module.css";
|
||||
|
||||
const LoadingDots = ({ color = "#000" }: { color?: string }) => {
|
||||
return (
|
||||
<span className={styles.loading}>
|
||||
<span style={{ backgroundColor: color }} />
|
||||
<span style={{ backgroundColor: color }} />
|
||||
<span style={{ backgroundColor: color }} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingDots;
|
||||
79
components/icons/loading-spinner.module.css
Normal file
79
components/icons/loading-spinner.module.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.spinner {
|
||||
color: gray;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
transform: scale(0.3) translateX(-95px);
|
||||
}
|
||||
.spinner div {
|
||||
transform-origin: 40px 40px;
|
||||
animation: spinner 1.2s linear infinite;
|
||||
}
|
||||
.spinner div:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 37px;
|
||||
width: 6px;
|
||||
height: 20px;
|
||||
border-radius: 20%;
|
||||
background: black;
|
||||
}
|
||||
.spinner div:nth-child(1) {
|
||||
transform: rotate(0deg);
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
.spinner div:nth-child(2) {
|
||||
transform: rotate(30deg);
|
||||
animation-delay: -1s;
|
||||
}
|
||||
.spinner div:nth-child(3) {
|
||||
transform: rotate(60deg);
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
.spinner div:nth-child(4) {
|
||||
transform: rotate(90deg);
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
.spinner div:nth-child(5) {
|
||||
transform: rotate(120deg);
|
||||
animation-delay: -0.7s;
|
||||
}
|
||||
.spinner div:nth-child(6) {
|
||||
transform: rotate(150deg);
|
||||
animation-delay: -0.6s;
|
||||
}
|
||||
.spinner div:nth-child(7) {
|
||||
transform: rotate(180deg);
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.spinner div:nth-child(8) {
|
||||
transform: rotate(210deg);
|
||||
animation-delay: -0.4s;
|
||||
}
|
||||
.spinner div:nth-child(9) {
|
||||
transform: rotate(240deg);
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.spinner div:nth-child(10) {
|
||||
transform: rotate(270deg);
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
.spinner div:nth-child(11) {
|
||||
transform: rotate(300deg);
|
||||
animation-delay: -0.1s;
|
||||
}
|
||||
.spinner div:nth-child(12) {
|
||||
transform: rotate(330deg);
|
||||
animation-delay: 0s;
|
||||
}
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
20
components/icons/loading-spinner.tsx
Normal file
20
components/icons/loading-spinner.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import styles from "./loading-spinner.module.css";
|
||||
|
||||
export default function LoadingSpinner() {
|
||||
return (
|
||||
<div className={styles.spinner}>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
components/icons/moon.tsx
Normal file
16
components/icons/moon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default function PhMoonFill(props: any) {
|
||||
return (
|
||||
<svg
|
||||
width="1.5em"
|
||||
height="1.5em"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#000000"
|
||||
d="M235.54 150.21a104.84 104.84 0 0 1-37 52.91A104 104 0 0 1 32 120a103.09 103.09 0 0 1 20.88-62.52a104.84 104.84 0 0 1 52.91-37a8 8 0 0 1 10 10a88.08 88.08 0 0 0 109.8 109.8a8 8 0 0 1 10 10Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
16
components/icons/sun.tsx
Normal file
16
components/icons/sun.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default function PhSunBold(props: any) {
|
||||
return (
|
||||
<svg
|
||||
width="1.5em"
|
||||
height="1.5em"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M116 32V16a12 12 0 0 1 24 0v16a12 12 0 0 1-24 0Zm80 96a68 68 0 1 1-68-68a68.07 68.07 0 0 1 68 68Zm-24 0a44 44 0 1 0-44 44a44.05 44.05 0 0 0 44-44ZM51.51 68.49a12 12 0 1 0 17-17l-12-12a12 12 0 0 0-17 17Zm0 119l-12 12a12 12 0 0 0 17 17l12-12a12 12 0 1 0-17-17ZM196 72a12 12 0 0 0 8.49-3.51l12-12a12 12 0 0 0-17-17l-12 12A12 12 0 0 0 196 72Zm8.49 115.51a12 12 0 0 0-17 17l12 12a12 12 0 0 0 17-17ZM44 128a12 12 0 0 0-12-12H16a12 12 0 0 0 0 24h16a12 12 0 0 0 12-12Zm84 84a12 12 0 0 0-12 12v16a12 12 0 0 0 24 0v-16a12 12 0 0 0-12-12Zm112-96h-16a12 12 0 0 0 0 24h16a12 12 0 0 0 0-24Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
29
components/mdx/Aside.tsx
Normal file
29
components/mdx/Aside.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AsideProps {
|
||||
icon?: string;
|
||||
children?: React.ReactNode;
|
||||
type?: "default" | "warning" | "danger";
|
||||
}
|
||||
|
||||
export function Aside({
|
||||
children,
|
||||
icon,
|
||||
type = "default",
|
||||
...props
|
||||
}: AsideProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex border-5 py-3 px-4 ms-2 ms-md-0 my-10 rounded rounded-1 shadow",
|
||||
"bg-[#6edff633] border-[#6edff633] border-l-[#f6ef6e]"
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="rounded rounded-1 text-center h-8 w-8 bg-[#6edff6] text-2xl relative top-[-30px] left-[-30px]">
|
||||
{icon || "💡"}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
components/mdx/Callout.tsx
Normal file
27
components/mdx/Callout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CalloutProps {
|
||||
icon?: string;
|
||||
children?: React.ReactNode;
|
||||
type?: "default" | "warning" | "danger";
|
||||
}
|
||||
|
||||
export function Callout({
|
||||
children,
|
||||
icon,
|
||||
type = "default",
|
||||
...props
|
||||
}: CalloutProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("my-6 flex items-start rounded-md border border-l-4 p-4", {
|
||||
"border-red-900 bg-red-50": type === "danger",
|
||||
"border-yellow-900 bg-yellow-50": type === "warning",
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className="mr-4 text-2xl">{icon}</span>}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
components/mdx/MDXComponents.tsx
Normal file
125
components/mdx/MDXComponents.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Aside } from "@/components/mdx/Aside";
|
||||
import { Callout } from "@/components/mdx/Callout";
|
||||
import { MdxCard } from "@/components/mdx/MdxCard";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface HeadingProps {
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
className: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Heading: React.FC<HeadingProps> = ({ level, className, children }) => {
|
||||
const HeadingTag = `h${level}` as keyof React.ElementType;
|
||||
const headingId = children?.toString() ?? "";
|
||||
|
||||
return React.createElement(
|
||||
HeadingTag,
|
||||
{ id: headingId, className },
|
||||
children
|
||||
);
|
||||
};
|
||||
|
||||
interface MDXComponentsProps {
|
||||
[key: string]: React.FC<any>;
|
||||
}
|
||||
|
||||
const MDXComponents: MDXComponentsProps = {
|
||||
h1: (props) => (
|
||||
<Heading level={1} className="text-4xl font-bold mt-8 mb-6" {...props} />
|
||||
),
|
||||
h2: (props) => (
|
||||
<Heading
|
||||
level={2}
|
||||
className="text-3xl font-semibold mt-8 mb-6 border-b-2 border-gray-200 pb-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: (props) => (
|
||||
<Heading
|
||||
level={3}
|
||||
className="text-2xl font-semibold mt-6 mb-4"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h4: (props) => (
|
||||
<Heading level={4} className="text-xl font-semibold mt-6 mb-4" {...props} />
|
||||
),
|
||||
h5: (props) => (
|
||||
<Heading level={5} className="text-lg font-semibold mt-6 mb-4" {...props} />
|
||||
),
|
||||
h6: (props) => (
|
||||
<Heading
|
||||
level={6}
|
||||
className="text-base font-semibold mt-6 mb-4"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
hr: (props) => <hr className="border-t border-gray-200 my-8" {...props} />,
|
||||
p: (props) => (
|
||||
<p
|
||||
className="mt-6 mb-6 leading-relaxed text-gray-700 dark:text-gray-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
a: (props) => (
|
||||
<a
|
||||
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors underline underline-offset-4"
|
||||
target="_blank"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: (props) => <ul className="list-disc pl-6 mt-0 mb-6" {...props} />,
|
||||
ol: (props) => <ol className="list-decimal pl-6 mt-0 mb-6" {...props} />,
|
||||
li: (props) => (
|
||||
<li className="mb-3 text-gray-700 dark:text-gray-300" {...props} />
|
||||
),
|
||||
code: (props) => (
|
||||
<code
|
||||
className="bg-gray-100 dark:bg-gray-700 rounded px-2 py-1 font-mono text-sm"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
pre: (props) => (
|
||||
<pre
|
||||
className="rounded-lg p-4 overflow-x-auto my-4 bg-gray-100 dark:bg-gray-800"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
blockquote: (props) => (
|
||||
<blockquote
|
||||
className="pl-6 border-l-4 my-6 text-gray-600 dark:text-gray-400 italic"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
img: (props) => (
|
||||
<img className="rounded-lg border-2 border-gray-200 my-6" {...props} />
|
||||
),
|
||||
strong: (props) => <strong className="font-bold" {...props} />,
|
||||
table: (props) => (
|
||||
<div className="my-8 w-full overflow-x-auto">
|
||||
<table
|
||||
className="w-full shadow-sm rounded-lg overflow-hidden"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
tr: (props) => <tr className="border-t border-gray-200" {...props} />,
|
||||
th: (props) => (
|
||||
<th
|
||||
className="px-6 py-3 font-bold text-left bg-gray-100 dark:bg-gray-700 [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: (props) => (
|
||||
<td
|
||||
className="px-6 py-4 text-left border-t border-gray-100 dark:border-gray-700 [&[align=center]]:text-center [&[align=right]]:text-right"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
Aside,
|
||||
Callout,
|
||||
Card: MdxCard,
|
||||
};
|
||||
|
||||
export default MDXComponents;
|
||||
42
components/mdx/MdxCard.tsx
Normal file
42
components/mdx/MdxCard.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function MdxCard({
|
||||
href,
|
||||
className,
|
||||
children,
|
||||
disabled,
|
||||
...props
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative rounded-lg border p-6 shadow-md transition-shadow hover:shadow-lg",
|
||||
disabled && "cursor-not-allowed opacity-60",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-col justify-between space-y-4">
|
||||
<div className="space-y-2 [&>h3]:!mt-0 [&>h4]:!mt-0 [&>p]:text-muted-foreground">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{href && (
|
||||
<Link
|
||||
href={disabled ? "#" : href}
|
||||
className="absolute inset-0"
|
||||
prefetch={false}
|
||||
>
|
||||
<span className="sr-only">View</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
components/social-icons/icons.tsx
Normal file
114
components/social-icons/icons.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { SVGProps } from 'react'
|
||||
|
||||
// Icons taken from: https://simpleicons.org/
|
||||
// To add a new icon, add a new function here and add it to components in social-icons/index.tsx
|
||||
|
||||
export function Facebook(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Github(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Linkedin(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Mail(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" {...svgProps}>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Twitter(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function TwitterX(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 2H1l8.26 11.015L1.45 22H4.1l6.388-7.349L16 22h7l-8.608-11.478L21.8 2h-2.65l-5.986 6.886zm9 18L5 4h2l12 16z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Youtube(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M23.499 6.203a3.008 3.008 0 00-2.089-2.089c-1.87-.501-9.4-.501-9.4-.501s-7.509-.01-9.399.501a3.008 3.008 0 00-2.088 2.09A31.258 31.26 0 000 12.01a31.258 31.26 0 00.523 5.785 3.008 3.008 0 002.088 2.089c1.869.502 9.4.502 9.4.502s7.508 0 9.399-.502a3.008 3.008 0 002.089-2.09 31.258 31.26 0 00.5-5.784 31.258 31.26 0 00-.5-5.808zm-13.891 9.4V8.407l6.266 3.604z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Mastodon(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Threads(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Instagram(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function WeChat(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.85 8.14c.39 0 .77.03 1.14.08C16.31 5.25 13.19 3 9.44 3c-4.25 0-7.7 2.88-7.7 6.43c0 2.05 1.15 3.86 2.94 5.04L3.67 16.5l2.76-1.19c.59.21 1.21.38 1.87.47c-.09-.39-.14-.79-.14-1.21c-.01-3.54 3.44-6.43 7.69-6.43M12 5.89a.96.96 0 1 1 0 1.92a.96.96 0 0 1 0-1.92M6.87 7.82a.96.96 0 1 1 0-1.92a.96.96 0 0 1 0 1.92"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.26 14.57c0-2.84-2.87-5.14-6.41-5.14s-6.41 2.3-6.41 5.14s2.87 5.14 6.41 5.14c.58 0 1.14-.08 1.67-.2L20.98 21l-1.2-2.4c1.5-.94 2.48-2.38 2.48-4.03m-8.34-.32a.96.96 0 1 1 .96-.96c.01.53-.43.96-.96.96m3.85 0a.96.96 0 1 1 0-1.92a.96.96 0 0 1 0 1.92"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function JueJin(svgProps: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...svgProps}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12 14.316l7.454-5.88l-2.022-1.625L12 11.1l-.004.003l-5.432-4.288l-2.02 1.624l7.452 5.88Zm0-7.247l2.89-2.298L12 2.453l-.004-.005l-2.884 2.318l2.884 2.3Zm0 11.266l-.005.002l-9.975-7.87L0 12.088l.194.156l11.803 9.308l7.463-5.885L24 12.085l-2.023-1.624Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
62
components/social-icons/index.tsx
Normal file
62
components/social-icons/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
Facebook,
|
||||
Github,
|
||||
Instagram,
|
||||
JueJin,
|
||||
Linkedin,
|
||||
Mail,
|
||||
Mastodon,
|
||||
Threads,
|
||||
Twitter,
|
||||
TwitterX,
|
||||
WeChat,
|
||||
Youtube
|
||||
} from './icons'
|
||||
|
||||
const components = {
|
||||
mail: Mail,
|
||||
github: Github,
|
||||
facebook: Facebook,
|
||||
youtube: Youtube,
|
||||
linkedin: Linkedin,
|
||||
twitter: Twitter,
|
||||
twitterX: TwitterX,
|
||||
weChat: WeChat,
|
||||
jueJin: JueJin,
|
||||
mastodon: Mastodon,
|
||||
threads: Threads,
|
||||
instagram: Instagram
|
||||
}
|
||||
|
||||
type SocialIconProps = {
|
||||
kind: keyof typeof components
|
||||
href: string | undefined
|
||||
size?: number
|
||||
}
|
||||
|
||||
const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
|
||||
if (
|
||||
!href ||
|
||||
(kind === 'mail' &&
|
||||
!/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href))
|
||||
)
|
||||
return null
|
||||
|
||||
const SocialSvg = components[kind]
|
||||
|
||||
return (
|
||||
<a
|
||||
className="text-sm text-gray-500 transition hover:text-gray-600"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
>
|
||||
<span className="sr-only">{kind}</span>
|
||||
<SocialSvg
|
||||
className={`fill-current text-gray-400 hover:text-primary-500 dark:text-gray-200 dark:hover:text-primary-400 h-${size} w-${size}`}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default SocialIcon
|
||||
59
components/ui/alert.tsx
Normal file
59
components/ui/alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
57
components/ui/button.tsx
Normal file
57
components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
201
components/ui/dropdown-menu.tsx
Normal file
201
components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"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}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
159
components/ui/select.tsx
Normal file
159
components/ui/select.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
129
components/ui/toast.tsx
Normal file
129
components/ui/toast.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
35
components/ui/toaster.tsx
Normal file
35
components/ui/toaster.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
44
config/site.ts
Normal file
44
config/site.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { SiteConfig } from "@/types/siteConfig";
|
||||
|
||||
export const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://nextforge.dev";
|
||||
|
||||
export const SOURCE_CODE_URL = "https://github.com/weijunext/nextjs-starter";
|
||||
export const PRO_VERSION = "https://nexty.dev";
|
||||
|
||||
const TWITTER_URL = 'https://x.com/weijunext'
|
||||
const BSKY_URL = 'https://bsky.app/profile/judewei.bsky.social'
|
||||
const EMAIL_URL = 'mailto:weijunext@gmail.com'
|
||||
const GITHUB_URL = 'https://github.com/weijunext'
|
||||
const DISCORD_URL = process.env.NEXT_PUBLIC_DISCORD_INVITE_URL
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
name: "福安德外贸",
|
||||
tagLine: '福安德 外贸 门户网站',
|
||||
description:
|
||||
"福安德 外贸 门户网站,提供专业的外贸服务和解决方案。",
|
||||
url: BASE_URL,
|
||||
authors: [
|
||||
{
|
||||
name: "chao",
|
||||
url: "https://chao.com",
|
||||
}
|
||||
],
|
||||
creator: 'chao',
|
||||
socialLinks: {
|
||||
discord: DISCORD_URL,
|
||||
twitter: TWITTER_URL,
|
||||
github: GITHUB_URL,
|
||||
bluesky: BSKY_URL,
|
||||
email: EMAIL_URL
|
||||
},
|
||||
themeColors: [
|
||||
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: 'black' },
|
||||
],
|
||||
defaultNextTheme: 'system', // next-theme option: system | dark | light
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/logo.png",
|
||||
apple: "/logo.png", // apple-touch-icon.png
|
||||
},
|
||||
}
|
||||
99
content/about/en.mdx
Normal file
99
content/about/en.mdx
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: About
|
||||
description: About Next Forge - Multilingual Next.js 16 Starter
|
||||
lastUpdated: 2025-02-19
|
||||
---
|
||||
|
||||
> Update date: 2025-02-19
|
||||
|
||||
# About Next Forge
|
||||
|
||||
Next Forge is a feature-rich Next.js 16 multilingual starter template designed to help developers quickly build globally-ready websites. It comes with built-in i18n support, modern UI design, dark/light theme toggling, responsive layout, MDX blog system, SEO optimization, and integrated analytics tools.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
- 🌐 **Built-in i18n Support**: Supports English, Chinese, and Japanese out of the box, making it easy to create multilingual websites.
|
||||
- 🎨 **Modern UI Design**: Clean and modern UI powered by Tailwind CSS.
|
||||
- 🌙 **Dark/Light Theme Toggle**: Allows users to switch between dark and light themes effortlessly.
|
||||
- 📱 **Responsive Layout**: Fully responsive design ensures a great experience on both mobile and desktop devices.
|
||||
- 📝 **MDX Blog System**: Write blog posts using MDX for enhanced flexibility and power.
|
||||
- 🔍 **SEO Optimization**: Comprehensive SEO features including automatic sitemap.xml generation, robots.txt configuration, and optimized metadata.
|
||||
- 📊 **Analytics Integration**: Integrated with Google Analytics, Baidu Analytics, Google Adsense, and Vercel Analytics for easy tracking and insights.
|
||||
- 🌿 **Eco-Friendly Performance**: Achieved A+ rating on [Website Carbon](https://www.websitecarbon.com/website/nextforge-dev/), making it one of the most energy-efficient websites.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/weijunext/nextjs-starter.git
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Copy environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit [http://localhost:3000](http://localhost:3000) to view your application.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
1. **Basic Setup**:
|
||||
- Edit `config/site.ts` to configure website information.
|
||||
- Update icons and logo in the `public/` directory.
|
||||
- Configure `app/sitemap.ts` and `app/robots.ts`.
|
||||
|
||||
2. **i18n Setup**:
|
||||
- Add or modify language files in `i18n/messages/`.
|
||||
- Configure supported languages in `i18n/routing.ts`.
|
||||
- Set up i18n routing in `middleware.ts`.
|
||||
|
||||
## 📝 Content Management
|
||||
|
||||
### Blog Posts
|
||||
Create MDX files in `blog/[locale]` with the following format:
|
||||
````
|
||||
|
||||
---
|
||||
title: Post Title
|
||||
description: Post Description
|
||||
image: /image.png
|
||||
slug: /url-path
|
||||
tags: tag1,tag2
|
||||
date: 2025-02-20
|
||||
visible: published
|
||||
pin: true
|
||||
---
|
||||
|
||||
Post content...
|
||||
````
|
||||
|
||||
### Static Pages
|
||||
Manage static page content in `content/[page]/[locale].mdx`.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Next Forge is licensed under the MIT License, allowing you to freely use, modify, and distribute the template.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome issues and pull requests! Your contributions help us improve this project.
|
||||
|
||||
## About the Author
|
||||
|
||||
A Next.js full-stack specialist offering expert services in project development, performance optimization, and SEO improvement.
|
||||
|
||||
For consulting and training opportunities, reach out at weijunext@gmail.com
|
||||
|
||||
- [Github](https://github.com/weijunext)
|
||||
- [Bento](https://bento.me/weijunext)
|
||||
- [Twitter/X](https://twitter.com/judewei_dev)
|
||||
99
content/about/ja.mdx
Normal file
99
content/about/ja.mdx
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: サイトについて
|
||||
description: Next Forge - 多言語対応 Next.js 16 スターターについて
|
||||
lastUpdated: 2025-02-19
|
||||
---
|
||||
|
||||
> 更新日: 2025-02-19
|
||||
|
||||
# Next Forge について
|
||||
|
||||
Next Forge は、グローバル対応のウェブサイトを素早く構築するための、機能豊富な Next.js 16 多言語スターターテンプレートです。多言語対応、モダンな UI デザイン、ダーク/ライトテーマ切り替え、レスポンシブデザイン、MDX ブログシステム、SEO 最適化、アナリティクス統合など、多くの機能を備えています。
|
||||
|
||||
## ✨ 主な機能
|
||||
|
||||
- 🌐 **多言語対応**: 英語、中国語、日本語をサポートし、多言語サイトを簡単に構築できます。
|
||||
- 🎨 **モダンな UI デザイン**: Tailwind CSS を利用したクリーンでモダンな UI デザイン。
|
||||
- 🌙 **ダーク/ライトテーマ切り替え**: ユーザーが簡単にダークモードとライトモードを切り替えられる機能。
|
||||
- 📱 **レスポンシブデザイン**: モバイルとデスクトップの両方で最適なユーザー体験を提供します。
|
||||
- 📝 **MDX ブログシステム**: MDX を使用してブログ記事を執筆し、柔軟性とパワーを兼ね備えています。
|
||||
- 🔍 **SEO 最適化**: sitemap.xml の自動生成、robots.txt の設定、最適化されたメタデータなど、包括的な SEO 機能を搭載。
|
||||
- 📊 **アナリティクス統合**: Google Analytics、Baidu Analytics、Google Adsense、Vercel Analytics を統合し、簡単にデータを追跡できます。
|
||||
- 🌿 **エコフレンドリーなパフォーマンス**: [Website Carbon](https://www.websitecarbon.com/website/nextforge-dev/) で A+ 評価を獲得し、最もエネルギー効率の良いウェブサイトの一つです。
|
||||
|
||||
## 🚀 クイックスタート
|
||||
|
||||
1. リポジトリのクローン:
|
||||
```bash
|
||||
git clone https://github.com/weijunext/nextjs-starter.git
|
||||
```
|
||||
|
||||
2. 依存関係のインストール:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. 環境変数の設定:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. 開発サーバーの起動:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
[http://localhost:3000](http://localhost:3000) にアクセスしてアプリケーションを確認できます。
|
||||
|
||||
## ⚙️ 設定方法
|
||||
|
||||
1. **基本設定**:
|
||||
- `config/site.ts` を編集してウェブサイト情報を設定します。
|
||||
- `public/` ディレクトリ内のアイコンとロゴを更新します。
|
||||
- `app/sitemap.ts` と `app/robots.ts` を設定します。
|
||||
|
||||
2. **多言語設定**:
|
||||
- `i18n/messages/` 内の言語ファイルを追加または編集します。
|
||||
- `i18n/routing.ts` でサポートする言語を設定します。
|
||||
- `middleware.ts` で多言語ルーティングを設定します。
|
||||
|
||||
## 📝 コンテンツ管理
|
||||
|
||||
### ブログ投稿
|
||||
`blog/[locale]` ディレクトリに以下のフォーマットで MDX ファイルを作成します:
|
||||
````
|
||||
|
||||
---
|
||||
title: 投稿タイトル
|
||||
description: 投稿の説明
|
||||
image: /image.png
|
||||
slug: /url-path
|
||||
tags: tag1,tag2
|
||||
date: 2025-02-20
|
||||
visible: published
|
||||
pin: true
|
||||
---
|
||||
|
||||
投稿内容...
|
||||
````
|
||||
|
||||
### 静的ページ
|
||||
`content/[page]/[locale].mdx` で静的ページのコンテンツを管理します。
|
||||
|
||||
## 📄 ライセンス
|
||||
|
||||
Next Forge は MIT ライセンスで提供されており、自由に使用、変更、配布することができます。
|
||||
|
||||
## 🤝 コントリビューション
|
||||
|
||||
Issue や Pull Request を歓迎します!皆さんの貢献がこのプロジェクトをより良いものにします。
|
||||
|
||||
## 作者について
|
||||
|
||||
Next.js のフルスタックスペシャリストとして、プロジェクト開発、パフォーマンス最適化、SEO 改善のエキスパートサービスを提供しています。
|
||||
|
||||
コンサルティングやトレーニングの機会については、weijunext@gmail.com までご連絡ください。
|
||||
|
||||
- [Github](https://github.com/weijunext)
|
||||
- [Bento](https://bento.me/weijunext)
|
||||
- [Twitter/X](https://twitter.com/judewei_dev)
|
||||
99
content/about/zh.mdx
Normal file
99
content/about/zh.mdx
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: 关于
|
||||
description: 关于 Next Forge 多语言启动模板
|
||||
lastUpdated: 2025-02-19
|
||||
---
|
||||
|
||||
> 更新日期:2025-02-19
|
||||
|
||||
# 关于 Next Forge
|
||||
|
||||
Next Forge 是一个功能完备的 Next.js 16 多语言启动模板,旨在帮助开发者快速构建面向全球的网站。它内置了多语言支持、现代 UI 设计、深色/浅色主题切换、响应式布局、MDX 博客系统、SEO 优化以及多种统计分析工具。
|
||||
|
||||
## ✨ 主要特性
|
||||
|
||||
- 🌐 **多语言支持**:内置中文、英文、日语的国际化支持,轻松实现多语言网站。
|
||||
- 🎨 **现代 UI 设计**:基于 Tailwind CSS 的现代 UI 设计,简洁美观。
|
||||
- 🌙 **深色/浅色主题切换**:支持用户自由切换深色和浅色主题。
|
||||
- 📱 **响应式布局**:适配各种设备,确保在移动端和桌面端都有良好的用户体验。
|
||||
- 📝 **MDX 博客系统**:支持使用 MDX 编写博客文章,灵活且强大。
|
||||
- 🔍 **SEO 优化**:内置完整的 SEO 优化方案,包括自动生成 sitemap.xml 和 robots.txt。
|
||||
- 📊 **统计分析**:集成 Google Analytics、Baidu Analytics、Google Adsense 和 Vercel Analytics,方便进行数据追踪。
|
||||
- 🌿 **环保性能**:在 [Website Carbon](https://www.websitecarbon.com/website/nextforge-dev/) 上获得 A+ 评级,成为最节能的网站之一。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
1. 克隆项目:
|
||||
```bash
|
||||
git clone https://github.com/weijunext/nextjs-starter.git
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. 复制环境变量文件:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. 启动开发服务器:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
访问 [http://localhost:3000](http://localhost:3000) 查看你的应用。
|
||||
|
||||
## ⚙️ 配置
|
||||
|
||||
1. **基础配置**:
|
||||
- 修改 `config/site.ts` 配置网站信息。
|
||||
- 更新 `public/` 下的图标和 logo。
|
||||
- 配置 `app/sitemap.ts` 和 `app/robots.ts`。
|
||||
|
||||
2. **多语言配置**:
|
||||
- 在 `i18n/messages/` 下添加或修改语言文件。
|
||||
- 在 `i18n/routing.ts` 中配置支持的语言。
|
||||
- 在 `middleware.ts` 中配置多语言路由。
|
||||
|
||||
## 📝 内容管理
|
||||
|
||||
### 博客文章
|
||||
在 `blog/[locale]` 目录下创建 MDX 文件,支持以下格式:
|
||||
```markdown
|
||||
---
|
||||
title: 文章标题
|
||||
description: 文章描述
|
||||
image: /image.png
|
||||
slug: /url-path
|
||||
tags: tag1,tag2
|
||||
date: 2025-02-20
|
||||
visible: published
|
||||
pin: true
|
||||
---
|
||||
|
||||
文章内容...
|
||||
```
|
||||
|
||||
### 静态页面
|
||||
在 `content/[page]/[locale].mdx` 下管理静态页面内容。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
Next Forge 采用 MIT 许可证,您可以自由使用、修改和分发。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!您的贡献将帮助我们不断改进这个项目。
|
||||
|
||||
## 关于作者
|
||||
|
||||
专注于 Next.js 全栈开发,欢迎探讨开发、咨询与培训等合作机会,联系微信:bigye_chengpu
|
||||
|
||||
- [Github](https://github.com/weijunext)
|
||||
- [Twitter/X](https://twitter.com/weijunext)
|
||||
- [博客 - J实验室](https://weijunext.com)
|
||||
- [Medium](https://medium.com/@weijunext)
|
||||
- [掘金](https://juejin.cn/user/26044008768029)
|
||||
- [知乎](https://www.zhihu.com/people/mo-mo-mo-89-12-11)
|
||||
37
content/privacy-policy/en.mdx
Normal file
37
content/privacy-policy/en.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Privacy Policy
|
||||
description: How we collect and use your information
|
||||
lastUpdated: "2025-02-19"
|
||||
---
|
||||
|
||||
> Updated on 2025-02-19
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
This Privacy Policy describes how we collect and use your information.
|
||||
|
||||
## Information We Collect
|
||||
|
||||
We collect information that you provide directly to us:
|
||||
- Account information
|
||||
- Usage data
|
||||
- Communication preferences
|
||||
|
||||
## How We Use Your Information
|
||||
|
||||
We use the information we collect to:
|
||||
- Provide our services
|
||||
- Improve user experience
|
||||
- Send important updates
|
||||
|
||||
## Data Security
|
||||
|
||||
We implement appropriate security measures to protect your data.
|
||||
|
||||
## Contact Information
|
||||
|
||||
For privacy-related questions, please reach out to our team.
|
||||
|
||||
## Updates to This Policy
|
||||
|
||||
We may update this policy from time to time. Please review it periodically.
|
||||
37
content/privacy-policy/ja.mdx
Normal file
37
content/privacy-policy/ja.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: プライバシーポリシー
|
||||
description: お客様の情報の収集および利用方法について
|
||||
lastUpdated: "2025-02-19"
|
||||
---
|
||||
|
||||
> 更新日:2025年2月19日
|
||||
|
||||
# プライバシーポリシー
|
||||
|
||||
本プライバシーポリシーでは、当社がお客様の情報をどのように収集し、利用するかについて説明いたします。
|
||||
|
||||
## 収集する情報
|
||||
|
||||
当社は、お客様から直接提供いただく以下の情報を収集いたします:
|
||||
- アカウント情報
|
||||
- 利用データ
|
||||
- 通信設定
|
||||
|
||||
## 情報の利用目的
|
||||
|
||||
収集した情報は、以下の目的で利用させていただきます:
|
||||
- サービスの提供
|
||||
- ユーザー体験の向上
|
||||
- 重要なお知らせの送信
|
||||
|
||||
## データセキュリティ
|
||||
|
||||
お客様のデータを保護するため、適切なセキュリティ対策を実施しております。
|
||||
|
||||
## お問い合わせ先
|
||||
|
||||
プライバシーに関するご質問は、当社サポートチームまでお問い合わせください。
|
||||
|
||||
## ポリシーの更新
|
||||
|
||||
本ポリシーは随時更新される場合がございます。定期的にご確認いただきますようお願いいたします。
|
||||
37
content/privacy-policy/zh.mdx
Normal file
37
content/privacy-policy/zh.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: 隐私政策
|
||||
description: 我们如何收集和使用您的信息
|
||||
lastUpdated: "2025-02-19"
|
||||
---
|
||||
|
||||
> 更新日期:2025年2月19日
|
||||
|
||||
# 隐私政策
|
||||
|
||||
本隐私政策说明了我们如何收集和使用您的信息。
|
||||
|
||||
## 我们收集的信息
|
||||
|
||||
我们收集您直接提供给我们的信息:
|
||||
- 账户信息
|
||||
- 使用数据
|
||||
- 通信偏好设置
|
||||
|
||||
## 我们如何使用您的信息
|
||||
|
||||
我们将收集的信息用于:
|
||||
- 提供服务
|
||||
- 改善用户体验
|
||||
- 发送重要更新
|
||||
|
||||
## 数据安全
|
||||
|
||||
我们采取适当的安全措施来保护您的数据。
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有隐私相关问题,请联系我们的团队。
|
||||
|
||||
## 政策更新
|
||||
|
||||
我们可能会不时更新本政策。请定期查看。
|
||||
32
content/terms-of-service/en.mdx
Normal file
32
content/terms-of-service/en.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Terms of Service
|
||||
description: Terms and conditions for using our service
|
||||
lastUpdated: "2025-02-19"
|
||||
---
|
||||
|
||||
> Updated on 2025-02-19
|
||||
|
||||
# Terms of Service
|
||||
|
||||
Welcome to our service. By using our website, you agree to these terms.
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By accessing and using this website, you accept and agree to be bound by the terms and conditions of this agreement.
|
||||
|
||||
## 2. User Responsibilities
|
||||
|
||||
You agree to:
|
||||
- Provide accurate information
|
||||
- Maintain the security of your account
|
||||
- Comply with all applicable laws
|
||||
|
||||
## 3. Service Changes
|
||||
|
||||
We reserve the right to:
|
||||
- Modify or discontinue services
|
||||
- Update these terms at any time
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have any questions about these terms, please contact us.
|
||||
32
content/terms-of-service/ja.mdx
Normal file
32
content/terms-of-service/ja.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: 利用規約
|
||||
description: サービス利用に関する規約と条件
|
||||
lastUpdated: "2025-02-19"
|
||||
---
|
||||
|
||||
> 更新日:2025年2月19日
|
||||
|
||||
# 利用規約
|
||||
|
||||
当社のサービスをご利用いただき、ありがとうございます。本ウェブサイトをご利用になる際は、以下の利用規約に同意いただいたものとみなします。
|
||||
|
||||
## 1. 規約の同意
|
||||
|
||||
本ウェブサイトにアクセスし利用することにより、お客様は本規約の条件に同意し、拘束されることを承諾するものとします。
|
||||
|
||||
## 2. ユーザーの責任
|
||||
|
||||
お客様には以下の事項に同意していただきます:
|
||||
- 正確な情報の提供
|
||||
- アカウントの安全管理
|
||||
- 適用される法令の遵守
|
||||
|
||||
## 3. サービスの変更
|
||||
|
||||
当社は以下の権利を留保します:
|
||||
- サービスの変更または終了
|
||||
- 本規約の随時更新
|
||||
|
||||
## お問い合わせ
|
||||
|
||||
本規約に関するご質問がございましたら、お気軽にお問い合わせください。
|
||||
32
content/terms-of-service/zh.mdx
Normal file
32
content/terms-of-service/zh.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: 服务条款
|
||||
description: 使用我们服务的条款和条件
|
||||
lastUpdated: "2025-02-19"
|
||||
---
|
||||
|
||||
> 更新日期:2025年2月19日
|
||||
|
||||
# 服务条款
|
||||
|
||||
欢迎使用我们的服务。使用本网站即表示您同意以下条款。
|
||||
|
||||
## 1. 条款接受
|
||||
|
||||
访问和使用本网站即表示您接受并同意受本协议条款和条件的约束。
|
||||
|
||||
## 2. 用户责任
|
||||
|
||||
您同意:
|
||||
- 提供准确信息
|
||||
- 维护账户安全
|
||||
- 遵守所有适用法律
|
||||
|
||||
## 3. 服务变更
|
||||
|
||||
我们保留以下权利:
|
||||
- 修改或终止服务
|
||||
- 随时更新这些条款
|
||||
|
||||
## 联系我们
|
||||
|
||||
如果您对这些条款有任何疑问,请与我们联系。
|
||||
15
gtag.js
Normal file
15
gtag.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GOOGLE_ID || null;
|
||||
|
||||
export const pageview = (url) => {
|
||||
window.gtag("config", GA_TRACKING_ID, {
|
||||
page_path: url,
|
||||
});
|
||||
};
|
||||
|
||||
export const event = ({ action, category, label, value }) => {
|
||||
window.gtag("event", action, {
|
||||
event_category: category,
|
||||
event_label: label,
|
||||
value: value,
|
||||
});
|
||||
};
|
||||
194
hooks/use-toast.ts
Normal file
194
hooks/use-toast.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
139
i18n/messages/en.json
Normal file
139
i18n/messages/en.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"LanguageDetection": {
|
||||
"title": "Language Suggestion",
|
||||
"description": "We noticed your browser language differs from the current site language. You can switch languages anytime.",
|
||||
"countdown": "Closing in {countdown} seconds",
|
||||
"switchTo": "Switch to"
|
||||
},
|
||||
"Header": {
|
||||
"links": [
|
||||
{
|
||||
"name": "Blog",
|
||||
"href": "/blog"
|
||||
},
|
||||
{
|
||||
"name": "About",
|
||||
"href": "/about"
|
||||
},
|
||||
{
|
||||
"name": "Source Code",
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer nofollow"
|
||||
},
|
||||
{
|
||||
"name": "Pro Version",
|
||||
"href": "https://nexty.dev",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Footer": {
|
||||
"Copyright": "Copyright © {year} {name} All rights reserved.",
|
||||
"PrivacyPolicy": "Privacy Policy",
|
||||
"TermsOfService": "Terms of Service",
|
||||
"Links": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Languages",
|
||||
"links": [
|
||||
{
|
||||
"href": "/en",
|
||||
"name": "English",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/zh",
|
||||
"name": "中文",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/ja",
|
||||
"name": "日本語",
|
||||
"useA": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Open Source",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"name": "Next Forge",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/landing-page-boilerplate",
|
||||
"name": "Landing Page Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/weekly-boilerplate",
|
||||
"name": "Blog Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Other Products",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://nexty.dev/",
|
||||
"name": "Nexty - SaaS Template",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://ogimage.click/",
|
||||
"name": "OG Image Generator",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://dofollow.tools/",
|
||||
"name": "Dofollow.Tools",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Newsletter": {
|
||||
"title": "Subscribe to our newsletter",
|
||||
"description": "Get the latest news and updates from Next Forge",
|
||||
"defaultErrorMessage": "Please enter a valid email address",
|
||||
"successMessage": "Subscription successful",
|
||||
"errorMessage": "Subscription failed",
|
||||
"errorMessage2": "Subscription failed. Please try again later.",
|
||||
"subscribe": "Subscribe",
|
||||
"subscribing": "Subscribing...",
|
||||
"subscribed": "Subscription successful! Thank you for your attention."
|
||||
}
|
||||
},
|
||||
"Home": {
|
||||
"title": "Next Forge",
|
||||
"tagLine": "Multilingual Next.js 16 Starter",
|
||||
"description": "A multilingual Next.js 16 starter with built-in i18n support. Launch your global-ready web application with a clean, efficient, and SEO-friendly foundation."
|
||||
},
|
||||
"Blog": {
|
||||
"title": "Blog Posts",
|
||||
"description": "A list of blog posts"
|
||||
},
|
||||
"About": {
|
||||
"title": "About",
|
||||
"description": "About the site"
|
||||
},
|
||||
"TermsOfService": {
|
||||
"title": "Terms of Service",
|
||||
"description": "Terms of Service"
|
||||
},
|
||||
"PrivacyPolicy": {
|
||||
"title": "Privacy Policy",
|
||||
"description": "Privacy Policy"
|
||||
}
|
||||
}
|
||||
139
i18n/messages/ja.json
Normal file
139
i18n/messages/ja.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"LanguageDetection": {
|
||||
"title": "言語の提案",
|
||||
"description": "ブラウザの言語設定が現在のサイト言語と異なっています。いつでも言語を切り替えることができます。",
|
||||
"countdown": "閉じるまで {countdown} 秒",
|
||||
"switchTo": "に切り替える"
|
||||
},
|
||||
"Header": {
|
||||
"links": [
|
||||
{
|
||||
"name": "ブログ",
|
||||
"href": "/blog"
|
||||
},
|
||||
{
|
||||
"name": "サイトについて",
|
||||
"href": "/about"
|
||||
},
|
||||
{
|
||||
"name": "Source Code",
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer nofollow"
|
||||
},
|
||||
{
|
||||
"name": "Pro Version",
|
||||
"href": "https://nexty.dev",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Footer": {
|
||||
"Copyright": "Copyright © {year} {name} All rights reserved.",
|
||||
"PrivacyPolicy": "プライバシーポリシー",
|
||||
"TermsOfService": "利用規約",
|
||||
"Links": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "言語",
|
||||
"links": [
|
||||
{
|
||||
"href": "/en",
|
||||
"name": "English",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/zh",
|
||||
"name": "中文",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/ja",
|
||||
"name": "日本語",
|
||||
"useA": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "オープンソースプロジェクト",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"name": "Next Forge",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/landing-page-boilerplate",
|
||||
"name": "Landing Page Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/weekly-boilerplate",
|
||||
"name": "Blog Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "その他の製品",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://nexty.dev/",
|
||||
"name": "Nexty - SaaS Template",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://ogimage.click/",
|
||||
"name": "OG Image Generator",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://dofollow.tools/",
|
||||
"name": "Dofollow.Tools",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Newsletter": {
|
||||
"title": "Next Forgeのニュースレターに登録する",
|
||||
"description": "Next Forgeの最新のニュースと更新情報を入手します。",
|
||||
"defaultErrorMessage": "有効なメールアドレスを入力してください。",
|
||||
"successMessage": "登録が完了しました。",
|
||||
"errorMessage": "登録に失敗しました。",
|
||||
"errorMessage2": "登録に失敗しました。少し待ってから再試行してください。",
|
||||
"subscribe": "登録する",
|
||||
"subscribing": "登録中...",
|
||||
"subscribed": "登録が完了しました。ありがとうございます。"
|
||||
}
|
||||
},
|
||||
"Home": {
|
||||
"title": "Next Forge",
|
||||
"tagLine": "Next.js 多言語スターターテンプレート",
|
||||
"description": "多言語対応機能を搭載したNext.js 15スターターテンプレート。SEO最適化済みで、グローバル展開可能なウェブアプリを効率的に構築できます。"
|
||||
},
|
||||
"Blog": {
|
||||
"title": "ブログ一覧",
|
||||
"description": "ブログ投稿の一覧"
|
||||
},
|
||||
"About": {
|
||||
"title": "サイトについて",
|
||||
"description": "サイトについて"
|
||||
},
|
||||
"TermsOfService": {
|
||||
"title": "サービス利用規約",
|
||||
"description": "サービス利用規約"
|
||||
},
|
||||
"PrivacyPolicy": {
|
||||
"title": "プライバシーポリシー",
|
||||
"description": "プライバシーポリシー"
|
||||
}
|
||||
}
|
||||
144
i18n/messages/vi.json
Normal file
144
i18n/messages/vi.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"LanguageDetection": {
|
||||
"title": "Gợi ý ngôn ngữ",
|
||||
"description": "Phát hiện ngôn ngữ trình duyệt của bạn khác với ngôn ngữ hiện tại, bạn có thể chuyển đổi mọi lúc.",
|
||||
"countdown": "Sẽ đóng sau {countdown} giây",
|
||||
"switchTo": "Chuyển sang"
|
||||
},
|
||||
"Header": {
|
||||
"links": [
|
||||
{
|
||||
"name": "Blog",
|
||||
"href": "/blog"
|
||||
},
|
||||
{
|
||||
"name": "Về chúng tôi",
|
||||
"href": "/about"
|
||||
},
|
||||
{
|
||||
"name": "Mã nguồn",
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer nofollow"
|
||||
},
|
||||
{
|
||||
"name": "Phiên bản cao cấp",
|
||||
"href": "https://nexty.dev",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Footer": {
|
||||
"Copyright": "Bản quyền © {year} {name} mọi quyền được bảo lưu.",
|
||||
"PrivacyPolicy": "Chính sách bảo mật",
|
||||
"TermsOfService": "Điều khoản dịch vụ",
|
||||
"Links": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Ngôn ngữ",
|
||||
"links": [
|
||||
{
|
||||
"href": "/en",
|
||||
"name": "English",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/zh",
|
||||
"name": "Tiếng Trung",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/ja",
|
||||
"name": "Tiếng Nhật",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/vi",
|
||||
"name": "Tiếng Việt",
|
||||
"useA": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Dự án mã nguồn mở",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"name": "Next Forge",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/landing-page-boilerplate",
|
||||
"name": "Landing Page Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/weekly-boilerplate",
|
||||
"name": "Blog Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Sản phẩm khác",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://nexty.dev/",
|
||||
"name": "Nexty - Mẫu SaaS",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://ogimage.click/",
|
||||
"name": "Trình tạo ảnh OG",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://dofollow.tools/",
|
||||
"name": "Dofollow.Tools",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Newsletter": {
|
||||
"title": "Đăng ký nhận email của chúng tôi",
|
||||
"description": "Nhận tin tức và hướng dẫn Next.js mới nhất",
|
||||
"defaultErrorMessage": "Vui lòng nhập địa chỉ email hợp lệ",
|
||||
"successMessage": "Đăng ký thành công",
|
||||
"errorMessage": "Đăng ký thất bại",
|
||||
"errorMessage2": "Đăng ký thất bại, vui lòng thử lại sau",
|
||||
"subscribe": "Đăng ký",
|
||||
"subscribing": "Đang đăng ký...",
|
||||
"subscribed": "Đăng ký thành công! Cảm ơn bạn đã quan tâm."
|
||||
}
|
||||
},
|
||||
"Home": {
|
||||
"title": "Next Forge",
|
||||
"tagLine": "Mẫu khởi động đa ngôn ngữ Next.js",
|
||||
"description": "Mẫu khởi động Next.js 16 tích hợp sẵn hỗ trợ đa ngôn ngữ, giúp bạn nhanh chóng xây dựng trang web ra thị trường quốc tế. Rõ ràng, hiệu quả, sẵn sàng sử dụng với cơ sở kiến trúc SEO hoàn toàn tối ưu."
|
||||
},
|
||||
"Blog": {
|
||||
"title": "Danh sách blog",
|
||||
"description": "Danh sách các bài viết blog"
|
||||
},
|
||||
"About": {
|
||||
"title": "Về chúng tôi",
|
||||
"description": "Thông tin về trang web"
|
||||
},
|
||||
"TermsOfService": {
|
||||
"title": "Điều khoản dịch vụ",
|
||||
"description": "Điều khoản sử dụng dịch vụ"
|
||||
},
|
||||
"PrivacyPolicy": {
|
||||
"title": "Chính sách bảo mật",
|
||||
"description": "Chính sách bảo mật thông tin cá nhân"
|
||||
}
|
||||
}
|
||||
139
i18n/messages/zh.json
Normal file
139
i18n/messages/zh.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"LanguageDetection": {
|
||||
"title": "语言建议",
|
||||
"description": "检测到你的浏览器语言和当前语言不一样,你随时都可切换语言。",
|
||||
"countdown": "将在 {countdown} 秒后关闭",
|
||||
"switchTo": "切换到"
|
||||
},
|
||||
"Header": {
|
||||
"links": [
|
||||
{
|
||||
"name": "博客",
|
||||
"href": "/blog"
|
||||
},
|
||||
{
|
||||
"name": "关于",
|
||||
"href": "/about"
|
||||
},
|
||||
{
|
||||
"name": "源码",
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer nofollow"
|
||||
},
|
||||
{
|
||||
"name": "高级版",
|
||||
"href": "https://nexty.dev",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Footer": {
|
||||
"Copyright": "版权所有 © {year} {name} 保留所有权利。",
|
||||
"PrivacyPolicy": "隐私政策",
|
||||
"TermsOfService": "服务条款",
|
||||
"Links": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "语言",
|
||||
"links": [
|
||||
{
|
||||
"href": "/en",
|
||||
"name": "English",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/zh",
|
||||
"name": "中文",
|
||||
"useA": true
|
||||
},
|
||||
{
|
||||
"href": "/ja",
|
||||
"name": "日本語",
|
||||
"useA": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "开源项目",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://github.com/weijunext/nextjs-starter",
|
||||
"name": "Next Forge",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/landing-page-boilerplate",
|
||||
"name": "Landing Page Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://github.com/weijunext/weekly-boilerplate",
|
||||
"name": "Blog Boilerplate",
|
||||
"rel": "noopener noreferrer nofollow",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "其他产品",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://nexty.dev/",
|
||||
"name": "Nexty - SaaS Template",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://ogimage.click/",
|
||||
"name": "OG Image Generator",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
},
|
||||
{
|
||||
"href": "https://dofollow.tools/",
|
||||
"name": "Dofollow.Tools",
|
||||
"rel": "noopener noreferrer",
|
||||
"target": "_blank"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Newsletter": {
|
||||
"title": "订阅我们的邮件",
|
||||
"description": "获取最新的 Next.js 资讯和教程",
|
||||
"defaultErrorMessage": "请输入有效的邮箱地址",
|
||||
"successMessage": "订阅成功",
|
||||
"errorMessage": "订阅失败",
|
||||
"errorMessage2": "订阅失败,请稍后再试",
|
||||
"subscribe": "订阅",
|
||||
"subscribing": "订阅中",
|
||||
"subscribed": "订阅成功!感谢您的关注。"
|
||||
}
|
||||
},
|
||||
"Home": {
|
||||
"title": "Next Forge",
|
||||
"tagLine": "Next.js 多语言启动模板",
|
||||
"description": "内置多语言支持的 Next.js 16 启动模板,助您快速构建面向全球的出海网站。简洁高效,开箱即用,完全优化的SEO基础架构。"
|
||||
},
|
||||
"Blog": {
|
||||
"title": "博客列表",
|
||||
"description": "博客列表"
|
||||
},
|
||||
"About": {
|
||||
"title": "关于",
|
||||
"description": "关于网站"
|
||||
},
|
||||
"TermsOfService": {
|
||||
"title": "服务条款",
|
||||
"description": "服务条款"
|
||||
},
|
||||
"PrivacyPolicy": {
|
||||
"title": "隐私政策",
|
||||
"description": "隐私政策"
|
||||
}
|
||||
}
|
||||
32
i18n/request.ts
Normal file
32
i18n/request.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { routing } from './routing';
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// This typically corresponds to the `[locale]` segment
|
||||
let locale = await requestLocale;
|
||||
|
||||
if (locale?.startsWith('zh')) {
|
||||
locale = 'zh';
|
||||
} else if (locale?.startsWith('ja')) {
|
||||
locale = 'ja';
|
||||
} else if (locale?.startsWith('en')) {
|
||||
locale = 'en';
|
||||
} else if (locale?.startsWith('vi')) {
|
||||
locale = 'vi';
|
||||
} else {
|
||||
locale = 'zh';
|
||||
}
|
||||
|
||||
// Ensure that a valid locale is used
|
||||
if (!locale || !routing.locales.includes(locale as any)) {
|
||||
return {
|
||||
locale: routing.defaultLocale,
|
||||
messages: (await import(`./messages/${routing.defaultLocale}.json`)).default
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`./messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
37
i18n/routing.ts
Normal file
37
i18n/routing.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createNavigation } from 'next-intl/navigation';
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
|
||||
export const LOCALES = ['en', 'zh', 'ja', 'vi']
|
||||
export const DEFAULT_LOCALE = 'zh'
|
||||
export const LOCALE_NAMES: Record<string, string> = {
|
||||
'en': "English",
|
||||
'zh': "中文",
|
||||
'ja': "日本語",
|
||||
'vi': "Tiếng Việt",
|
||||
};
|
||||
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: LOCALES,
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: DEFAULT_LOCALE,
|
||||
|
||||
// auto detect locale
|
||||
localeDetection: process.env.NEXT_PUBLIC_LOCALE_DETECTION === 'true',
|
||||
|
||||
localePrefix: 'always',
|
||||
});
|
||||
|
||||
// Lightweight wrappers around Next.js' navigation APIs
|
||||
// that will consider the routing configuration
|
||||
export const {
|
||||
Link,
|
||||
redirect,
|
||||
usePathname,
|
||||
useRouter,
|
||||
getPathname,
|
||||
} = createNavigation(routing);
|
||||
|
||||
|
||||
export type Locale = (typeof routing.locales)[number];
|
||||
91
lib/email.ts
Normal file
91
lib/email.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
const DISPOSABLE_EMAIL_DOMAINS = [
|
||||
'tempmail.com',
|
||||
'throwawaymail.com',
|
||||
'tempmail100.com'
|
||||
];
|
||||
|
||||
export type EmailValidationError =
|
||||
| 'invalid_email_format'
|
||||
| 'email_part_too_long'
|
||||
| 'disposable_email_not_allowed'
|
||||
| 'invalid_characters';
|
||||
|
||||
const EMAIL_REGEX = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/;
|
||||
|
||||
export function validateEmail(email: string): {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
} {
|
||||
// validate email format
|
||||
if (!EMAIL_REGEX.test(email)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'invalid_email_format'
|
||||
};
|
||||
}
|
||||
|
||||
// check domain length
|
||||
const [localPart, domain] = email.split('@');
|
||||
if (domain.length > 255 || localPart.length > 64) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'email_part_too_long'
|
||||
};
|
||||
}
|
||||
|
||||
// check if it's a disposable email
|
||||
if (DISPOSABLE_EMAIL_DOMAINS.includes(domain.toLowerCase())) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'disposable_email_not_allowed'
|
||||
};
|
||||
}
|
||||
|
||||
// check for special characters
|
||||
if (/[<>()[\]\\.,;:\s@"]+/.test(localPart)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'invalid_characters'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
// email validation (including alias detection)
|
||||
export function normalizeEmail(email: string): string {
|
||||
if (!email) return '';
|
||||
|
||||
// convert to lowercase
|
||||
let normalizedEmail = email.toLowerCase();
|
||||
|
||||
// separate email local part and domain part
|
||||
const [localPart, domain] = normalizedEmail.split('@');
|
||||
|
||||
// handle different email service provider alias rules
|
||||
switch (domain) {
|
||||
case 'gmail.com':
|
||||
// remove dot and + suffix
|
||||
const gmailBase = localPart
|
||||
.replace(/\./g, '')
|
||||
.split('+')[0];
|
||||
return `${gmailBase}@${domain}`;
|
||||
|
||||
case 'outlook.com':
|
||||
case 'hotmail.com':
|
||||
case 'live.com':
|
||||
// remove + suffix
|
||||
const microsoftBase = localPart.split('+')[0];
|
||||
return `${microsoftBase}@${domain}`;
|
||||
|
||||
case 'yahoo.com':
|
||||
// remove - suffix
|
||||
const yahooBase = localPart.split('-')[0];
|
||||
return `${yahooBase}@${domain}`;
|
||||
|
||||
default:
|
||||
// for other emails, only remove + suffix
|
||||
const baseLocalPart = localPart.split('+')[0];
|
||||
return `${baseLocalPart}@${domain}`;
|
||||
}
|
||||
}
|
||||
66
lib/getBlogs.ts
Normal file
66
lib/getBlogs.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { DEFAULT_LOCALE } from '@/i18n/routing';
|
||||
import { BlogPost } from '@/types/blog';
|
||||
import fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import path from 'path';
|
||||
|
||||
const POSTS_BATCH_SIZE = 10;
|
||||
|
||||
export async function getPosts(locale: string = DEFAULT_LOCALE): Promise<{ posts: BlogPost[] }> {
|
||||
const postsDirectory = path.join(process.cwd(), 'blogs', locale);
|
||||
|
||||
// is directory exist
|
||||
if (!fs.existsSync(postsDirectory)) {
|
||||
return { posts: [] };
|
||||
}
|
||||
|
||||
let filenames = await fs.promises.readdir(postsDirectory);
|
||||
filenames = filenames.reverse();
|
||||
|
||||
let allPosts: BlogPost[] = [];
|
||||
|
||||
// read file by batch
|
||||
for (let i = 0; i < filenames.length; i += POSTS_BATCH_SIZE) {
|
||||
const batchFilenames = filenames.slice(i, i + POSTS_BATCH_SIZE);
|
||||
|
||||
const batchPosts: BlogPost[] = await Promise.all(
|
||||
batchFilenames.map(async (filename) => {
|
||||
const fullPath = path.join(postsDirectory, filename);
|
||||
const fileContents = await fs.promises.readFile(fullPath, 'utf8');
|
||||
|
||||
const { data, content } = matter(fileContents);
|
||||
|
||||
return {
|
||||
locale, // use locale parameter
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
image: data.image || '',
|
||||
slug: data.slug,
|
||||
tags: data.tags,
|
||||
date: data.date,
|
||||
visible: data.visible || 'published',
|
||||
pin: data.pin || false,
|
||||
content,
|
||||
metadata: data,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
allPosts.push(...batchPosts);
|
||||
}
|
||||
|
||||
// filter out non-published articles
|
||||
allPosts = allPosts.filter(post => post.visible === 'published');
|
||||
|
||||
// sort posts by pin and date
|
||||
allPosts = allPosts.sort((a, b) => {
|
||||
if (a.pin !== b.pin) {
|
||||
return (b.pin ? 1 : 0) - (a.pin ? 1 : 0);
|
||||
}
|
||||
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||
});
|
||||
|
||||
return {
|
||||
posts: allPosts,
|
||||
};
|
||||
}
|
||||
40
lib/logger.ts
Normal file
40
lib/logger.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as fs from 'fs';
|
||||
import * as winston from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
|
||||
const logDir: string = process.env.LOG_DIR || 'log';
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileTransport = new winston.transports.DailyRotateFile({
|
||||
filename: `${logDir}/%DATE%-results.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '3d',
|
||||
level: 'info',
|
||||
});
|
||||
|
||||
const logger: winston.Logger = winston.createLogger({
|
||||
level: 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss'
|
||||
}),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
fileTransport,
|
||||
new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export default logger;
|
||||
100
lib/metadata.ts
Normal file
100
lib/metadata.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { DEFAULT_LOCALE, LOCALE_NAMES, Locale } from '@/i18n/routing'
|
||||
import { Metadata } from 'next'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
type MetadataProps = {
|
||||
page?: string
|
||||
title?: string
|
||||
description?: string
|
||||
images?: string[]
|
||||
noIndex?: boolean
|
||||
locale: Locale
|
||||
path?: string
|
||||
canonicalUrl?: string
|
||||
}
|
||||
|
||||
export async function constructMetadata({
|
||||
page = 'Home',
|
||||
title,
|
||||
description,
|
||||
images = [],
|
||||
noIndex = false,
|
||||
locale,
|
||||
path,
|
||||
canonicalUrl,
|
||||
}: MetadataProps): Promise<Metadata> {
|
||||
// get translations
|
||||
const t = await getTranslations({ locale, namespace: 'Home' })
|
||||
|
||||
// get page specific metadata translations
|
||||
const pageTitle = title || t(`title`)
|
||||
const pageDescription = description || t(`description`)
|
||||
|
||||
// build full title
|
||||
const finalTitle = page === 'Home'
|
||||
? `${pageTitle} - ${t('tagLine')}`
|
||||
: `${pageTitle} | ${t('title')}`
|
||||
|
||||
// build image URLs
|
||||
const imageUrls = images.length > 0
|
||||
? images.map(img => ({
|
||||
url: img.startsWith('http') ? img : `${siteConfig.url}/${img}`,
|
||||
alt: pageTitle,
|
||||
}))
|
||||
: [{
|
||||
url: `${siteConfig.url}/og.png`,
|
||||
alt: pageTitle,
|
||||
}]
|
||||
|
||||
// Open Graph Site
|
||||
const pageURL = `${locale === DEFAULT_LOCALE ? '' : `/${locale}`}${path}` || siteConfig.url
|
||||
|
||||
// build alternate language links
|
||||
const alternateLanguages = Object.keys(LOCALE_NAMES).reduce((acc, lang) => {
|
||||
const path = canonicalUrl
|
||||
? `${lang === DEFAULT_LOCALE ? '' : `/${lang}`}${canonicalUrl === '/' ? '' : canonicalUrl}`
|
||||
: `${lang === DEFAULT_LOCALE ? '' : `/${lang}`}`
|
||||
acc[lang] = `${siteConfig.url}${path}`
|
||||
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
return {
|
||||
title: finalTitle,
|
||||
description: pageDescription,
|
||||
keywords: [],
|
||||
authors: siteConfig.authors,
|
||||
creator: siteConfig.creator,
|
||||
metadataBase: new URL(siteConfig.url),
|
||||
alternates: {
|
||||
canonical: canonicalUrl ? `${siteConfig.url}${locale === DEFAULT_LOCALE ? '' : `/${locale}`}${canonicalUrl === '/' ? '' : canonicalUrl}` : undefined,
|
||||
languages: alternateLanguages,
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
title: finalTitle,
|
||||
description: pageDescription,
|
||||
url: pageURL,
|
||||
siteName: t('title'),
|
||||
locale: locale,
|
||||
images: imageUrls,
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: finalTitle,
|
||||
description: pageDescription,
|
||||
site: `${siteConfig.url}${pageURL === '/' ? '' : pageURL}`,
|
||||
images: imageUrls,
|
||||
creator: siteConfig.creator,
|
||||
},
|
||||
robots: {
|
||||
index: !noIndex,
|
||||
follow: !noIndex,
|
||||
googleBot: {
|
||||
index: !noIndex,
|
||||
follow: !noIndex,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
19
lib/utils.ts
Normal file
19
lib/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const getDomain = (url: string) => {
|
||||
try {
|
||||
// Add https:// protocol if not present
|
||||
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
|
||||
const domain = new URL(urlWithProtocol).hostname;
|
||||
// Remove 'www.' prefix if exists
|
||||
return domain.replace(/^www\./, '');
|
||||
} catch (error) {
|
||||
// Return original input if URL parsing fails
|
||||
return url;
|
||||
}
|
||||
};
|
||||
15
log/.db69a462fcd0bc6ae6d0cc5220e6aa21ec198c81-audit.json
Normal file
15
log/.db69a462fcd0bc6ae6d0cc5220e6aa21ec198c81-audit.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 3
|
||||
},
|
||||
"auditLog": "log/.db69a462fcd0bc6ae6d0cc5220e6aa21ec198c81-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1733723462799,
|
||||
"name": "log/2024-12-09-results.log",
|
||||
"hash": "05d583fcbceaa268a024a210f7aee5ba516a76a6faaaea634dd5caab904bd55c"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
29
next.config.mjs
Normal file
29
next.config.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// output: "export",
|
||||
images: {
|
||||
remotePatterns: [
|
||||
...(process.env.R2_PUBLIC_URL
|
||||
? [
|
||||
{
|
||||
hostname: process.env.R2_PUBLIC_URL.replace("https://", ""),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
compiler: {
|
||||
removeConsole:
|
||||
process.env.NODE_ENV === "production"
|
||||
? {
|
||||
exclude: ["error"],
|
||||
}
|
||||
: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
61
package.json
Normal file
61
package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "next-forge",
|
||||
"version": "2.1.0",
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"engines": {
|
||||
"node": "20.x"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@upstash/ratelimit": "^2.0.5",
|
||||
"@upstash/redis": "^1.34.5",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.468.0",
|
||||
"next": "16.0.0",
|
||||
"next-intl": "^4.4.0",
|
||||
"next-mdx-remote-client": "2",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"resend": "^4.1.2",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.19.5"
|
||||
}
|
||||
}
|
||||
7152
pnpm-lock.yaml
generated
Normal file
7152
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
20
proxy.ts
Normal file
20
proxy.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Enable a redirect to a matching locale at the root
|
||||
'/',
|
||||
|
||||
// Set a cookie to remember the previous locale for
|
||||
// all requests that have a locale prefix
|
||||
'/(en|zh|ja)/:path*',
|
||||
|
||||
// Enable redirects that add missing locales
|
||||
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||
'/((?!api|_next|_vercel|.*\\.|favicon.ico).*)'
|
||||
]
|
||||
};
|
||||
|
||||
1
public/ads.txt
Normal file
1
public/ads.txt
Normal file
@@ -0,0 +1 @@
|
||||
google.com, pub-xxxxxxxxxxxxxx, DIRECT, yyyyyyyyyyyyyyyyyy
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
539
public/logo.svg
Normal file
539
public/logo.svg
Normal file
@@ -0,0 +1,539 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1200px" height="1200px" viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve"> <image id="image0" width="1200" height="1200" x="0" y="0"
|
||||
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABLAAAASwCAMAAADc/0P9AAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
|
||||
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAL6UExURQAAAIUGMJYCIJcEH5cDIJcDIJYDH5cDIJcDIJcD
|
||||
IJcDIJcDH5YEIJcDIJQDIZcDH2MOPmMOPWIOPmIOPWIOPmIOPmMOPmIOPmIOPmIOPmINPmIOPmMN
|
||||
PmINPn4JMJcDH2IOPmIPPZcEIJcDIJcEH2IQPZYDH5gDH5YEH5gDH5cDH2AOP48FJZEFJJEEI5AE
|
||||
I5AFJI4FJI4FJY0FJYwFJYwFJosFJooGJ4kGJ4kGKIgGKIcGKIcGKYYGKYUHKoQHK4MHKoIHK4MI
|
||||
LIIIK4IILIEHK4EILIAILX8ILX4JLn0ILn0JL3wILnwJL3sJL3sJMHoJMHkJMXgJMXcJMXgKMncK
|
||||
MnYKMnYKM3UKM3QKM3QKNHMKNHILNXELNXALNnELNm8LNW8MN24LN20MN20MOGwMOGsMOWoMOWkM
|
||||
OmoNOmgNOmgOO5IEIo0GJowGJ4YHKnoKMXoKMHULNGwMOWkNO2cNO5EEIpMEImcNPGYNO2YOPGUO
|
||||
PZQEIZMEIWUNPJUEIWQOPWQNPGQOPJYDIGMOPpcDIGIOPmMPP5YDH5gEH2EPP2EOPpgDH2EOP18O
|
||||
P5kDHmAPQN25wseLm7lpfalFXY8QMHETPZJJaqlvicKYqtq/yqA1UP///+rX3dKir71wg6Y+Vo9C
|
||||
Y651jsyot4MvVOnT2dCbqIw7XsmisubS2fXs79aotJguS4c2WtOzwLRgdfDi5pwoRJdQcOPFzcyT
|
||||
obJab5ggPaZog9GuvOXJ0ZQYNngdRp9deuLN1vfw8teuuX0mTf37/Na4xPz5+uDJ0qpLY+C/yMN9
|
||||
jbd9k8SElbyLn/Tp7cCSpe7e46tXcHgTO6JZdMadrtqzvahSaptXdfnz9Pr2976GmrNleqRhfbeD
|
||||
mLZvg69vh97Fz/Lm6p08Wa9QZ753io8eP4IdQs2gr6JFYJdEY+fN1L9/kpInR4YmSZlLaZ5Sb61p
|
||||
gbF7k9q7xpM7W8mXp440VocTNbV2jKNMZ4otT+za4Kxfd38WO8SQoZc0UtGott/By8qRfv8AAAAs
|
||||
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" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 41 KiB |
BIN
public/logo_nexty.png
Normal file
BIN
public/logo_nexty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/og.png
Normal file
BIN
public/og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 297 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user