diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9ef9c5d --- /dev/null +++ b/CLAUDE.md @@ -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) diff --git a/CODEBUDDY.md b/CODEBUDDY.md new file mode 100644 index 0000000..8dec568 --- /dev/null +++ b/CODEBUDDY.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 30a602c..2565a3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2e19f20..5fdd833 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/(dashboard)/tools/audio-compress/layout.tsx b/src/app/(dashboard)/tools/audio-compress/layout.tsx new file mode 100644 index 0000000..2aea891 --- /dev/null +++ b/src/app/(dashboard)/tools/audio-compress/layout.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from "next"; +import { headers } from "next/headers"; + +export async function generateMetadata(): Promise { + 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; +} diff --git a/src/app/(dashboard)/tools/audio-compress/page.tsx b/src/app/(dashboard)/tools/audio-compress/page.tsx index 2338b40..d6c13c7 100644 --- a/src/app/(dashboard)/tools/audio-compress/page.tsx +++ b/src/app/(dashboard)/tools/audio-compress/page.tsx @@ -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,59 +24,64 @@ const defaultConfig: AudioCompressConfig = { channels: 2, }; -const configOptions: ConfigOption[] = [ - { - id: "bitrate", - type: "select", - label: "Bitrate", - description: "Higher bitrate = better quality, larger file", - value: defaultConfig.bitrate, - options: [ - { label: "64 kbps", value: 64 }, - { label: "128 kbps", value: 128 }, - { label: "192 kbps", value: 192 }, - { label: "256 kbps", value: 256 }, - { label: "320 kbps", value: 320 }, - ], - }, - { - id: "format", - type: "select", - label: "Output Format", - description: "Target audio format", - value: defaultConfig.format, - options: [ - { label: "MP3", value: "mp3" }, - { label: "AAC", value: "aac" }, - { label: "OGG", value: "ogg" }, - { label: "FLAC", value: "flac" }, - ], - }, - { - id: "sampleRate", - type: "select", - label: "Sample Rate", - description: "Audio sample rate in Hz", - value: defaultConfig.sampleRate, - options: [ - { label: "44.1 kHz", value: 44100 }, - { label: "48 kHz", value: 48000 }, - ], - }, - { - id: "channels", - type: "radio", - label: "Channels", - description: "Audio channels", - value: defaultConfig.channels, - options: [ - { label: "Stereo (2 channels)", value: 2 }, - { label: "Mono (1 channel)", value: 1 }, - ], - }, -]; +function useConfigOptions(config: AudioCompressConfig): ConfigOption[] { + const { t } = useTranslation(); + + return [ + { + id: "bitrate", + type: "select", + label: t("config.audioCompression.bitrate"), + description: t("config.audioCompression.bitrateDescription"), + value: config.bitrate, + options: [ + { label: "64 kbps", value: 64 }, + { label: "128 kbps", value: 128 }, + { label: "192 kbps", value: 192 }, + { label: "256 kbps", value: 256 }, + { label: "320 kbps", value: 320 }, + ], + }, + { + id: "format", + type: "select", + label: t("config.audioCompression.format"), + description: t("config.audioCompression.formatDescription"), + value: config.format, + options: [ + { label: "MP3", value: "mp3" }, + { label: "AAC", value: "aac" }, + { label: "OGG", value: "ogg" }, + { label: "FLAC", value: "flac" }, + ], + }, + { + id: "sampleRate", + type: "select", + 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 }, + ], + }, + { + id: "channels", + type: "radio", + label: t("config.audioCompression.channels"), + description: t("config.audioCompression.channelsDescription"), + value: config.channels, + options: [ + { 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 (
@@ -192,9 +199,9 @@ export default function AudioCompressPage() {
-

Audio Compression

+

{t("tools.audioCompression.title")}

- Compress and convert audio files with quality control + {t("tools.audioCompression.description")}

@@ -213,8 +220,8 @@ export default function AudioCompressPage() { /> ({ ...opt, value: config[opt.id as keyof AudioCompressConfig], @@ -226,7 +233,7 @@ export default function AudioCompressPage() { {canProcess && ( )} @@ -241,15 +248,15 @@ export default function AudioCompressPage() { )}
-

Supported Formats

+

{t("tools.audioCompression.supportedFormats")}

-

Input

-

MP3, WAV, OGG, AAC, FLAC, M4A

+

{t("tools.audioCompression.input")}

+

{t("tools.audioCompression.inputFormats")}

-

Output

-

MP3, AAC, OGG, FLAC

+

{t("tools.audioCompression.output")}

+

{t("tools.audioCompression.outputFormats")}

diff --git a/src/app/(dashboard)/tools/image-compress/layout.tsx b/src/app/(dashboard)/tools/image-compress/layout.tsx new file mode 100644 index 0000000..0c88530 --- /dev/null +++ b/src/app/(dashboard)/tools/image-compress/layout.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from "next"; +import { headers } from "next/headers"; + +export async function generateMetadata(): Promise { + 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; +} diff --git a/src/app/(dashboard)/tools/image-compress/page.tsx b/src/app/(dashboard)/tools/image-compress/page.tsx index c4fb5b2..4232fd6 100644 --- a/src/app/(dashboard)/tools/image-compress/page.tsx +++ b/src/app/(dashboard)/tools/image-compress/page.tsx @@ -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,35 +22,40 @@ const defaultConfig: ImageCompressConfig = { format: "original", }; -const configOptions: ConfigOption[] = [ - { - id: "quality", - type: "slider", - label: "Compression Quality", - description: "Lower quality = smaller file size", - value: defaultConfig.quality, - min: 1, - max: 100, - step: 1, - suffix: "%", - icon: , - }, - { - id: "format", - type: "select", - label: "Output Format", - description: "Convert to a different format (optional)", - value: defaultConfig.format, - options: [ - { label: "Original", value: "original" }, - { label: "JPEG", value: "jpeg" }, - { label: "PNG", value: "png" }, - { label: "WebP", value: "webp" }, - ], - }, -]; +function useConfigOptions(config: ImageCompressConfig): ConfigOption[] { + const { t } = useTranslation(); + + return [ + { + id: "quality", + type: "slider", + label: t("config.imageCompression.quality"), + description: t("config.imageCompression.qualityDescription"), + value: config.quality, + min: 1, + max: 100, + step: 1, + suffix: "%", + icon: , + }, + { + id: "format", + type: "select", + label: t("config.imageCompression.format"), + description: t("config.imageCompression.formatDescription"), + value: config.format, + options: [ + { 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 (
@@ -165,9 +172,9 @@ export default function ImageCompressPage() {
-

Image Compression

+

{t("tools.imageCompression.title")}

- Optimize images for web and mobile without quality loss + {t("tools.imageCompression.description")}

@@ -186,8 +193,8 @@ export default function ImageCompressPage() { /> ({ ...opt, value: config[opt.id as keyof ImageCompressConfig], @@ -199,7 +206,7 @@ export default function ImageCompressPage() { {canProcess && ( )} @@ -214,12 +221,11 @@ export default function ImageCompressPage() { )}
-

Features

+

{t("tools.imageCompression.features")}

    -
  • • 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
  • + {(t("tools.imageCompression.featureList") as unknown as string[]).map((feature, index) => ( +
  • • {feature}
  • + ))}
diff --git a/src/app/(dashboard)/tools/video-frames/layout.tsx b/src/app/(dashboard)/tools/video-frames/layout.tsx new file mode 100644 index 0000000..15eb4fb --- /dev/null +++ b/src/app/(dashboard)/tools/video-frames/layout.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from "next"; +import { headers } from "next/headers"; + +export async function generateMetadata(): Promise { + 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; +} diff --git a/src/app/(dashboard)/tools/video-frames/page.tsx b/src/app/(dashboard)/tools/video-frames/page.tsx index 6d63183..a9c6361 100644 --- a/src/app/(dashboard)/tools/video-frames/page.tsx +++ b/src/app/(dashboard)/tools/video-frames/page.tsx @@ -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,45 +25,50 @@ const defaultConfig: VideoFramesConfig = { height: undefined, }; -const configOptions: ConfigOption[] = [ - { - id: "fps", - type: "slider", - label: "Frame Rate", - description: "Number of frames to extract per second", - value: defaultConfig.fps, - min: 1, - max: 60, - step: 1, - suffix: " fps", - icon: