feat: 支持多语言能力
This commit is contained in:
52
CLAUDE.md
Normal file
52
CLAUDE.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Start dev server with Turbopack |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run start` | Run production server |
|
||||
| `npm run lint` | ESLint check |
|
||||
| `npm run type-check` | TypeScript type checking without emit |
|
||||
| `npm run format` | Format code with Prettier |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Tech Stack
|
||||
- **Framework**: Next.js 15 with App Router, React 19
|
||||
- **State**: Zustand for client state
|
||||
- **Styling**: Tailwind CSS with HSL CSS variables (dark mode by default)
|
||||
- **UI Components**: Custom components built on Radix UI primitives
|
||||
- **Animations**: Framer Motion
|
||||
- **Form Validation**: Zod
|
||||
- **Media Processing**: Sharp (images), FFmpeg (video/audio)
|
||||
|
||||
### Route Groups
|
||||
- `(auth)` - Authentication routes (login, register)
|
||||
- `(dashboard)` - Dashboard routes with shared Sidebar layout
|
||||
|
||||
### Tool Pages Pattern
|
||||
Each tool (image-compress, video-frames, audio-compress) follows a consistent pattern:
|
||||
1. `FileUploader` - Drag-drop file input using react-dropzone
|
||||
2. `ConfigPanel` - Tool-specific configuration options
|
||||
3. `ProgressBar` - Processing status indicator
|
||||
4. `ResultPreview` - Display processed files
|
||||
5. State managed via `useUploadStore` Zustand store
|
||||
|
||||
### State Management
|
||||
- `store/uploadStore.ts` - File list, processing status, progress tracking
|
||||
- `store/authStore.ts` - User authentication state
|
||||
|
||||
### Styling Convention
|
||||
Use `cn()` utility from `lib/utils.ts` for Tailwind class merging. Theme colors are CSS variables in `app/globals.css` accessed via HSL: `hsl(var(--primary))`.
|
||||
|
||||
### Type Definitions
|
||||
All shared types in `types/index.ts` including file types, processing configs, API responses.
|
||||
|
||||
### Development Phases
|
||||
- Current (Phase 1-4): Basic tools with mock API implementations
|
||||
- Phase 5: AI services integration (Replicate, OpenAI)
|
||||
- Phase 6: Authentication, database (PostgreSQL), payment processing (Stripe), cloud storage (Cloudflare R2)
|
||||
70
CODEBUDDY.md
Normal file
70
CODEBUDDY.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# CODEBUDDY.md This file provides guidance to CodeBuddy when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `npm run dev` | Start dev server with Turbopack (fast refresh) |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run start` | Run production server |
|
||||
| `npm run lint` | ESLint check |
|
||||
| `npm run type-check` | TypeScript type checking without emit |
|
||||
| `npm run format` | Format code with Prettier |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Tech Stack
|
||||
- **Framework**: Next.js 15 with App Router, React 19
|
||||
- **State**: Zustand for client state
|
||||
- **Styling**: Tailwind CSS with HSL CSS variables (dark mode by default)
|
||||
- **UI Components**: Custom components built on Radix UI primitives
|
||||
- **Animations**: Framer Motion
|
||||
- **Form Validation**: Zod
|
||||
- **Media Processing**: Sharp (images), FFmpeg (video/audio)
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # Auth routes group (login, register)
|
||||
│ ├── (dashboard)/ # Dashboard routes with Sidebar layout
|
||||
│ │ ├── tools/ # Tool pages (image-compress, video-frames, audio-compress)
|
||||
│ │ └── layout.tsx # Dashboard layout with Sidebar
|
||||
│ ├── api/ # API routes
|
||||
│ │ ├── upload/ # File upload endpoint
|
||||
│ │ └── process/ # Processing endpoints per tool type
|
||||
│ ├── globals.css # Global styles with CSS variables
|
||||
│ └── layout.tsx # Root layout (Header + Footer)
|
||||
├── components/
|
||||
│ ├── ui/ # Base UI primitives (button, card, input, etc.)
|
||||
│ ├── tools/ # Tool-specific components (FileUploader, ConfigPanel, ProgressBar, ResultPreview)
|
||||
│ └── layout/ # Layout components (Header, Footer, Sidebar)
|
||||
├── lib/
|
||||
│ ├── api.ts # API client functions
|
||||
│ └── utils.ts # Utility functions (cn, formatFileSize, etc.)
|
||||
├── store/
|
||||
│ ├── authStore.ts # Auth state
|
||||
│ └── uploadStore.ts # File upload and processing state
|
||||
└── types/
|
||||
└── index.ts # TypeScript types (UploadedFile, ProcessedFile, configs, etc.)
|
||||
```
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**Route Groups**: Uses `(auth)` and `(dashboard)` route groups. Dashboard routes share a layout with `Sidebar` component.
|
||||
|
||||
**Tool Pages Pattern**: Each tool (image-compress, video-frames, audio-compress) follows the same pattern:
|
||||
1. Uses `FileUploader` for drag-drop file input
|
||||
2. Uses `ConfigPanel` for tool-specific configuration options
|
||||
3. Uses `ProgressBar` to show processing status
|
||||
4. Uses `ResultPreview` to display processed files
|
||||
5. State managed via `useUploadStore` Zustand store
|
||||
|
||||
**API Routes**: API routes under `app/api/` use Node.js runtime. Each processing endpoint validates input and returns JSON responses. Currently mock implementations - production would use Sharp/FFmpeg and cloud storage.
|
||||
|
||||
**State Management**: Zustand stores in `store/` directory. `uploadStore` manages file list, processing status and progress. `authStore` manages user authentication state.
|
||||
|
||||
**Styling**: Uses `cn()` utility from `lib/utils.ts` for Tailwind class merging. Theme colors defined as CSS variables in `globals.css`. Component styling uses HSL color functions like `hsl(var(--primary))`.
|
||||
|
||||
**Type Definitions**: All shared types in `types/index.ts`. Includes file types, processing configs, API responses, and user types.
|
||||
851
package-lock.json
generated
851
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
@@ -278,6 +279,44 @@
|
||||
"node": ">=18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1052,6 +1091,70 @@
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
@@ -1164,6 +1267,243 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
|
||||
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-menu": "2.1.16",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
|
||||
@@ -1187,6 +1527,249 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.1.16",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
|
||||
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
|
||||
@@ -1210,6 +1793,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
|
||||
@@ -1302,6 +1957,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
@@ -1339,6 +2009,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
@@ -1369,6 +2057,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
@@ -1387,6 +2093,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://mirrors.tencent.com/npm/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -2139,6 +2851,18 @@
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://mirrors.tencent.com/npm/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
@@ -2918,6 +3642,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://mirrors.tencent.com/npm/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -3909,6 +4639,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://mirrors.tencent.com/npm/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
@@ -6143,6 +6882,75 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://mirrors.tencent.com/npm/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.7",
|
||||
"react-style-singleton": "^2.2.3",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.3",
|
||||
"use-sidecar": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://mirrors.tencent.com/npm/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-nonce": "^1.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -7264,6 +8072,49 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://mirrors.tencent.com/npm/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-node-es": "^1.1.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
|
||||
39
src/app/(dashboard)/tools/audio-compress/layout.tsx
Normal file
39
src/app/(dashboard)/tools/audio-compress/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
const lang = acceptLanguage.includes("zh") ? "zh" : "en";
|
||||
|
||||
const titles = {
|
||||
en: "Audio Compression - Compress & Convert Audio Files",
|
||||
zh: "音频压缩 - 压缩并转换音频文件",
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
en: "Compress and convert audio files to various formats including MP3, AAC, OGG, FLAC. Adjust bitrate and sample rate for optimal quality.",
|
||||
zh: "压缩并转换音频文件为多种格式,包括 MP3、AAC、OGG、FLAC。调整比特率和采样率以获得最佳质量。",
|
||||
};
|
||||
|
||||
return {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
openGraph: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
twitter: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function AudioCompressLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, AudioCompressConfig } from "@/types";
|
||||
|
||||
const audioAccept = {
|
||||
@@ -23,13 +24,16 @@ const defaultConfig: AudioCompressConfig = {
|
||||
channels: 2,
|
||||
};
|
||||
|
||||
const configOptions: ConfigOption[] = [
|
||||
function useConfigOptions(config: AudioCompressConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return [
|
||||
{
|
||||
id: "bitrate",
|
||||
type: "select",
|
||||
label: "Bitrate",
|
||||
description: "Higher bitrate = better quality, larger file",
|
||||
value: defaultConfig.bitrate,
|
||||
label: t("config.audioCompression.bitrate"),
|
||||
description: t("config.audioCompression.bitrateDescription"),
|
||||
value: config.bitrate,
|
||||
options: [
|
||||
{ label: "64 kbps", value: 64 },
|
||||
{ label: "128 kbps", value: 128 },
|
||||
@@ -41,9 +45,9 @@ const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: "Output Format",
|
||||
description: "Target audio format",
|
||||
value: defaultConfig.format,
|
||||
label: t("config.audioCompression.format"),
|
||||
description: t("config.audioCompression.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "MP3", value: "mp3" },
|
||||
{ label: "AAC", value: "aac" },
|
||||
@@ -54,9 +58,9 @@ const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "sampleRate",
|
||||
type: "select",
|
||||
label: "Sample Rate",
|
||||
description: "Audio sample rate in Hz",
|
||||
value: defaultConfig.sampleRate,
|
||||
label: t("config.audioCompression.sampleRate"),
|
||||
description: t("config.audioCompression.sampleRateDescription"),
|
||||
value: config.sampleRate,
|
||||
options: [
|
||||
{ label: "44.1 kHz", value: 44100 },
|
||||
{ label: "48 kHz", value: 48000 },
|
||||
@@ -65,17 +69,19 @@ const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "channels",
|
||||
type: "radio",
|
||||
label: "Channels",
|
||||
description: "Audio channels",
|
||||
value: defaultConfig.channels,
|
||||
label: t("config.audioCompression.channels"),
|
||||
description: t("config.audioCompression.channelsDescription"),
|
||||
value: config.channels,
|
||||
options: [
|
||||
{ label: "Stereo (2 channels)", value: 2 },
|
||||
{ label: "Mono (1 channel)", value: 1 },
|
||||
{ label: t("config.audioCompression.stereo"), value: 2 },
|
||||
{ label: t("config.audioCompression.mono"), value: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function AudioCompressPage() {
|
||||
const { t } = useTranslation();
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -112,7 +118,7 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: "Uploading audio...",
|
||||
message: t("processing.uploadingAudio"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -122,14 +128,14 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: `Uploading... ${i}%`,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: "Compressing audio...",
|
||||
message: t("processing.compressingAudio"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -138,7 +144,7 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: `Compressing... ${i}%`,
|
||||
message: t("processing.compressProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,14 +168,14 @@ export default function AudioCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: "Compression complete!",
|
||||
message: t("processing.compressionComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: "Compression failed",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: t("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -179,6 +185,7 @@ export default function AudioCompressPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -192,9 +199,9 @@ export default function AudioCompressPage() {
|
||||
<Music className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Audio Compression</h1>
|
||||
<h1 className="text-3xl font-bold">{t("tools.audioCompression.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Compress and convert audio files with quality control
|
||||
{t("tools.audioCompression.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,8 +220,8 @@ export default function AudioCompressPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title="Audio Settings"
|
||||
description="Configure compression parameters"
|
||||
title={t("config.audioCompression.title")}
|
||||
description={t("config.audioCompression.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof AudioCompressConfig],
|
||||
@@ -226,7 +233,7 @@ export default function AudioCompressPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Volume2 className="mr-2 h-4 w-4" />
|
||||
Compress Audio
|
||||
{t("tools.audioCompression.compressAudio")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -241,15 +248,15 @@ export default function AudioCompressPage() {
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">Supported Formats</h3>
|
||||
<h3 className="mb-3 font-semibold">{t("tools.audioCompression.supportedFormats")}</h3>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Input</p>
|
||||
<p>MP3, WAV, OGG, AAC, FLAC, M4A</p>
|
||||
<p className="font-medium text-foreground">{t("tools.audioCompression.input")}</p>
|
||||
<p>{t("tools.audioCompression.inputFormats")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Output</p>
|
||||
<p>MP3, AAC, OGG, FLAC</p>
|
||||
<p className="font-medium text-foreground">{t("tools.audioCompression.output")}</p>
|
||||
<p>{t("tools.audioCompression.outputFormats")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
39
src/app/(dashboard)/tools/image-compress/layout.tsx
Normal file
39
src/app/(dashboard)/tools/image-compress/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
const lang = acceptLanguage.includes("zh") ? "zh" : "en";
|
||||
|
||||
const titles = {
|
||||
en: "Image Compression - Optimize Images for Web & Mobile",
|
||||
zh: "图片压缩 - 为网页和移动端优化图片",
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
en: "Optimize images for web and mobile without quality loss. Support for batch processing and format conversion including PNG, JPEG, WebP.",
|
||||
zh: "为网页和移动端优化图片,不影响质量。支持批量处理和格式转换,包括 PNG、JPEG、WebP。",
|
||||
};
|
||||
|
||||
return {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
openGraph: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
twitter: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function ImageCompressLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, ImageCompressConfig } from "@/types";
|
||||
|
||||
const imageAccept = {
|
||||
@@ -21,13 +22,16 @@ const defaultConfig: ImageCompressConfig = {
|
||||
format: "original",
|
||||
};
|
||||
|
||||
const configOptions: ConfigOption[] = [
|
||||
function useConfigOptions(config: ImageCompressConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return [
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: "Compression Quality",
|
||||
description: "Lower quality = smaller file size",
|
||||
value: defaultConfig.quality,
|
||||
label: t("config.imageCompression.quality"),
|
||||
description: t("config.imageCompression.qualityDescription"),
|
||||
value: config.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
@@ -37,19 +41,21 @@ const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: "Output Format",
|
||||
description: "Convert to a different format (optional)",
|
||||
value: defaultConfig.format,
|
||||
label: t("config.imageCompression.format"),
|
||||
description: t("config.imageCompression.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "Original", value: "original" },
|
||||
{ label: "JPEG", value: "jpeg" },
|
||||
{ label: "PNG", value: "png" },
|
||||
{ label: "WebP", value: "webp" },
|
||||
{ label: t("config.imageCompression.formatOriginal"), value: "original" },
|
||||
{ label: t("config.imageCompression.formatJpeg"), value: "jpeg" },
|
||||
{ label: t("config.imageCompression.formatPng"), value: "png" },
|
||||
{ label: t("config.imageCompression.formatWebp"), value: "webp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function ImageCompressPage() {
|
||||
const { t } = useTranslation();
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -86,7 +92,7 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: "Uploading images...",
|
||||
message: t("processing.uploadingImages"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -96,14 +102,14 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: `Uploading... ${i}%`,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: "Compressing images...",
|
||||
message: t("processing.compressingImages"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -112,7 +118,7 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: `Compressing... ${i}%`,
|
||||
message: t("processing.compressProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,14 +141,14 @@ export default function ImageCompressPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: "Compression complete!",
|
||||
message: t("processing.compressionComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: "Compression failed",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: t("processing.compressionFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -152,6 +158,7 @@ export default function ImageCompressPage() {
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -165,9 +172,9 @@ export default function ImageCompressPage() {
|
||||
<ImageIcon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Image Compression</h1>
|
||||
<h1 className="text-3xl font-bold">{t("tools.imageCompression.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Optimize images for web and mobile without quality loss
|
||||
{t("tools.imageCompression.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,8 +193,8 @@ export default function ImageCompressPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title="Compression Settings"
|
||||
description="Configure compression options"
|
||||
title={t("config.imageCompression.title")}
|
||||
description={t("config.imageCompression.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof ImageCompressConfig],
|
||||
@@ -199,7 +206,7 @@ export default function ImageCompressPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
Compress Images
|
||||
{t("tools.imageCompression.compressImages")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -214,12 +221,11 @@ export default function ImageCompressPage() {
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">Features</h3>
|
||||
<h3 className="mb-3 font-semibold">{t("tools.imageCompression.features")}</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>• Batch processing - compress multiple images at once</li>
|
||||
<li>• Smart compression - maintains visual quality</li>
|
||||
<li>• Format conversion - PNG to JPEG, WebP, and more</li>
|
||||
<li>• Up to 80% size reduction without quality loss</li>
|
||||
{(t("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => (
|
||||
<li key={index}>• {feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
39
src/app/(dashboard)/tools/video-frames/layout.tsx
Normal file
39
src/app/(dashboard)/tools/video-frames/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const headersList = await headers();
|
||||
const acceptLanguage = headersList.get("accept-language") || "";
|
||||
const lang = acceptLanguage.includes("zh") ? "zh" : "en";
|
||||
|
||||
const titles = {
|
||||
en: "Video to Frames - Extract Frames from Videos",
|
||||
zh: "视频抽帧 - 从视频中提取帧",
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
en: "Extract frames from videos with customizable frame rates. Perfect for sprite animations and game asset preparation. Supports MP4, MOV, AVI, WebM.",
|
||||
zh: "从视频中提取帧,可自定义帧率。非常适合精灵动画制作和游戏素材准备。支持 MP4、MOV、AVI、WebM。",
|
||||
};
|
||||
|
||||
return {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
openGraph: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
twitter: {
|
||||
title: titles[lang],
|
||||
description: descriptions[lang],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function VideoFramesLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUploadStore } from "@/store/uploadStore";
|
||||
import { generateId } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile, ProcessedFile, VideoFramesConfig } from "@/types";
|
||||
|
||||
const videoAccept = {
|
||||
@@ -24,13 +25,16 @@ const defaultConfig: VideoFramesConfig = {
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
const configOptions: ConfigOption[] = [
|
||||
function useConfigOptions(config: VideoFramesConfig): ConfigOption[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return [
|
||||
{
|
||||
id: "fps",
|
||||
type: "slider",
|
||||
label: "Frame Rate",
|
||||
description: "Number of frames to extract per second",
|
||||
value: defaultConfig.fps,
|
||||
label: t("config.videoFrames.fps"),
|
||||
description: t("config.videoFrames.fpsDescription"),
|
||||
value: config.fps,
|
||||
min: 1,
|
||||
max: 60,
|
||||
step: 1,
|
||||
@@ -40,9 +44,9 @@ const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "format",
|
||||
type: "select",
|
||||
label: "Output Format",
|
||||
description: "Image format for the extracted frames",
|
||||
value: defaultConfig.format,
|
||||
label: t("config.videoFrames.format"),
|
||||
description: t("config.videoFrames.formatDescription"),
|
||||
value: config.format,
|
||||
options: [
|
||||
{ label: "PNG", value: "png" },
|
||||
{ label: "JPEG", value: "jpeg" },
|
||||
@@ -52,17 +56,19 @@ const configOptions: ConfigOption[] = [
|
||||
{
|
||||
id: "quality",
|
||||
type: "slider",
|
||||
label: "Quality",
|
||||
description: "Image quality (for JPEG and WebP)",
|
||||
value: defaultConfig.quality,
|
||||
label: t("config.videoFrames.quality"),
|
||||
description: t("config.videoFrames.qualityDescription"),
|
||||
value: config.quality,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
suffix: "%",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function VideoFramesPage() {
|
||||
const { t } = useTranslation();
|
||||
const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } =
|
||||
useUploadStore();
|
||||
|
||||
@@ -99,7 +105,7 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: 0,
|
||||
message: "Uploading video...",
|
||||
message: t("processing.uploadingVideo"),
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -109,14 +115,14 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "uploading",
|
||||
progress: i,
|
||||
message: `Uploading... ${i}%`,
|
||||
message: t("processing.uploadProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: 0,
|
||||
message: "Extracting frames...",
|
||||
message: t("processing.extractingFrames"),
|
||||
});
|
||||
|
||||
// Simulate processing
|
||||
@@ -125,7 +131,7 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "processing",
|
||||
progress: i,
|
||||
message: `Processing... ${i}%`,
|
||||
message: t("processing.processProgress", { progress: i }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,24 +155,24 @@ export default function VideoFramesPage() {
|
||||
setProcessingStatus({
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
message: "Processing complete!",
|
||||
message: t("processing.processingComplete"),
|
||||
});
|
||||
} catch (error) {
|
||||
setProcessingStatus({
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
message: "Processing failed",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
message: t("processing.processingFailed"),
|
||||
error: error instanceof Error ? error.message : t("processing.unknownError"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = (fileId: string) => {
|
||||
console.log("Downloading file:", fileId);
|
||||
// Implement download logic
|
||||
};
|
||||
|
||||
const canProcess = files.length > 0 && processingStatus.status !== "processing";
|
||||
const configOptions = useConfigOptions(config);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -181,9 +187,9 @@ export default function VideoFramesPage() {
|
||||
<Video className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Video to Frames</h1>
|
||||
<h1 className="text-3xl font-bold">{t("tools.videoFrames.title")}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Extract frames from videos with customizable settings
|
||||
{t("tools.videoFrames.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,8 +209,8 @@ export default function VideoFramesPage() {
|
||||
/>
|
||||
|
||||
<ConfigPanel
|
||||
title="Export Settings"
|
||||
description="Configure how frames are extracted"
|
||||
title={t("config.videoFrames.title")}
|
||||
description={t("config.videoFrames.description")}
|
||||
options={configOptions.map((opt) => ({
|
||||
...opt,
|
||||
value: config[opt.id as keyof VideoFramesConfig],
|
||||
@@ -216,7 +222,7 @@ export default function VideoFramesPage() {
|
||||
{canProcess && (
|
||||
<Button onClick={handleProcess} size="lg" className="w-full">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Process Video
|
||||
{t("tools.videoFrames.processVideo")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -233,12 +239,11 @@ export default function VideoFramesPage() {
|
||||
|
||||
{/* Info Card */}
|
||||
<div className="rounded-lg border border-border/40 bg-card/50 p-6">
|
||||
<h3 className="mb-3 font-semibold">How it works</h3>
|
||||
<h3 className="mb-3 font-semibold">{t("tools.videoFrames.howItWorks")}</h3>
|
||||
<ol className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>1. Upload your video file (MP4, MOV, AVI, etc.)</li>
|
||||
<li>2. Configure frame rate, format, and quality</li>
|
||||
<li>3. Click "Process Video" to start extraction</li>
|
||||
<li>4. Download the ZIP file with all frames</li>
|
||||
{(t("tools.videoFrames.steps") as unknown as string[]).map((step, index) => (
|
||||
<li key={index}>{index + 1}. {step}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
155
src/app/page.tsx
155
src/app/page.tsx
@@ -16,99 +16,94 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
const features = [
|
||||
function useFeatures() {
|
||||
const { t } = useTranslation();
|
||||
return [
|
||||
{
|
||||
icon: Video,
|
||||
title: "Video to Frames",
|
||||
description: "Extract frames from videos with customizable frame rates and formats. Perfect for sprite animations.",
|
||||
title: t("home.tools.videoToFrames.title"),
|
||||
description: t("home.tools.videoToFrames.description"),
|
||||
href: "/tools/video-frames",
|
||||
},
|
||||
{
|
||||
icon: Image,
|
||||
title: "Image Compression",
|
||||
description: "Optimize images for web and mobile without quality loss. Support for batch processing.",
|
||||
title: t("home.tools.imageCompression.title"),
|
||||
description: t("home.tools.imageCompression.description"),
|
||||
href: "/tools/image-compress",
|
||||
},
|
||||
{
|
||||
icon: Music,
|
||||
title: "Audio Compression",
|
||||
description: "Compress and convert audio files to various formats. Adjust bitrate and sample rate.",
|
||||
title: t("home.tools.audioCompression.title"),
|
||||
description: t("home.tools.audioCompression.description"),
|
||||
href: "/tools/audio-compress",
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "AI-Powered Tools",
|
||||
description: "Enhance your assets with AI. Upscale images, remove backgrounds, and more.",
|
||||
title: t("home.tools.aiTools.title"),
|
||||
description: t("home.tools.aiTools.description"),
|
||||
href: "/tools/ai-tools",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const benefits = [
|
||||
function useBenefits() {
|
||||
const { t } = useTranslation();
|
||||
return [
|
||||
{
|
||||
icon: Zap,
|
||||
title: "Lightning Fast",
|
||||
description: "Process files in seconds with our optimized infrastructure.",
|
||||
title: t("home.benefits.lightningFast.title"),
|
||||
description: t("home.benefits.lightningFast.description"),
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Secure & Private",
|
||||
description: "Your files are encrypted and automatically deleted after processing.",
|
||||
title: t("home.benefits.secure.title"),
|
||||
description: t("home.benefits.secure.description"),
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Built for Developers",
|
||||
description: "API access, batch processing, and tools designed for game development workflows.",
|
||||
title: t("home.benefits.forDevelopers.title"),
|
||||
description: t("home.benefits.forDevelopers.description"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const pricingPlans = [
|
||||
function usePricingPlans() {
|
||||
const { t } = useTranslation();
|
||||
return [
|
||||
{
|
||||
name: "Free",
|
||||
price: "$0",
|
||||
description: "Perfect for trying out",
|
||||
features: [
|
||||
"10 processes per day",
|
||||
"50MB max file size",
|
||||
"Basic tools",
|
||||
"Community support",
|
||||
],
|
||||
cta: "Get Started",
|
||||
name: t("home.pricing.plans.free.name"),
|
||||
price: t("home.pricing.plans.free.price"),
|
||||
description: t("home.pricing.plans.free.description"),
|
||||
features: t("home.pricing.plans.free.features") as unknown as string[],
|
||||
cta: t("home.pricing.plans.free.cta"),
|
||||
href: "/register",
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
price: "$19",
|
||||
period: "/month",
|
||||
description: "For serious developers",
|
||||
features: [
|
||||
"Unlimited processes",
|
||||
"500MB max file size",
|
||||
"All tools including AI",
|
||||
"Priority support",
|
||||
"API access",
|
||||
],
|
||||
cta: "Start Free Trial",
|
||||
name: t("home.pricing.plans.pro.name"),
|
||||
price: t("home.pricing.plans.pro.price"),
|
||||
period: t("home.pricing.plans.pro.period"),
|
||||
description: t("home.pricing.plans.pro.description"),
|
||||
features: t("home.pricing.plans.pro.features") as unknown as string[],
|
||||
cta: t("home.pricing.plans.pro.cta"),
|
||||
href: "/pricing",
|
||||
popular: true,
|
||||
popular: t("home.pricing.plans.pro.popular") as string,
|
||||
},
|
||||
{
|
||||
name: "Enterprise",
|
||||
price: "Custom",
|
||||
description: "For teams and businesses",
|
||||
features: [
|
||||
"Everything in Pro",
|
||||
"Unlimited file size",
|
||||
"Custom integrations",
|
||||
"Dedicated support",
|
||||
"SLA guarantee",
|
||||
],
|
||||
cta: "Contact Sales",
|
||||
name: t("home.pricing.plans.enterprise.name"),
|
||||
price: t("home.pricing.plans.enterprise.price"),
|
||||
description: t("home.pricing.plans.enterprise.description"),
|
||||
features: t("home.pricing.plans.enterprise.features") as unknown as string[],
|
||||
cta: t("home.pricing.plans.enterprise.cta"),
|
||||
href: "/contact",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function HeroSection() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden">
|
||||
{/* Background gradient */}
|
||||
@@ -126,26 +121,22 @@ function HeroSection() {
|
||||
>
|
||||
<Badge className="mb-4" variant="secondary">
|
||||
<Sparkles className="mr-1 h-3 w-3" />
|
||||
AI-Powered Tools
|
||||
{t("home.hero.badge")}
|
||||
</Badge>
|
||||
<h1 className="mb-6 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl 2xl:text-9xl">
|
||||
Build Games{" "}
|
||||
<span className="bg-gradient-to-r from-purple-400 via-pink-500 to-blue-500 bg-clip-text text-transparent">
|
||||
Faster
|
||||
</span>
|
||||
{t("home.hero.title", { speed: t("home.hero.speed") })}
|
||||
</h1>
|
||||
<p className="mb-8 text-lg text-muted-foreground md:text-xl xl:text-2xl 2xl:text-3xl">
|
||||
Transform your game development workflow with powerful AI tools. Video to frames,
|
||||
image compression, audio processing, and more.
|
||||
{t("home.hero.description")}
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row xl:gap-6">
|
||||
<Button size="lg" asChild className="xl:text-lg xl:px-8 xl:py-6 2xl:text-xl 2xl:px-10 2xl:py-7">
|
||||
<Link href="/tools">
|
||||
Start Building <ArrowRight className="ml-2 h-4 w-4" />
|
||||
{t("home.hero.startBuilding")} <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" asChild className="xl:text-lg xl:px-8 xl:py-6 2xl:text-xl 2xl:px-10 2xl:py-7">
|
||||
<Link href="/pricing">View Pricing</Link>
|
||||
<Link href="/pricing">{t("common.viewPricing")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -158,15 +149,15 @@ function HeroSection() {
|
||||
>
|
||||
<div>
|
||||
<div className="text-3xl font-bold md:text-4xl xl:text-5xl 2xl:text-6xl">10K+</div>
|
||||
<div className="text-sm text-muted-foreground md:text-base xl:text-lg 2xl:text-xl">Developers</div>
|
||||
<div className="text-sm text-muted-foreground md:text-base xl:text-lg 2xl:text-xl">{t("home.hero.stats.developers")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold md:text-4xl xl:text-5xl 2xl:text-6xl">1M+</div>
|
||||
<div className="text-sm text-muted-foreground md:text-base xl:text-lg 2xl:text-xl">Files Processed</div>
|
||||
<div className="text-sm text-muted-foreground md:text-base xl:text-lg 2xl:text-xl">{t("home.hero.stats.filesProcessed")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl font-bold md:text-4xl xl:text-5xl 2xl:text-6xl">99.9%</div>
|
||||
<div className="text-sm text-muted-foreground md:text-base xl:text-lg 2xl:text-xl">Uptime</div>
|
||||
<div className="text-sm text-muted-foreground md:text-base xl:text-lg 2xl:text-xl">{t("home.hero.stats.uptime")}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
@@ -176,6 +167,9 @@ function HeroSection() {
|
||||
}
|
||||
|
||||
function FeaturesSection() {
|
||||
const { t } = useTranslation();
|
||||
const features = useFeatures();
|
||||
|
||||
return (
|
||||
<section className="border-t border-border/40 bg-background/50 py-24 xl:py-32 2xl:py-40">
|
||||
<div className="container">
|
||||
@@ -187,10 +181,10 @@ function FeaturesSection() {
|
||||
className="mb-16 text-center xl:mb-20 2xl:mb-24"
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight md:text-4xl xl:text-5xl 2xl:text-6xl">
|
||||
Everything You Need
|
||||
{t("home.featuresSection.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground md:text-xl xl:text-2xl 2xl:text-3xl">
|
||||
Powerful tools designed specifically for game developers
|
||||
{t("home.featuresSection.description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@@ -214,7 +208,7 @@ function FeaturesSection() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="ghost" size="sm" className="w-full xl:text-base 2xl:text-lg">
|
||||
Try it now <ArrowRight className="ml-2 h-4 w-4" />
|
||||
{t("common.tryNow")} <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -228,6 +222,9 @@ function FeaturesSection() {
|
||||
}
|
||||
|
||||
function BenefitsSection() {
|
||||
const { t } = useTranslation();
|
||||
const benefits = useBenefits();
|
||||
|
||||
return (
|
||||
<section className="py-24 xl:py-32 2xl:py-40">
|
||||
<div className="container">
|
||||
@@ -239,14 +236,13 @@ function BenefitsSection() {
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight md:text-4xl xl:text-5xl 2xl:text-6xl">
|
||||
Why Choose Mini Game AI?
|
||||
{t("home.benefits.title")}
|
||||
</h2>
|
||||
<p className="mb-8 text-lg text-muted-foreground md:text-xl xl:text-2xl 2xl:text-3xl">
|
||||
We understand the unique challenges of game development. Our tools are built to help
|
||||
you work faster and smarter.
|
||||
{t("home.benefits.description")}
|
||||
</p>
|
||||
<Button size="lg" asChild className="xl:text-lg xl:px-8 xl:py-6 2xl:text-xl 2xl:px-10 2xl:py-7">
|
||||
<Link href="/about">Learn More About Us</Link>
|
||||
<Link href="/about">{t("common.learnMore")}</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
@@ -280,6 +276,9 @@ function BenefitsSection() {
|
||||
}
|
||||
|
||||
function PricingSection() {
|
||||
const { t } = useTranslation();
|
||||
const pricingPlans = usePricingPlans();
|
||||
|
||||
return (
|
||||
<section className="border-t border-border/40 bg-background/50 py-24 xl:py-32 2xl:py-40">
|
||||
<div className="container">
|
||||
@@ -291,10 +290,10 @@ function PricingSection() {
|
||||
className="mb-16 text-center xl:mb-20 2xl:mb-24"
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight md:text-4xl xl:text-5xl 2xl:text-6xl">
|
||||
Simple, Transparent Pricing
|
||||
{t("home.pricing.title")}
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground md:text-xl xl:text-2xl 2xl:text-3xl">
|
||||
Start free, scale as you grow. No hidden fees.
|
||||
{t("home.pricing.description")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@@ -310,7 +309,7 @@ function PricingSection() {
|
||||
<Card className={`relative h-full ${plan.popular ? "border-primary" : ""} xl:p-8 2xl:p-10`}>
|
||||
{plan.popular && (
|
||||
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 xl:text-base 2xl:text-lg">
|
||||
Most Popular
|
||||
{plan.popular}
|
||||
</Badge>
|
||||
)}
|
||||
<CardHeader>
|
||||
@@ -346,6 +345,8 @@ function PricingSection() {
|
||||
}
|
||||
|
||||
function CTASection() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="py-24 xl:py-32 2xl:py-40">
|
||||
<div className="container">
|
||||
@@ -358,17 +359,17 @@ function CTASection() {
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight md:text-4xl xl:text-5xl 2xl:text-6xl">
|
||||
Ready to Level Up?
|
||||
{t("home.cta.title")}
|
||||
</h2>
|
||||
<p className="mb-8 text-lg text-muted-foreground md:text-xl xl:text-2xl 2xl:text-3xl">
|
||||
Join thousands of game developers building amazing games with our tools.
|
||||
{t("home.cta.description")}
|
||||
</p>
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row xl:gap-6">
|
||||
<Button size="lg" asChild className="xl:text-lg xl:px-8 xl:py-6 2xl:text-xl 2xl:px-10 2xl:py-7">
|
||||
<Link href="/register">Get Started for Free</Link>
|
||||
<Link href="/register">{t("home.cta.getStarted")}</Link>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" asChild className="xl:text-lg xl:px-8 xl:py-6 2xl:text-xl 2xl:px-10 2xl:py-7">
|
||||
<Link href="/contact">Contact Sales</Link>
|
||||
<Link href="/contact">{t("common.contactSales")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Sparkles, Github, Twitter } from "lucide-react";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
const footerLinks = {
|
||||
function useFooterLinks() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
product: [
|
||||
{ name: "Features", href: "/features" },
|
||||
{ name: "Pricing", href: "/pricing" },
|
||||
{ name: t("common.features"), href: "/features" },
|
||||
{ name: t("nav.pricing"), href: "/pricing" },
|
||||
{ name: "API", href: "/api" },
|
||||
{ name: "Documentation", href: "/docs" },
|
||||
{ name: t("nav.docs"), href: "/docs" },
|
||||
],
|
||||
tools: [
|
||||
{ name: "Video to Frames", href: "/tools/video-frames" },
|
||||
{ name: "Image Compression", href: "/tools/image-compress" },
|
||||
{ name: "Audio Compression", href: "/tools/audio-compress" },
|
||||
{ name: "AI Tools", href: "/tools/ai-tools" },
|
||||
{ name: t("sidebar.videoToFrames"), href: "/tools/video-frames" },
|
||||
{ name: t("sidebar.imageCompression"), href: "/tools/image-compress" },
|
||||
{ name: t("sidebar.audioCompression"), href: "/tools/audio-compress" },
|
||||
{ name: t("home.tools.aiTools.title"), href: "/tools/ai-tools" },
|
||||
],
|
||||
company: [
|
||||
{ name: "About", href: "/about" },
|
||||
{ name: t("nav.about"), href: "/about" },
|
||||
{ name: "Blog", href: "/blog" },
|
||||
{ name: "Careers", href: "/careers" },
|
||||
{ name: "Contact", href: "/contact" },
|
||||
@@ -26,13 +32,24 @@ const footerLinks = {
|
||||
{ name: "Cookie Policy", href: "/cookies" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const socialLinks = [
|
||||
{ name: "Twitter", icon: Twitter, href: "https://twitter.com" },
|
||||
{ name: "GitHub", icon: Github, href: "https://github.com" },
|
||||
];
|
||||
|
||||
const sectionTitles: Record<string, string> = {
|
||||
product: "Product",
|
||||
tools: "Tools",
|
||||
company: "Company",
|
||||
legal: "Legal",
|
||||
};
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation();
|
||||
const footerLinks = useFooterLinks();
|
||||
|
||||
return (
|
||||
<footer className="border-t border-border/40 bg-background/50">
|
||||
<div className="container py-12 md:py-16">
|
||||
@@ -43,16 +60,16 @@ export function Footer() {
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary">
|
||||
<Sparkles className="h-5 w-5 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="text-xl font-bold">Mini Game AI</span>
|
||||
<span className="text-xl font-bold">{t("common.appName")}</span>
|
||||
</Link>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
AI-powered tools for mini game developers. Process media files with ease.
|
||||
{t("footer.tagline")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Product */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold">Product</h3>
|
||||
<h3 className="mb-4 text-sm font-semibold">{sectionTitles.product}</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
{footerLinks.product.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -69,7 +86,7 @@ export function Footer() {
|
||||
|
||||
{/* Tools */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold">Tools</h3>
|
||||
<h3 className="mb-4 text-sm font-semibold">{sectionTitles.tools}</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
{footerLinks.tools.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -86,7 +103,7 @@ export function Footer() {
|
||||
|
||||
{/* Company */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold">Company</h3>
|
||||
<h3 className="mb-4 text-sm font-semibold">{sectionTitles.company}</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
{footerLinks.company.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -103,7 +120,7 @@ export function Footer() {
|
||||
|
||||
{/* Legal */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold">Legal</h3>
|
||||
<h3 className="mb-4 text-sm font-semibold">{sectionTitles.legal}</h3>
|
||||
<ul className="space-y-3 text-sm">
|
||||
{footerLinks.legal.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -122,7 +139,7 @@ export function Footer() {
|
||||
{/* Bottom section */}
|
||||
<div className="mt-12 flex flex-col items-center justify-between border-t border-border/40 pt-8 md:flex-row">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} Mini Game AI. All rights reserved.
|
||||
© {new Date().getFullYear()} {t("common.appName")}. All rights reserved.
|
||||
</p>
|
||||
<div className="mt-4 flex space-x-6 md:mt-0">
|
||||
{socialLinks.map((link) => {
|
||||
|
||||
@@ -5,19 +5,27 @@ import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Menu, X, Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
// import { Button } from "@/components/ui/button"; // TODO: Uncomment when adding login/register buttons
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const navItems = [
|
||||
{ name: "Tools", href: "/tools" },
|
||||
{ name: "Pricing", href: "/pricing" },
|
||||
{ name: "Docs", href: "/docs" },
|
||||
{ name: "About", href: "/about" },
|
||||
function useNavItems() {
|
||||
const { t } = useTranslation();
|
||||
return [
|
||||
{ name: t("nav.tools"), href: "/tools/image-compress" },
|
||||
{ name: t("nav.pricing"), href: "/tools/video-frames" },
|
||||
{ name: t("nav.docs"), href: "/tools/audio-compress" },
|
||||
// Note: Temporarily redirecting to existing tool pages
|
||||
// TODO: Create dedicated pages for pricing, docs, about
|
||||
];
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
const pathname = usePathname();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const navItems = useNavItems();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
@@ -27,7 +35,7 @@ export function Header() {
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary">
|
||||
<Sparkles className="h-5 w-5 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="text-xl font-bold">Mini Game AI</span>
|
||||
<span className="text-xl font-bold">{t("common.appName")}</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
@@ -48,12 +56,15 @@ export function Header() {
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="hidden md:flex md:items-center md:space-x-4">
|
||||
<LanguageSwitcher />
|
||||
{/* TODO: Create login/register pages
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/login">Sign In</Link>
|
||||
<Link href="/login">{t("common.signIn")}</Link>
|
||||
</Button>
|
||||
<Button size="sm" asChild>
|
||||
<Link href="/register">Get Started</Link>
|
||||
<Link href="/register">{t("common.getStarted")}</Link>
|
||||
</Button>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -95,12 +106,15 @@ export function Header() {
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex flex-col space-y-2 pt-4">
|
||||
<LanguageSwitcher />
|
||||
{/* TODO: Create login/register pages
|
||||
<Button variant="ghost" size="sm" asChild className="w-full">
|
||||
<Link href="/login">Sign In</Link>
|
||||
<Link href="/login">{t("common.signIn")}</Link>
|
||||
</Button>
|
||||
<Button size="sm" asChild className="w-full">
|
||||
<Link href="/register">Get Started</Link>
|
||||
<Link href="/register">{t("common.getStarted")}</Link>
|
||||
</Button>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
51
src/components/layout/LanguageSwitcher.tsx
Normal file
51
src/components/layout/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useTranslation, type Locale } from "@/lib/i18n";
|
||||
import { Check, Globe } from "lucide-react";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { locale, setLocale, locales } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleLocaleChange = (newLocale: Locale) => {
|
||||
setLocale(newLocale);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="hidden md:inline">{locales[locale].flag}</span>
|
||||
<span className="hidden lg:inline">{locales[locale].name}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[160px]">
|
||||
{Object.entries(locales).map(([key, { name, flag }]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onClick={() => handleLocaleChange(key as Locale)}
|
||||
className="flex cursor-pointer items-center justify-between gap-2"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{flag}</span>
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
{locale === key && (
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -9,41 +9,44 @@ import {
|
||||
Music,
|
||||
Sparkles,
|
||||
LayoutDashboard,
|
||||
CreditCard,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
const sidebarNavItems = [
|
||||
function useSidebarNavItems() {
|
||||
const { t } = useTranslation();
|
||||
return [
|
||||
{
|
||||
title: "Dashboard",
|
||||
title: t("nav.dashboard"),
|
||||
items: [
|
||||
{ name: "Overview", href: "/dashboard", icon: LayoutDashboard },
|
||||
{ name: t("nav.overview"), href: "/", icon: LayoutDashboard },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tools",
|
||||
title: t("sidebar.tools"),
|
||||
items: [
|
||||
{ name: "Video to Frames", href: "/tools/video-frames", icon: Video },
|
||||
{ name: "Image Compression", href: "/tools/image-compress", icon: Image },
|
||||
{ name: "Audio Compression", href: "/tools/audio-compress", icon: Music },
|
||||
{ name: t("sidebar.videoToFrames"), href: "/tools/video-frames", icon: Video },
|
||||
{ name: t("sidebar.imageCompression"), href: "/tools/image-compress", icon: Image },
|
||||
{ name: t("sidebar.audioCompression"), href: "/tools/audio-compress", icon: Music },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "AI Tools",
|
||||
title: t("sidebar.aiTools"),
|
||||
items: [
|
||||
{ name: "AI Image", href: "/tools/ai-image", icon: Sparkles },
|
||||
{ name: "AI Audio", href: "/tools/ai-audio", icon: Sparkles },
|
||||
{ name: t("sidebar.aiImage"), href: "/tools/ai-tools", icon: Sparkles },
|
||||
{ name: t("sidebar.aiAudio"), href: "/tools/ai-tools", icon: Sparkles },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Account",
|
||||
title: t("nav.account"),
|
||||
items: [
|
||||
{ name: "Pricing", href: "/pricing", icon: CreditCard },
|
||||
{ name: "Settings", href: "/settings", icon: Settings },
|
||||
// TODO: Create pricing and settings pages
|
||||
// { name: t("nav.pricing"), href: "/pricing", icon: CreditCard },
|
||||
// { name: t("common.settings"), href: "/settings", icon: Settings },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
className?: string;
|
||||
@@ -51,6 +54,7 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ className }: SidebarProps) {
|
||||
const pathname = usePathname();
|
||||
const sidebarNavItems = useSidebarNavItems();
|
||||
|
||||
return (
|
||||
<aside
|
||||
@@ -61,7 +65,9 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
>
|
||||
<div className="h-full overflow-y-auto py-6 pr-4">
|
||||
<nav className="space-y-8 px-4">
|
||||
{sidebarNavItems.map((section) => (
|
||||
{sidebarNavItems
|
||||
.filter((section) => section.items.length > 0)
|
||||
.map((section) => (
|
||||
<div key={section.title}>
|
||||
<h3 className="mb-4 px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{section.title}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Slider } from "@/components/ui/slider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
|
||||
export interface ConfigOption {
|
||||
id: string;
|
||||
@@ -38,6 +39,8 @@ export function ConfigPanel({
|
||||
onReset,
|
||||
className,
|
||||
}: ConfigPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
@@ -50,7 +53,7 @@ export function ConfigPanel({
|
||||
</div>
|
||||
{onReset && (
|
||||
<Button variant="ghost" size="sm" onClick={onReset}>
|
||||
Reset
|
||||
{t("common.reset")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { formatFileSize, getFileExtension } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { UploadedFile } from "@/types";
|
||||
|
||||
interface FileUploaderProps {
|
||||
@@ -35,6 +36,8 @@ export function FileUploader({
|
||||
maxFiles = 10,
|
||||
disabled = false,
|
||||
}: FileUploaderProps) {
|
||||
const { t, plural } = useTranslation();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
if (disabled) return;
|
||||
@@ -85,14 +88,17 @@ export function FileUploader({
|
||||
<div className="mt-4">
|
||||
<p className="text-lg font-medium">
|
||||
{isDragActive
|
||||
? "Drop your files here"
|
||||
? t("uploader.dropActive")
|
||||
: isDragReject
|
||||
? "File type not accepted"
|
||||
: "Drag & drop files here"}
|
||||
? t("uploader.fileRejected")
|
||||
: t("uploader.dropFiles")}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
or click to browse • Max {formatFileSize(maxSize)} • Up to {maxFiles} file
|
||||
{maxFiles > 1 ? "s" : ""}
|
||||
{t("uploader.browseFiles", {
|
||||
maxSize: formatFileSize(maxSize),
|
||||
maxFiles,
|
||||
file: plural("uploader.file", maxFiles),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { ProcessingProgress } from "@/types";
|
||||
|
||||
interface ProgressBarProps {
|
||||
@@ -29,6 +30,7 @@ const statusColors = {
|
||||
};
|
||||
|
||||
export function ProgressBar({ progress, className }: ProgressBarProps) {
|
||||
const { t } = useTranslation();
|
||||
const { status, progress: value, message, error } = progress;
|
||||
const showProgress = status === "uploading" || status === "processing";
|
||||
const Icon = statusIcons[status];
|
||||
@@ -46,11 +48,11 @@ export function ProgressBar({ progress, className }: ProgressBarProps) {
|
||||
statusColors[status]
|
||||
)}
|
||||
>
|
||||
{status === "idle" && "Ready to process"}
|
||||
{status === "uploading" && "Uploading..."}
|
||||
{status === "processing" && "Processing..."}
|
||||
{status === "completed" && "Completed!"}
|
||||
{status === "failed" && "Failed"}
|
||||
{status === "idle" && t("progress.status.idle")}
|
||||
{status === "uploading" && t("progress.status.uploading")}
|
||||
{status === "processing" && t("progress.status.processing")}
|
||||
{status === "completed" && t("progress.status.completed")}
|
||||
{status === "failed" && t("progress.status.failed")}
|
||||
</p>
|
||||
{showProgress && (
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { formatFileSize } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import type { ProcessedFile } from "@/types";
|
||||
|
||||
interface ResultPreviewProps {
|
||||
@@ -21,6 +22,8 @@ export function ResultPreview({
|
||||
onShare,
|
||||
className,
|
||||
}: ResultPreviewProps) {
|
||||
const { t, plural } = useTranslation();
|
||||
|
||||
if (results.length === 0) return null;
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
@@ -36,7 +39,7 @@ export function ResultPreview({
|
||||
|
||||
if (metadata.compressionRatio) {
|
||||
badges.push({
|
||||
label: `Saved ${metadata.compressionRatio}%`,
|
||||
label: t("results.saved", { ratio: metadata.compressionRatio }),
|
||||
variant: "default" as const,
|
||||
});
|
||||
}
|
||||
@@ -58,9 +61,12 @@ export function ResultPreview({
|
||||
className={className}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold">Processing Complete</h3>
|
||||
<h3 className="text-lg font-semibold">{t("results.processingComplete")}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{results.length} file{results.length > 1 ? "s" : ""} ready for download
|
||||
{t("results.filesReady", {
|
||||
count: results.length,
|
||||
file: plural("results.file", results.length),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +121,7 @@ export function ResultPreview({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onDownload(result.id)}
|
||||
title="Download"
|
||||
title={t("common.download")}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -124,7 +130,7 @@ export function ResultPreview({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onShare(result.id)}
|
||||
title="Share"
|
||||
title={t("common.share")}
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
191
src/components/ui/dropdown-menu.tsx
Normal file
191
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight } 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 select-none items-center gap-2 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]:pointer-events-none [&_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>
|
||||
<span className="h-2 w-2 rounded-full bg-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,
|
||||
};
|
||||
85
src/lib/i18n.ts
Normal file
85
src/lib/i18n.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { create } from "zustand";
|
||||
import { persist, createJSONStorage } from "zustand/middleware";
|
||||
import en from "@/locales/en.json";
|
||||
import zh from "@/locales/zh.json";
|
||||
|
||||
export type Locale = "en" | "zh";
|
||||
export type LocaleMessages = typeof en;
|
||||
|
||||
export const locales: Record<Locale, { name: string; flag: string }> = {
|
||||
en: { name: "English", flag: "🇺🇸" },
|
||||
zh: { name: "中文", flag: "🇨🇳" },
|
||||
};
|
||||
|
||||
const messages: Record<Locale, LocaleMessages> = { en, zh };
|
||||
|
||||
interface I18nState {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: (key: string, params?: Record<string, string | number>) => string;
|
||||
}
|
||||
|
||||
function getNestedValue(obj: any, path: string): string | unknown {
|
||||
return path.split(".").reduce((acc, part) => acc?.[part], obj) || path;
|
||||
}
|
||||
|
||||
function interpolate(template: string, params: Record<string, string | number | unknown> = {}): string {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
||||
const value = params[key];
|
||||
if (value === undefined || value === null) return `{{${key}}}`;
|
||||
return String(value);
|
||||
});
|
||||
}
|
||||
|
||||
export const useI18nStore = create<I18nState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
locale: "en",
|
||||
setLocale: (locale) => set({ locale }),
|
||||
|
||||
t: (key, params) => {
|
||||
const { locale } = get();
|
||||
const value = getNestedValue(messages[locale], key);
|
||||
// If the value is not a string (e.g., an array), return it as-is
|
||||
if (typeof value !== "string") return value as string;
|
||||
if (!params || Object.keys(params).length === 0) return value;
|
||||
return interpolate(value, params);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "locale-storage",
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Convenience hook
|
||||
export function useTranslation() {
|
||||
const { locale, setLocale, t } = useI18nStore();
|
||||
|
||||
const plural = (key: string, count: number) => {
|
||||
const suffix = count === 1 ? "_one" : "_other";
|
||||
return t(`${key}${suffix}`, { count });
|
||||
};
|
||||
|
||||
return {
|
||||
locale,
|
||||
setLocale,
|
||||
t,
|
||||
plural,
|
||||
locales,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper for SSR
|
||||
export function getServerTranslations(locale: Locale = "en") {
|
||||
return {
|
||||
locale,
|
||||
t: (key: string, params?: Record<string, string | number>) => {
|
||||
const value = getNestedValue(messages[locale], key);
|
||||
if (typeof value !== "string") return value as string;
|
||||
if (!params || Object.keys(params).length === 0) return value;
|
||||
return interpolate(value, params);
|
||||
},
|
||||
};
|
||||
}
|
||||
254
src/locales/en.json
Normal file
254
src/locales/en.json
Normal file
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "KYMR",
|
||||
"loading": "Loading...",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"download": "Download",
|
||||
"share": "Share",
|
||||
"reset": "Reset",
|
||||
"submit": "Submit",
|
||||
"tryNow": "Try it now",
|
||||
"learnMore": "Learn More",
|
||||
"getStarted": "Get Started",
|
||||
"startBuilding": "Start Building",
|
||||
"viewPricing": "View Pricing",
|
||||
"contactSales": "Contact Sales",
|
||||
"signIn": "Sign In",
|
||||
"register": "Register",
|
||||
"features": "Features",
|
||||
"settings": "Settings",
|
||||
"processing": "Processing...",
|
||||
"uploading": "Uploading...",
|
||||
"completed": "Completed!",
|
||||
"failed": "Failed",
|
||||
"ready": "Ready to process",
|
||||
"file": "File",
|
||||
"files": "files"
|
||||
},
|
||||
"nav": {
|
||||
"tools": "Tools",
|
||||
"pricing": "Pricing",
|
||||
"docs": "Docs",
|
||||
"about": "About",
|
||||
"dashboard": "Dashboard",
|
||||
"overview": "Overview",
|
||||
"account": "Account"
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"badge": "Media Processing Tools",
|
||||
"title": "Empowering Game Development",
|
||||
"description": "Video to frames, image compression, audio optimization. Everything you need to prepare game assets, in one place.",
|
||||
"stats": {
|
||||
"developers": "Developers",
|
||||
"filesProcessed": "Files Processed",
|
||||
"uptime": "Uptime"
|
||||
}
|
||||
},
|
||||
"featuresSection": {
|
||||
"title": "Everything You Need",
|
||||
"description": "Powerful tools designed specifically for game developers"
|
||||
},
|
||||
"tools": {
|
||||
"videoToFrames": {
|
||||
"title": "Video to Frames",
|
||||
"description": "Extract frames from videos with customizable frame rates. Perfect for sprite animations."
|
||||
},
|
||||
"imageCompression": {
|
||||
"title": "Image Compression",
|
||||
"description": "Optimize images for web and mobile without quality loss. Support for batch processing."
|
||||
},
|
||||
"audioCompression": {
|
||||
"title": "Audio Compression",
|
||||
"description": "Compress and convert audio files to various formats. Adjust bitrate and sample rate."
|
||||
},
|
||||
"aiTools": {
|
||||
"title": "More Tools",
|
||||
"description": "Additional utilities for game development. Coming soon."
|
||||
}
|
||||
},
|
||||
"benefits": {
|
||||
"title": "Why Choose KYMR?",
|
||||
"description": "We understand the unique challenges of game development. Our tools are built to help you work faster and smarter.",
|
||||
"lightningFast": {
|
||||
"title": "Lightning Fast",
|
||||
"description": "Process files in seconds with our optimized infrastructure."
|
||||
},
|
||||
"secure": {
|
||||
"title": "Secure & Private",
|
||||
"description": "Your files are encrypted and automatically deleted after processing."
|
||||
},
|
||||
"forDevelopers": {
|
||||
"title": "Built for Developers",
|
||||
"description": "API access, batch processing, and tools designed for game development workflows."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Simple, Transparent Pricing",
|
||||
"description": "Start free, scale as you grow. No hidden fees.",
|
||||
"plans": {
|
||||
"free": {
|
||||
"name": "Free",
|
||||
"price": "$0",
|
||||
"description": "Perfect for trying out",
|
||||
"features": ["10 processes per day", "50MB max file size", "Basic tools", "Community support"],
|
||||
"cta": "Get Started"
|
||||
},
|
||||
"pro": {
|
||||
"name": "Pro",
|
||||
"price": "$19",
|
||||
"period": "/month",
|
||||
"description": "For serious developers",
|
||||
"features": ["Unlimited processes", "500MB max file size", "All tools including AI", "Priority support", "API access"],
|
||||
"cta": "Start Free Trial",
|
||||
"popular": "Most Popular"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "Enterprise",
|
||||
"price": "Custom",
|
||||
"description": "For teams and businesses",
|
||||
"features": ["Everything in Pro", "Unlimited file size", "Custom integrations", "Dedicated support", "SLA guarantee"],
|
||||
"cta": "Contact Sales"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"title": "Ready to Level Up?",
|
||||
"description": "Join thousands of game developers building amazing games with our tools.",
|
||||
"getStarted": "Get Started for Free"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"tools": "Tools",
|
||||
"aiTools": "More Tools",
|
||||
"videoToFrames": "Video to Frames",
|
||||
"imageCompression": "Image Compression",
|
||||
"audioCompression": "Audio Compression",
|
||||
"aiImage": "AI Image",
|
||||
"aiAudio": "AI Audio"
|
||||
},
|
||||
"uploader": {
|
||||
"dropFiles": "Drag & drop files here",
|
||||
"dropActive": "Drop your files here",
|
||||
"fileRejected": "File type not accepted",
|
||||
"browseFiles": "or click to browse • Max {{maxSize}} • Up to {{maxFiles}} {{file}}",
|
||||
"file_one": "file",
|
||||
"file_other": "files"
|
||||
},
|
||||
"progress": {
|
||||
"status": {
|
||||
"idle": "Ready to process",
|
||||
"uploading": "Uploading...",
|
||||
"processing": "Processing...",
|
||||
"completed": "Completed!",
|
||||
"failed": "Failed"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"imageCompression": {
|
||||
"title": "Compression Settings",
|
||||
"description": "Configure compression options",
|
||||
"quality": "Compression Quality",
|
||||
"qualityDescription": "Lower quality = smaller file size",
|
||||
"format": "Output Format",
|
||||
"formatDescription": "Convert to a different format (optional)",
|
||||
"formatOriginal": "Original",
|
||||
"formatJpeg": "JPEG",
|
||||
"formatPng": "PNG",
|
||||
"formatWebp": "WebP"
|
||||
},
|
||||
"videoFrames": {
|
||||
"title": "Export Settings",
|
||||
"description": "Configure how frames are extracted",
|
||||
"fps": "Frame Rate",
|
||||
"fpsDescription": "Number of frames to extract per second",
|
||||
"format": "Output Format",
|
||||
"formatDescription": "Image format for the extracted frames",
|
||||
"quality": "Quality",
|
||||
"qualityDescription": "Image quality (for JPEG and WebP)"
|
||||
},
|
||||
"audioCompression": {
|
||||
"title": "Audio Settings",
|
||||
"description": "Configure compression parameters",
|
||||
"bitrate": "Bitrate",
|
||||
"bitrateDescription": "Higher bitrate = better quality, larger file",
|
||||
"format": "Output Format",
|
||||
"formatDescription": "Target audio format",
|
||||
"sampleRate": "Sample Rate",
|
||||
"sampleRateDescription": "Audio sample rate in Hz",
|
||||
"channels": "Channels",
|
||||
"channelsDescription": "Audio channels",
|
||||
"stereo": "Stereo (2 channels)",
|
||||
"mono": "Mono (1 channel)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"imageCompression": {
|
||||
"title": "Image Compression",
|
||||
"description": "Optimize images for web and mobile without quality loss",
|
||||
"compressImages": "Compress Images",
|
||||
"features": "Features",
|
||||
"featureList": [
|
||||
"Batch processing - compress multiple images at once",
|
||||
"Smart compression - maintains visual quality",
|
||||
"Format conversion - PNG to JPEG, WebP, and more",
|
||||
"Up to 80% size reduction without quality loss"
|
||||
]
|
||||
},
|
||||
"videoFrames": {
|
||||
"title": "Video to Frames",
|
||||
"description": "Extract frames from videos with customizable settings",
|
||||
"processVideo": "Process Video",
|
||||
"howItWorks": "How it works",
|
||||
"steps": [
|
||||
"Upload your video file (MP4, MOV, AVI, etc.)",
|
||||
"Configure frame rate, format, and quality",
|
||||
"Click \"Process Video\" to start extraction",
|
||||
"Download the ZIP file with all frames"
|
||||
]
|
||||
},
|
||||
"audioCompression": {
|
||||
"title": "Audio Compression",
|
||||
"description": "Compress and convert audio files with quality control",
|
||||
"compressAudio": "Compress Audio",
|
||||
"supportedFormats": "Supported Formats",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"inputFormats": "MP3, WAV, OGG, AAC, FLAC, M4A",
|
||||
"outputFormats": "MP3, AAC, OGG, FLAC"
|
||||
}
|
||||
},
|
||||
"processing": {
|
||||
"uploadingImages": "Uploading images...",
|
||||
"compressingImages": "Compressing images...",
|
||||
"uploadingVideo": "Uploading video...",
|
||||
"extractingFrames": "Extracting frames...",
|
||||
"uploadingAudio": "Uploading audio...",
|
||||
"compressingAudio": "Compressing audio...",
|
||||
"compressionComplete": "Compression complete!",
|
||||
"processingComplete": "Processing complete!",
|
||||
"compressionFailed": "Compression failed",
|
||||
"processingFailed": "Processing failed",
|
||||
"unknownError": "Unknown error",
|
||||
"uploadProgress": "Uploading... {{progress}}%",
|
||||
"compressProgress": "Compressing... {{progress}}%",
|
||||
"processProgress": "Processing... {{progress}}%"
|
||||
},
|
||||
"results": {
|
||||
"processingComplete": "Processing Complete",
|
||||
"filesReady": "{{count}} {{file}} ready for download",
|
||||
"file_one": "file",
|
||||
"file_other": "files",
|
||||
"saved": "Saved {{ratio}}%"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "Media processing tools for game developers. Extract frames, compress images, optimize audio."
|
||||
}
|
||||
}
|
||||
254
src/locales/zh.json
Normal file
254
src/locales/zh.json
Normal file
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"common": {
|
||||
"appName": "KYMR",
|
||||
"loading": "加载中...",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"download": "下载",
|
||||
"share": "分享",
|
||||
"reset": "重置",
|
||||
"submit": "提交",
|
||||
"tryNow": "立即尝试",
|
||||
"learnMore": "了解更多",
|
||||
"getStarted": "开始使用",
|
||||
"startBuilding": "开始创作",
|
||||
"viewPricing": "查看价格",
|
||||
"contactSales": "联系销售",
|
||||
"signIn": "登录",
|
||||
"register": "注册",
|
||||
"features": "功能",
|
||||
"settings": "设置",
|
||||
"processing": "处理中...",
|
||||
"uploading": "上传中...",
|
||||
"completed": "已完成!",
|
||||
"failed": "失败",
|
||||
"ready": "准备处理",
|
||||
"file": "文件",
|
||||
"files": "文件"
|
||||
},
|
||||
"nav": {
|
||||
"tools": "工具",
|
||||
"pricing": "价格",
|
||||
"docs": "文档",
|
||||
"about": "关于",
|
||||
"dashboard": "仪表盘",
|
||||
"overview": "概览",
|
||||
"account": "账户"
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"badge": "媒体处理工具",
|
||||
"title": "为小游戏开发提供全链路提效赋能",
|
||||
"description": "视频抽帧、图片压缩、音频优化。一站式游戏素材处理工具,让开发更高效。",
|
||||
"stats": {
|
||||
"developers": "开发者",
|
||||
"filesProcessed": "文件处理量",
|
||||
"uptime": "正常运行时间"
|
||||
}
|
||||
},
|
||||
"featuresSection": {
|
||||
"title": "您需要的一切",
|
||||
"description": "专为游戏开发者设计的强大工具"
|
||||
},
|
||||
"tools": {
|
||||
"videoToFrames": {
|
||||
"title": "视频抽帧",
|
||||
"description": "从视频中提取帧,可自定义帧率。非常适合精灵动画制作。"
|
||||
},
|
||||
"imageCompression": {
|
||||
"title": "图片压缩",
|
||||
"description": "为网页和移动端优化图片,不影响质量。支持批量处理。"
|
||||
},
|
||||
"audioCompression": {
|
||||
"title": "音频压缩",
|
||||
"description": "压缩并转换音频文件为多种格式。调整比特率和采样率。"
|
||||
},
|
||||
"aiTools": {
|
||||
"title": "更多工具",
|
||||
"description": "更多游戏开发实用工具,敬请期待。"
|
||||
}
|
||||
},
|
||||
"benefits": {
|
||||
"title": "为什么选择 KYMR?",
|
||||
"description": "我们了解游戏开发的独特挑战。我们的工具帮助您更快速、更智能地工作。",
|
||||
"lightningFast": {
|
||||
"title": "极速处理",
|
||||
"description": "通过优化的基础设施,在几秒钟内处理文件。"
|
||||
},
|
||||
"secure": {
|
||||
"title": "安全私密",
|
||||
"description": "您的文件将被加密,处理完成后自动删除。"
|
||||
},
|
||||
"forDevelopers": {
|
||||
"title": "专为开发者打造",
|
||||
"description": "API 访问、批量处理,以及专为游戏开发工作流程设计的工具。"
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
"title": "简单透明的定价",
|
||||
"description": "免费开始,随您增长。无隐藏费用。",
|
||||
"plans": {
|
||||
"free": {
|
||||
"name": "免费版",
|
||||
"price": "¥0",
|
||||
"description": "适合尝试使用",
|
||||
"features": ["每天 10 次处理", "最大 50MB 文件", "基础工具", "社区支持"],
|
||||
"cta": "开始使用"
|
||||
},
|
||||
"pro": {
|
||||
"name": "专业版",
|
||||
"price": "¥99",
|
||||
"period": "/月",
|
||||
"description": "适合专业开发者",
|
||||
"features": ["无限处理次数", "最大 500MB 文件", "所有工具包括 AI", "优先支持", "API 访问"],
|
||||
"cta": "免费试用",
|
||||
"popular": "最受欢迎"
|
||||
},
|
||||
"enterprise": {
|
||||
"name": "企业版",
|
||||
"price": "定制",
|
||||
"description": "适合团队和企业",
|
||||
"features": ["专业版所有功能", "无限文件大小", "定制集成", "专属支持", "SLA 保证"],
|
||||
"cta": "联系销售"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cta": {
|
||||
"title": "准备好升级了吗?",
|
||||
"description": "加入数千名使用我们工具开发精彩游戏的开发者。",
|
||||
"getStarted": "免费开始"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"tools": "工具",
|
||||
"aiTools": "更多工具",
|
||||
"videoToFrames": "视频抽帧",
|
||||
"imageCompression": "图片压缩",
|
||||
"audioCompression": "音频压缩",
|
||||
"aiImage": "AI 图片",
|
||||
"aiAudio": "AI 音频"
|
||||
},
|
||||
"uploader": {
|
||||
"dropFiles": "拖拽文件到这里",
|
||||
"dropActive": "释放文件即可上传",
|
||||
"fileRejected": "不支持的文件类型",
|
||||
"browseFiles": "或点击选择文件 • 最大 {{maxSize}} • 最多 {{maxFiles}} 个{{file}}",
|
||||
"file_one": "文件",
|
||||
"file_other": "文件"
|
||||
},
|
||||
"progress": {
|
||||
"status": {
|
||||
"idle": "准备处理",
|
||||
"uploading": "上传中...",
|
||||
"processing": "处理中...",
|
||||
"completed": "已完成!",
|
||||
"failed": "失败"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"imageCompression": {
|
||||
"title": "压缩设置",
|
||||
"description": "配置压缩选项",
|
||||
"quality": "压缩质量",
|
||||
"qualityDescription": "质量越低 = 文件越小",
|
||||
"format": "输出格式",
|
||||
"formatDescription": "转换为其他格式(可选)",
|
||||
"formatOriginal": "原始",
|
||||
"formatJpeg": "JPEG",
|
||||
"formatPng": "PNG",
|
||||
"formatWebp": "WebP"
|
||||
},
|
||||
"videoFrames": {
|
||||
"title": "导出设置",
|
||||
"description": "配置帧提取方式",
|
||||
"fps": "帧率",
|
||||
"fpsDescription": "每秒提取的帧数",
|
||||
"format": "输出格式",
|
||||
"formatDescription": "提取帧的图片格式",
|
||||
"quality": "质量",
|
||||
"qualityDescription": "图片质量(适用于 JPEG 和 WebP)"
|
||||
},
|
||||
"audioCompression": {
|
||||
"title": "音频设置",
|
||||
"description": "配置压缩参数",
|
||||
"bitrate": "比特率",
|
||||
"bitrateDescription": "比特率越高 = 质量越好,文件越大",
|
||||
"format": "输出格式",
|
||||
"formatDescription": "目标音频格式",
|
||||
"sampleRate": "采样率",
|
||||
"sampleRateDescription": "音频采样率(Hz)",
|
||||
"channels": "声道",
|
||||
"channelsDescription": "音频声道",
|
||||
"stereo": "立体声(2 声道)",
|
||||
"mono": "单声道(1 声道)"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"imageCompression": {
|
||||
"title": "图片压缩",
|
||||
"description": "为网页和移动端优化图片,不影响质量",
|
||||
"compressImages": "压缩图片",
|
||||
"features": "功能特点",
|
||||
"featureList": [
|
||||
"批量处理 - 一次压缩多张图片",
|
||||
"智能压缩 - 保持视觉质量",
|
||||
"格式转换 - PNG 转 JPEG、WebP 等",
|
||||
"高达 80% 的压缩率且不影响质量"
|
||||
]
|
||||
},
|
||||
"videoFrames": {
|
||||
"title": "视频抽帧",
|
||||
"description": "从视频中提取帧,可自定义设置",
|
||||
"processVideo": "处理视频",
|
||||
"howItWorks": "工作原理",
|
||||
"steps": [
|
||||
"上传视频文件(MP4、MOV、AVI 等)",
|
||||
"配置帧率、格式和质量",
|
||||
"点击「处理视频」开始提取",
|
||||
"下载包含所有帧的 ZIP 文件"
|
||||
]
|
||||
},
|
||||
"audioCompression": {
|
||||
"title": "音频压缩",
|
||||
"description": "压缩并转换音频文件,可控质量",
|
||||
"compressAudio": "压缩音频",
|
||||
"supportedFormats": "支持的格式",
|
||||
"input": "输入",
|
||||
"output": "输出",
|
||||
"inputFormats": "MP3、WAV、OGG、AAC、FLAC、M4A",
|
||||
"outputFormats": "MP3、AAC、OGG、FLAC"
|
||||
}
|
||||
},
|
||||
"processing": {
|
||||
"uploadingImages": "上传图片中...",
|
||||
"compressingImages": "压缩图片中...",
|
||||
"uploadingVideo": "上传视频中...",
|
||||
"extractingFrames": "提取帧中...",
|
||||
"uploadingAudio": "上传音频中...",
|
||||
"compressingAudio": "压缩音频中...",
|
||||
"compressionComplete": "压缩完成!",
|
||||
"processingComplete": "处理完成!",
|
||||
"compressionFailed": "压缩失败",
|
||||
"processingFailed": "处理失败",
|
||||
"unknownError": "未知错误",
|
||||
"uploadProgress": "上传中... {{progress}}%",
|
||||
"compressProgress": "压缩中... {{progress}}%",
|
||||
"processProgress": "处理中... {{progress}}%"
|
||||
},
|
||||
"results": {
|
||||
"processingComplete": "处理完成",
|
||||
"filesReady": "{{count}} 个{{file}}可下载",
|
||||
"file_one": "文件",
|
||||
"file_other": "文件",
|
||||
"saved": "节省 {{ratio}}%"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "面向游戏开发者的媒体处理工具。视频抽帧、图片压缩、音频优化。"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user