diff --git a/app/create/page.tsx b/app/create/page.tsx new file mode 100644 index 0000000..b332b09 --- /dev/null +++ b/app/create/page.tsx @@ -0,0 +1,10 @@ +import CreateVideo from '@/components/pages/create-video/CreateVideo'; +import { DashboardLayout } from '@/components/layout/dashboard-layout'; + +export default function CreatePage() { + return ( + + + + ); +} \ No newline at end of file diff --git a/components/layout/type.ts b/components/layout/type.ts index 9660c8b..8441d55 100644 --- a/components/layout/type.ts +++ b/components/layout/type.ts @@ -1,4 +1,4 @@ -import { BookHeart, Gift } from "lucide-react"; +import { BookHeart, Gift, Plus } from "lucide-react"; interface NavigationItem { name: string; @@ -17,6 +17,7 @@ export const navigationItems: Navigations[] = [ items: [ { name: 'My Portfolio', href: '/movies', icon: BookHeart }, { name: 'Share', href: '/share', icon: Gift }, + { name: 'Create', href: '/create', icon: Plus }, ], } ]; \ No newline at end of file diff --git a/components/pages/create-video/CreateInput/ConfigPanel.tsx b/components/pages/create-video/CreateInput/ConfigPanel.tsx new file mode 100644 index 0000000..7a949d8 --- /dev/null +++ b/components/pages/create-video/CreateInput/ConfigPanel.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { + GlobalOutlined, + ClockCircleOutlined, + DownOutlined, + MobileOutlined, + DesktopOutlined, +} from '@ant-design/icons'; +import { WandSparkles, RectangleHorizontal, RectangleVertical } from 'lucide-react'; +import { Dropdown, Menu, Tooltip } from 'antd'; +import { LanguageOptions, VideoDurationOptions } from './config-options'; +import type { ConfigOptions, LanguageValue, VideoDurationValue } from './config-options'; + +interface ConfigPanelProps { + /** Current configuration options */ + configOptions: ConfigOptions; + /** Handler for configuration changes */ + onConfigChange: (key: K, value: ConfigOptions[K]) => void; + /** Whether it's mobile device */ + isMobile?: boolean; + /** Whether it's desktop device */ + isDesktop?: boolean; +} + +/** + * Configuration panel component for video creation settings. + * Includes language, auto script, duration, and aspect ratio selectors. + * @param {ConfigOptions} configOptions - current configuration + * @param {Function} onConfigChange - handler for config changes + * @param {boolean} isMobile - whether it's mobile device + * @param {boolean} isDesktop - whether it's desktop device + * @returns {JSX.Element} + */ +export const ConfigPanel = ({ + configOptions, + onConfigChange, + isMobile = false, + isDesktop = true, +}: ConfigPanelProps) => { + /** Language dropdown menu */ + const languageMenu = ( + onConfigChange('language', key as LanguageValue)} + items={LanguageOptions.map((option) => ({ + key: option.value, + label: {option.label}, + }))} + /> + ); + + /** Duration dropdown menu */ + const durationMenu = ( + onConfigChange('videoDuration', key as VideoDurationValue)} + items={VideoDurationOptions.map((option) => ({ + key: option.value, + label: {option.label}, + }))} + /> + ); + + const currentLanguage = LanguageOptions.find((option) => option.value === configOptions.language); + const currentDuration = configOptions.videoDuration === 'unlimited' ? 'auto' : configOptions.videoDuration; + + return ( + + {/* Language selector */} + + + + {currentLanguage?.code} + + + + + {/* Auto script toggle */} + + { + if (configOptions.videoDuration !== '8s') { + onConfigChange('expansion_mode', !configOptions.expansion_mode); + } + }} + > + + AutoScript + + + + {/* Duration selector */} + + + + {currentDuration} + + + + + {/* Aspect ratio toggles */} + + onConfigChange('aspect_ratio', 'VIDEO_ASPECT_RATIO_PORTRAIT')} + > + + + onConfigChange('aspect_ratio', 'VIDEO_ASPECT_RATIO_LANDSCAPE')} + > + + + + + ); +}; + diff --git a/components/pages/create-video/CreateInput/README.md b/components/pages/create-video/CreateInput/README.md new file mode 100644 index 0000000..b7951a8 --- /dev/null +++ b/components/pages/create-video/CreateInput/README.md @@ -0,0 +1,77 @@ +# CreateInput Component + +Video creation input form with configuration options. + +## File Structure + +``` +CreateInput/ +├── VideoCreationForm.tsx # Main form component +├── ConfigPanel.tsx # Configuration panel with all options +├── config-options.ts # Configuration options and types +├── index.ts # Module exports +└── README.md # This file +``` + +## Components + +### VideoCreationForm +Main form component for video creation, includes: +- Photo upload (character, scene, prop) +- Text input area +- Configuration options +- Create button + +### ConfigPanel +Configuration panel with unified circular button style: +- Language selector (14 languages) +- Auto Script toggle (AI Story Copilot) +- Duration selector (8s, 1min, 2min, auto) +- Aspect ratio selector (landscape/portrait) + +All buttons follow the same design pattern: +- Circular buttons with `rounded-full` +- Border: `border-white/20` +- Hover: `hover:bg-white/5 hover:border-cyan-400/60` +- Active state: `border-cyan-400 bg-cyan-400/10` +- Cyan color theme for interactions + +## Configuration Options + +Defined in `config-options.ts`: + +```typescript +interface ConfigOptions { + language: LanguageValue; // Default: 'english' + expansion_mode: boolean; // Default: false + videoDuration: VideoDurationValue; // Default: 'unlimited' + aspect_ratio: AspectRatioValue; // Default: 'VIDEO_ASPECT_RATIO_LANDSCAPE' +} +``` + +## Usage + +```tsx +import { VideoCreationForm } from '@/components/pages/create-video/CreateInput'; + +export default function CreatePage() { + return ; +} +``` + +## Design System + +All configuration buttons follow a unified design: +- **Base style**: `rounded-full border border-white/20 bg-transparent` +- **Hover**: `hover:bg-white/5 hover:border-cyan-400/60 hover:text-cyan-400` +- **Active**: `border-cyan-400 bg-cyan-400/10 text-cyan-400` +- **Transition**: `transition-all duration-200` + +## Notes + +- Configuration options are synchronized with ChatInputBox component +- All text and labels are in English as per project guidelines +- Uses Tailwind CSS 3.x for all styling +- Uses Ant Design icons for consistency with existing UI +- Responsive design with mobile/desktop support + diff --git a/components/pages/create-video/CreateInput/VideoCreationForm.tsx b/components/pages/create-video/CreateInput/VideoCreationForm.tsx new file mode 100644 index 0000000..c3fc030 --- /dev/null +++ b/components/pages/create-video/CreateInput/VideoCreationForm.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useState, useRef } from 'react'; +import { PhotoPreviewSection } from '../PhotoPreview'; +import type { PhotoItem, PhotoType } from '../PhotoPreview/types'; +import { + PlusOutlined, + UserOutlined, + CameraOutlined, + BulbOutlined, + ArrowRightOutlined, +} from '@ant-design/icons'; +import { Dropdown, Menu } from 'antd'; +import { ConfigPanel } from './ConfigPanel'; +import { defaultConfig } from './config-options'; +import type { ConfigOptions } from './config-options'; +import { useDeviceType } from '@/hooks/useDeviceType'; + +export default function VideoCreationForm() { + const [photos, setPhotos] = useState([]); + const [inputText, setInputText] = useState(''); + const [configOptions, setConfigOptions] = useState(defaultConfig); + + const { isMobile, isDesktop } = useDeviceType(); + + const characterInputRef = useRef(null); + const sceneInputRef = useRef(null); + const propInputRef = useRef(null); + + /** Handle photo deletion */ + const handleDeletePhoto = (index: number) => { + setPhotos(prevPhotos => prevPhotos.filter((_, i) => i !== index)); + }; + + /** Handle file upload */ + const handleFileUpload = (event: React.ChangeEvent, type: PhotoType) => { + const files = event.target.files; + if (!files || files.length === 0) return; + + const newPhotos: PhotoItem[] = Array.from(files).map((file, index) => ({ + url: URL.createObjectURL(file), + type, + id: `${type}-${Date.now()}-${index}`, + })); + + setPhotos(prevPhotos => [...prevPhotos, ...newPhotos]); + event.target.value = ''; + }; + + /** Handle configuration change */ + const handleConfigChange = ( + key: K, + value: ConfigOptions[K] + ) => { + setConfigOptions(prev => ({ ...prev, [key]: value })); + }; + + /** Handle video creation */ + const handleCreate = () => { + console.log({ + text: inputText, + photos, + config: configOptions, + }); + // TODO: Implement video creation logic + }; + + return ( + + {/* Main Content Area with Border */} + + {/* Photo Preview Section - Top */} + {photos.length > 0 && ( + + + + )} + + {/* Text Input Area - Middle */} + + setInputText(e.target.value)} + /> + + + {/* Control Panel - Bottom */} + + {/* Left Side - Upload and Options */} + + {/* Upload Button with Dropdown */} + + } + onClick={() => characterInputRef.current?.click()} + > + Character + + } + onClick={() => sceneInputRef.current?.click()} + > + Scene + + } + onClick={() => propInputRef.current?.click()} + > + Prop + + + } + trigger={['click']} + > + + + + + + {/* Hidden file inputs */} + handleFileUpload(e, 'character')} + /> + handleFileUpload(e, 'scene')} + /> + handleFileUpload(e, 'prop')} + /> + + {/* Mention Button */} + + @ + + + {/* Configuration Panel */} + + + + {/* Right Side - Create Button */} + + + + + + + ); +} + diff --git a/components/pages/create-video/CreateInput/config-options.ts b/components/pages/create-video/CreateInput/config-options.ts new file mode 100644 index 0000000..4ebc926 --- /dev/null +++ b/components/pages/create-video/CreateInput/config-options.ts @@ -0,0 +1,51 @@ +/** Language configuration options */ +export const LanguageOptions = [ + { value: "english", label: "English", isVip: false, code: 'EN' }, + { value: "chinese", label: "Chinese", isVip: false, code: 'ZH' }, + { value: "japanese", label: "Japanese", isVip: false, code: 'JA' }, + { value: "spanish", label: "Spanish", isVip: false, code: 'ES' }, + { value: "portuguese", label: "Portuguese", isVip: false, code: 'PT' }, + { value: "hindi", label: "Hindi", isVip: false, code: 'HI' }, + { value: "korean", label: "Korean", isVip: false, code: 'KO' }, + { value: "arabic", label: "Arabic", isVip: false, code: 'AR' }, + { value: "russian", label: "Russian", isVip: false, code: 'RU' }, + { value: "thai", label: "Thai", isVip: false, code: 'TH' }, + { value: "french", label: "French", isVip: false, code: 'FR' }, + { value: "german", label: "German", isVip: false, code: 'DE' }, + { value: "vietnamese", label: "Vietnamese", isVip: false, code: 'VI' }, + { value: "indonesian", label: "Indonesian", isVip: false, code: 'ID' } +]; + +/** Video duration options */ +export const VideoDurationOptions = [ + { value: "8s", label: "8s" }, + { value: "1min", label: "1min" }, + { value: "2min", label: "2min" }, + { value: "unlimited", label: "auto" }, +]; + +/** Language type */ +export type LanguageValue = typeof LanguageOptions[number]['value']; + +/** Video duration type */ +export type VideoDurationValue = typeof VideoDurationOptions[number]['value']; + +/** Aspect ratio type */ +export type AspectRatioValue = 'VIDEO_ASPECT_RATIO_LANDSCAPE' | 'VIDEO_ASPECT_RATIO_PORTRAIT'; + +/** Configuration options interface */ +export interface ConfigOptions { + language: LanguageValue; + expansion_mode: boolean; + videoDuration: VideoDurationValue; + aspect_ratio: AspectRatioValue; +} + +/** Default configuration */ +export const defaultConfig: ConfigOptions = { + language: 'english', + expansion_mode: false, + videoDuration: 'unlimited', + aspect_ratio: 'VIDEO_ASPECT_RATIO_LANDSCAPE', +}; + diff --git a/components/pages/create-video/CreateInput/index.ts b/components/pages/create-video/CreateInput/index.ts new file mode 100644 index 0000000..5eb3d91 --- /dev/null +++ b/components/pages/create-video/CreateInput/index.ts @@ -0,0 +1,4 @@ +export { default as VideoCreationForm } from './VideoCreationForm'; +export { ConfigPanel } from './ConfigPanel'; +export * from './config-options'; + diff --git a/components/pages/create-video/CreateVideo.tsx b/components/pages/create-video/CreateVideo.tsx new file mode 100644 index 0000000..c7d21d2 --- /dev/null +++ b/components/pages/create-video/CreateVideo.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { VideoCreationForm } from './CreateInput'; + +export default function CreateVideo() { + return ( + + Transform any idea into a compelling video + Generate professional videos from simple prompts. Browse community creations for inspiration, or start fresh with your own vision + + + + + + + ); +} diff --git a/components/pages/create-video/PhotoPreview/PhotoPreviewSection.tsx b/components/pages/create-video/PhotoPreview/PhotoPreviewSection.tsx new file mode 100644 index 0000000..6806e7c --- /dev/null +++ b/components/pages/create-video/PhotoPreview/PhotoPreviewSection.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useState } from 'react'; +import { Image } from 'antd'; +import { CloseOutlined, UserOutlined, CameraOutlined, BulbOutlined } from '@ant-design/icons'; +import type { PhotoPreviewSectionProps, PhotoType } from './types'; +import './styles.css'; + +/** + * Photo Preview Section Component + * Displays a horizontal list of photos with type indicators and delete functionality + * @param photos - Array of photos with type information + * @param onDelete - Callback when a photo is deleted + * @param className - Additional CSS classes + */ +export default function PhotoPreviewSection({ + photos = [], + onDelete, + className = '', +}: PhotoPreviewSectionProps) { + const [previewVisible, setPreviewVisible] = useState(false); + const [previewImage, setPreviewImage] = useState(''); + const [hoveredIndex, setHoveredIndex] = useState(null); + + /** + * Get icon for photo type + */ + const getTypeIcon = (type: PhotoType) => { + switch (type) { + case 'character': + return ; + case 'scene': + return ; + case 'prop': + return ; + default: + return null; + } + }; + + /** + * Get label for photo type + */ + const getTypeLabel = (type: PhotoType) => { + switch (type) { + case 'character': + return 'Character'; + case 'scene': + return 'Scene'; + case 'prop': + return 'Prop'; + default: + return ''; + } + }; + + /** + * Handle photo preview + */ + const handlePreview = (url: string) => { + setPreviewImage(url); + setPreviewVisible(true); + }; + + /** + * Handle photo deletion + */ + const handleDelete = (index: number) => { + onDelete?.(index); + }; + + /** Don't render if no photos */ + if (photos.length === 0) { + return null; + } + + return ( + + {/* Photo List Container with Horizontal Scroll */} + + {photos.map((photo, index) => ( + setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + > + {/* Photo Image */} + handlePreview(photo.url)} + > + + + + {/* Type Icon - Bottom Left Corner */} + + {getTypeIcon(photo.type)} + + + {/* Delete Button - Top Right Corner */} + {hoveredIndex === index && ( + { + e.stopPropagation(); + handleDelete(index); + }} + > + + + )} + + ))} + + + {/* Preview Modal - Using absolute positioning to avoid layout space */} + + { + setPreviewVisible(visible); + }, + }} + /> + + + ); +} + diff --git a/components/pages/create-video/PhotoPreview/README.md b/components/pages/create-video/PhotoPreview/README.md new file mode 100644 index 0000000..67b534f --- /dev/null +++ b/components/pages/create-video/PhotoPreview/README.md @@ -0,0 +1,113 @@ +# PhotoPreviewSection Component + +Compact photo preview component with type indicators for characters, scenes, and props. + +## Features + +✅ **Type Indicators** - Icons in bottom-left corner show photo type +✅ **Compact Design** - 64x64px photos with horizontal scroll +✅ **Smart Visibility** - Auto-hides when no photos +✅ **Preview Support** - Click to view full-size image +✅ **Delete Functionality** - X button appears on hover +✅ **Dark Theme** - Fully styled for dark UI + +## Usage + +```tsx +import { PhotoPreviewSection } from './PhotoPreview'; +import type { PhotoItem } from './PhotoPreview'; + +function MyComponent() { + const [photos, setPhotos] = useState([ + { url: 'photo1.jpg', type: 'character', id: '1' }, + { url: 'photo2.jpg', type: 'scene', id: '2' }, + { url: 'photo3.jpg', type: 'prop', id: '3' }, + ]); + + const handleDelete = (index: number) => { + setPhotos(prev => prev.filter((_, i) => i !== index)); + }; + + return ( + + ); +} +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `photos` | `PhotoItem[]` | `[]` | Array of photos with type information | +| `onDelete` | `(index: number) => void` | optional | Callback when a photo is deleted | +| `className` | `string` | `''` | Additional CSS classes | + +## Photo Types + +### PhotoItem Interface + +```typescript +interface PhotoItem { + url: string; // Photo URL + type: PhotoType; // 'character' | 'scene' | 'prop' + id?: string; // Optional unique identifier +} +``` + +### Type Icons + +- **Character** (👤): `UserOutlined` - Cyan icon in bottom-left +- **Scene** (📷): `CameraOutlined` - Cyan icon in bottom-left +- **Prop** (💡): `BulbOutlined` - Cyan icon in bottom-left + +## Design Specifications + +- **Photo Size**: 64x64px (w-16 h-16) +- **Gap**: 8px between items +- **Border Radius**: 6px (rounded-md) +- **Type Icon**: 20x20px square, bottom-left corner with backdrop blur +- **Delete Button**: Circular gray button, top-right corner (hover only) +- **Scrollbar**: 4px height, appears on hover + +## Interactions + +- **Click Photo**: Opens full-screen preview modal +- **Hover Photo**: Shows delete button at top-right corner +- **Click Delete**: Triggers `onDelete` callback with photo index +- **Type Icon**: Always visible in bottom-left corner + +## Visual Layout + +``` +┌───────────────────────────────────┐ +│ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ │ │ │ │ │ │ +│ │ [img]│ │ [img]│ │ [img]│ │ +│ │ 👤 │ │ 📷 │ │ 💡 │ │ +│ └──────┘ └──────┘ └──────┘ │ +│ Char Scene Prop │ +└───────────────────────────────────┘ +``` + +- Top-right: Delete button (X) - appears on hover +- Bottom-left: Type icon - always visible + +## Styling + +The component uses Tailwind CSS with custom dark theme colors: + +- **Primary Color**: `cyan-400` for type icons +- **Text Colors**: White with varying opacity +- **Borders**: White with 10% opacity, hover to cyan-400 with 60% opacity +- **Backgrounds**: Black/White with low opacity + backdrop blur +- **Delete Button**: Gray (`#636364`) with 80% opacity + +## Notes + +- Component returns `null` when `photos` array is empty +- Upload functionality should be implemented externally +- Each photo should have a unique `id` for optimal React key handling +- Preview uses antd's Image component for full-screen viewing diff --git a/components/pages/create-video/PhotoPreview/index.ts b/components/pages/create-video/PhotoPreview/index.ts new file mode 100644 index 0000000..dbf64db --- /dev/null +++ b/components/pages/create-video/PhotoPreview/index.ts @@ -0,0 +1,6 @@ +/** Export PhotoPreviewSection component */ +export { default as PhotoPreviewSection } from './PhotoPreviewSection'; + +/** Export types */ +export type { PhotoPreviewSectionProps, PhotoItem, PhotoType } from './types'; + diff --git a/components/pages/create-video/PhotoPreview/styles.css b/components/pages/create-video/PhotoPreview/styles.css new file mode 100644 index 0000000..cbf056f --- /dev/null +++ b/components/pages/create-video/PhotoPreview/styles.css @@ -0,0 +1,33 @@ +/** Custom styles for Photo Preview Section */ + +/** Override antd Upload component styles for compact mode */ +.photo-upload-button-compact .ant-upload.ant-upload-select { + width: 4rem !important; + height: 4rem !important; + margin: 0 !important; + padding: 0 !important; + background: transparent !important; + border: none !important; +} + +.photo-upload-button-compact .ant-upload-wrapper { + display: inline-block; +} + +/** Custom scrollbar for photo list */ +.photo-list-scrollbar::-webkit-scrollbar { + height: 4px; +} + +.photo-list-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.photo-list-scrollbar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} + +.photo-list-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); +} \ No newline at end of file diff --git a/components/pages/create-video/PhotoPreview/types.ts b/components/pages/create-video/PhotoPreview/types.ts new file mode 100644 index 0000000..bdff8f0 --- /dev/null +++ b/components/pages/create-video/PhotoPreview/types.ts @@ -0,0 +1,25 @@ +import type { ReactNode } from 'react'; + +/** Photo type enum */ +export type PhotoType = 'character' | 'scene' | 'prop'; + +/** Photo item with type information */ +export interface PhotoItem { + /** Photo URL */ + url: string; + /** Photo type: character, scene, or prop */ + type: PhotoType; + /** Optional unique identifier */ + id?: string; +} + +/** Props for PhotoPreviewSection component */ +export interface PhotoPreviewSectionProps { + /** List of photos with type information */ + photos: PhotoItem[]; + /** Callback when a photo is deleted */ + onDelete?: (index: number) => void; + /** Custom class name */ + className?: string; +} +