@@ -0,0 +1,419 @@
import { useState , useRef , useEffect , useCallback } from 'react' ;
import { motion , AnimatePresence } from 'framer-motion' ;
import { X , Camera , Type , Check , Palette , Smile , Brush , Filter , RotateCcw , Eye , MoreVertical , Globe , Users , Lock , ChevronLeft , Trash2 , Scissors , Music , Eraser , Minus , Plus , Maximize , Trash , SlidersHorizontal , Sun , Contrast , Droplets , Wind , Pipette , Type as FontIcon , AlignCenter } from 'lucide-react' ;
import { useLang } from '../../../../core/infrastructure/i18n' ;
import { ChatApi } from '../../../chats/infrastructure/chatApi' ;
import { StoryApi } from '../../infrastructure/storyApi' ;
import Cropper from 'react-easy-crop' ;
import { getMediaUrl } from '../../../../core/utils/utils' ;
import { useAuthStore } from '../../../auth/application/authStore' ;
import Avatar from '../../../../core/presentation/components/ui/Avatar' ;
import Picker from '@emoji-mart/react' ;
import data from '@emoji-mart/data' ;
const FONTS = [
{ id : 'inter' , name : 'Standard' , family : 'Inter, sans-serif' } ,
{ id : 'serif' , name : 'Playfair' , family : '"Playfair Display", serif' } ,
{ id : 'mono' , name : 'Retro' , family : '"Fira Code", monospace' } ,
{ id : 'script' , name : 'Elegant' , family : '"Dancing Script", cursive' } ,
{ id : 'black' , name : 'Impact' , family : '"Archivo Black", sans-serif' } ,
] ;
interface TextObject {
id : number ;
text : string ;
x : number ;
y : number ;
color : string ;
font : string ;
size : number ;
scale : number ;
}
interface CreateStoryModalProps {
onClose : ( ) = > void ;
onCreated : ( ) = > void ;
}
export function CreateStoryModal ( { onClose , onCreated } : CreateStoryModalProps ) {
const { t } = useLang ( ) ;
const { user } = useAuthStore ( ) ;
const [ mediaFile , setMediaFile ] = useState < File | null > ( null ) ;
const [ mediaPreview , setMediaPreview ] = useState < string | null > ( null ) ;
const [ croppedPreview , setCroppedPreview ] = useState < string | null > ( null ) ;
const [ bgColor , setBgColor ] = useState ( '#1e1e2e' ) ;
const [ isUploading , setIsUploading ] = useState ( false ) ;
const [ tool , setTool ] = useState < 'none' | 'crop' | 'brush' | 'stickers' | 'filters' | 'privacy' | 'text' > ( 'none' ) ;
const [ privacy , setPrivacy ] = useState < 'all' | 'contacts' | 'selected' > ( 'all' ) ;
const [ textObjects , setTextObjects ] = useState < TextObject [ ] > ( [ ] ) ;
const [ selectedTextId , setSelectedTextId ] = useState < number | null > ( null ) ;
const [ stickers , setStickers ] = useState < Array < { id : number , emoji : string , x : number , y : number , scale : number } > > ( [ ] ) ;
const [ selectedStickerId , setSelectedStickerId ] = useState < number | null > ( null ) ;
const [ brushColor , setBrushColor ] = useState ( '#ffffff' ) ;
const [ brushSize , setBrushSize ] = useState ( 8 ) ;
const [ crop , setCrop ] = useState ( { x : 0 , y : 0 } ) ;
const [ zoom , setZoom ] = useState ( 1 ) ;
const [ rotation , setRotation ] = useState ( 0 ) ;
const [ croppedAreaPixels , setCroppedAreaPixels ] = useState < any > ( null ) ;
const [ adjustments , setAdjustments ] = useState ( { brightness : 100 , contrast : 100 , saturate : 100 , sepia : 0 , grayscale : 0 } ) ;
const fileInputRef = useRef < HTMLInputElement > ( null ) ;
const stageRef = useRef < HTMLDivElement > ( null ) ;
const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
const isDrawingRef = useRef ( false ) ;
// Scroll Lock implementation
useEffect ( ( ) = > {
const originalStyle = window . getComputedStyle ( document . body ) . overflow ;
document . body . style . overflow = 'hidden' ;
return ( ) = > {
document . body . style . overflow = originalStyle ;
} ;
} , [ ] ) ;
const TARGET_W = 1080 ;
const TARGET_H = 1920 ;
const SCALE_FACTOR = TARGET_W / 400 ;
const getFilterCss = ( ) = > ` brightness( ${ adjustments . brightness } %) contrast( ${ adjustments . contrast } %) saturate( ${ adjustments . saturate } %) sepia( ${ adjustments . sepia } %) grayscale( ${ adjustments . grayscale } %) ` ;
const handleMediaSelect = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
const file = e . target . files ? . [ 0 ] ;
if ( ! file ) return ;
setMediaFile ( file ) ;
const url = URL . createObjectURL ( file ) ;
setMediaPreview ( url ) ;
setCroppedPreview ( null ) ;
setTool ( 'crop' ) ;
} ;
const removeMedia = ( ) = > {
setMediaFile ( null ) ;
setMediaPreview ( null ) ;
setCroppedPreview ( null ) ;
setCroppedAreaPixels ( null ) ;
setTool ( 'none' ) ;
} ;
const addText = ( ) = > {
const id = Date . now ( ) ;
setTextObjects ( [ . . . textObjects , { id , text : 'Текст' , x : 20 , y : 40 , color : '#ffffff' , font : FONTS [ 0 ] . family , size : 42 , scale : 1 } ] ) ;
setSelectedTextId ( id ) ;
setTool ( 'none' ) ;
setTimeout ( ( ) = > setTool ( 'text' ) , 50 ) ;
} ;
const addSticker = ( emoji : any ) = > {
const id = Date . now ( ) ;
setStickers ( [ . . . stickers , { id , emoji : emoji.native , x : 30 , y : 50 , scale : 1.5 } ] ) ;
setSelectedStickerId ( id ) ;
} ;
const clearCanvas = ( ) = > {
const canvas = canvasRef . current ;
if ( canvas ) {
const ctx = canvas . getContext ( '2d' ) ;
if ( ctx ) ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
}
} ;
const loadImage = ( src : string ) : Promise < HTMLImageElement > = > {
return new Promise ( ( resolve , reject ) = > {
const img = new Image ( ) ;
img . crossOrigin = 'anonymous' ;
img . onload = async ( ) = > {
try {
if ( 'decode' in img ) await img . decode ( ) ;
resolve ( img ) ;
} catch ( e ) {
resolve ( img ) ; // Fallback
}
} ;
img . onerror = ( e ) = > reject ( e ) ;
img . src = src ;
} ) ;
} ;
const handleCropDone = async ( ) = > {
if ( croppedAreaPixels && mediaPreview ) {
try {
const image = await loadImage ( mediaPreview ) ;
const canvas = document . createElement ( 'canvas' ) ;
canvas . width = croppedAreaPixels . width ;
canvas . height = croppedAreaPixels . height ;
const ctx = canvas . getContext ( '2d' ) ;
if ( ! ctx ) return ;
ctx . drawImage ( image , croppedAreaPixels . x , croppedAreaPixels . y , croppedAreaPixels . width , croppedAreaPixels . height , 0 , 0 , croppedAreaPixels . width , croppedAreaPixels . height ) ;
canvas . toBlob ( blob = > {
if ( blob ) {
const url = URL . createObjectURL ( blob ) ;
setCroppedPreview ( url ) ;
}
} , 'image/jpeg' , 0.95 ) ;
} catch ( e ) {
console . error ( 'Crop bake error' , e ) ;
}
}
setTool ( 'none' ) ;
} ;
const handlePublish = async ( ) = > {
setIsUploading ( true ) ;
try {
const canvas = document . createElement ( 'canvas' ) ;
canvas . width = TARGET_W ;
canvas . height = TARGET_H ;
const ctx = canvas . getContext ( '2d' ) ;
if ( ! ctx ) throw new Error ( 'Canvas init failed' ) ;
// 1. BG Color
ctx . fillStyle = bgColor ;
ctx . fillRect ( 0 , 0 , TARGET_W , TARGET_H ) ;
// 2. Base Image Layer
const baseSrc = croppedPreview || mediaPreview ;
if ( baseSrc && ( mediaFile ? . type . startsWith ( 'image/' ) || ! mediaFile ) ) {
const img = await loadImage ( baseSrc ) ;
ctx . save ( ) ;
ctx . filter = getFilterCss ( ) ;
if ( ! croppedPreview && croppedAreaPixels ) {
ctx . drawImage ( img , croppedAreaPixels . x , croppedAreaPixels . y , croppedAreaPixels . width , croppedAreaPixels . height , 0 , 0 , TARGET_W , TARGET_H ) ;
} else {
ctx . drawImage ( img , 0 , 0 , TARGET_W , TARGET_H ) ;
}
ctx . restore ( ) ;
}
// 3. Brush Layer
if ( canvasRef . current ) {
ctx . drawImage ( canvasRef . current , 0 , 0 , TARGET_W , TARGET_H ) ;
}
// 4. Overlays
ctx . textAlign = 'left' ;
ctx . textBaseline = 'middle' ;
stickers . forEach ( s = > {
ctx . font = ` ${ s . scale * 100 } px "Apple Color Emoji", "Segoe UI Emoji", serif ` ;
ctx . fillText ( s . emoji , ( s . x / 100 ) * TARGET_W , ( s . y / 100 ) * TARGET_H ) ;
} ) ;
textObjects . forEach ( t = > {
const fontSize = t . size * SCALE_FACTOR * t . scale ;
ctx . font = ` bold ${ fontSize } px ${ t . font } ` ;
ctx . fillStyle = t . color ;
ctx . fillText ( t . text , ( t . x / 100 ) * TARGET_W , ( t . y / 100 ) * TARGET_H ) ;
} ) ;
const blob = await new Promise < Blob > ( ( resolve , reject ) = > {
canvas . toBlob ( b = > b ? resolve ( b ) : reject ( 'Blob error' ) , 'image/jpeg' , 0.95 ) ;
} ) ;
const fileToUpload = new File ( [ blob ] , ` story_ ${ Date . now ( ) } .jpg ` , { type : 'image/jpeg' } ) ;
const { url } = await ChatApi . uploadFile ( fileToUpload ) ;
if ( ! url ) throw new Error ( 'Upload failed' ) ;
// Using PascalCase matching the DTO just in case, and including original content
await StoryApi . createStory ( {
type : 'image' ,
mediaUrl : url ,
bgColor : bgColor ,
privacy : privacy , // Note: might not be in backend but safe to send
} as any ) ;
onCreated ( ) ;
onClose ( ) ;
} catch ( e ) {
console . error ( 'Publishing error:' , e ) ;
} finally {
setIsUploading ( false ) ;
}
} ;
return (
< motion.div initial = { { opacity : 0 } } animate = { { opacity : 1 } } exit = { { opacity : 0 } } className = "fixed inset-0 z-[200] bg-[#000] flex flex-col font-body text-white overflow-hidden selection:bg-primary/20" >
< style > { `
@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@700&family=Playfair+Display:ital,wght@1,900&family=Fira+Code:wght@700&family=Archivo+Black&display=swap');
` } < / style >
< header className = "h-20 px-8 flex items-center justify-between border-b border-white/5 bg-[#0a0a0a] z-50 shadow-2xl" >
< div className = "flex items-center gap-3" >
< div className = "w-10 h-10 rounded-xl bg-primary/20 flex items-center justify-center text-primary border border-primary/20 italic font-black shadow-[0_0_20px_rgba(var(--primary-rgb),0.3)]" > S < / div >
< h1 className = "text-sm font-black uppercase tracking-widest italic text-zinc-400" > Stories Editor < / h1 >
< / div >
< div className = "flex items-center gap-4" >
< button onClick = { ( ) = > { setStickers ( [ ] ) ; setTextObjects ( [ ] ) ; setMediaPreview ( null ) ; setMediaFile ( null ) ; setCroppedPreview ( null ) ; clearCanvas ( ) ; setBgColor ( '#1e1e2e' ) ; } } className = "px-6 py-3 text-[10px] font-black uppercase tracking-widest text-zinc-500 hover:text-white border border-white/5 rounded-xl hover:bg-white/5 transition-all" > С б р о с < / button >
< button onClick = { handlePublish } disabled = { isUploading } className = "px-8 py-3 bg-primary text-on-primary rounded-xl font-black text-xs uppercase tracking-widest hover:scale-105 active:scale-95 disabled:opacity-50 transition-all shadow-xl shadow-primary/20" >
{ isUploading ? < div className = "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" / > : 'Опубликовать' }
< / button >
< / div >
< / header >
< main className = "flex-1 flex overflow-hidden" >
< section className = "flex-1 flex items-center justify-center p-12 relative bg-[#070707]" >
< div ref = { stageRef } className = "relative w-full max-w-[400px] aspect-[9/16] bg-black rounded-[3.5rem] border-[12px] border-[#161616] overflow-hidden shadow-[0_50px_100px_-30px_rgba(0,0,0,0.9)] ring-1 ring-white/5" >
< div className = "absolute inset-0 transition-colors duration-700" style = { { backgroundColor : bgColor } } >
{ ( croppedPreview || mediaPreview ) && (
< div className = "absolute inset-0" style = { { filter : getFilterCss ( ) } } >
{ mediaFile ? . type . startsWith ( 'video/' ) ? (
< video src = { mediaPreview ! } className = "w-full h-full object-cover" autoPlay muted loop / >
) : (
< img src = { croppedPreview || mediaPreview ! } className = "w-full h-full object-cover" alt = "base" / >
) }
< button onClick = { e = > { e . stopPropagation ( ) ; removeMedia ( ) ; } } className = "absolute top-8 right-8 w-12 h-12 bg-black/60 text-white rounded-2xl hover:bg-red-500 transition-all z-40 backdrop-blur-md flex items-center justify-center border border-white/10 group" > < Trash2 size = { 20 } className = "group-hover:scale-110 transition-transform" / > < / button >
< / div >
) }
< canvas ref = { canvasRef } width = { TARGET_W } height = { TARGET_H } className = { ` absolute inset-0 z-10 w-full h-full ${ tool === 'brush' ? 'cursor-crosshair' : 'pointer-events-none' } ` } onMouseDown = { e = > { isDrawingRef . current = true ; const canvas = canvasRef . current ; if ( ! canvas ) return ; const ctx = canvas . getContext ( '2d' ) ; if ( ! ctx ) return ; const rect = canvas . getBoundingClientRect ( ) ; const x = ( e . clientX - rect . left ) * ( canvas . width / rect . width ) ; const y = ( e . clientY - rect . top ) * ( canvas . height / rect . height ) ; ctx . beginPath ( ) ; ctx . moveTo ( x , y ) ; ctx . strokeStyle = brushColor ; ctx . lineWidth = brushSize * SCALE_FACTOR ; ctx . lineCap = 'round' ; } } onMouseMove = { e = > { if ( ! isDrawingRef . current ) return ; const canvas = canvasRef . current ; if ( ! canvas ) return ; const ctx = canvas . getContext ( '2d' ) ; if ( ! ctx ) return ; const rect = canvas . getBoundingClientRect ( ) ; const x = ( e . clientX - rect . left ) * ( canvas . width / rect . width ) ; const y = ( e . clientY - rect . top ) * ( canvas . height / rect . height ) ; ctx . lineTo ( x , y ) ; ctx . stroke ( ) ; } } onMouseUp = { ( ) = > isDrawingRef . current = false } / >
< AnimatePresence >
{ stickers . map ( s = > (
< motion.div key = { s . id } drag dragConstraints = { stageRef } dragMomentum = { false } initial = { { scale : 0 } } animate = { { scale : 1 } } exit = { { scale : 0 } } className = { ` absolute z-20 cursor-grab active:cursor-grabbing p-4 ${ selectedStickerId === s . id ? 'ring-2 ring-primary bg-white/5 rounded-[2rem] backdrop-blur-sm' : '' } ` } style = { { left : ` ${ s . x } % ` , top : ` ${ s . y } % ` , fontSize : ` ${ s . scale * 40 } px ` } } onClick = { ( ) = > { setSelectedStickerId ( s . id ) ; setSelectedTextId ( null ) ; setTool ( 'stickers' ) ; } } >
{ s . emoji }
{ selectedStickerId === s . id && < button onClick = { ( e ) = > { e . stopPropagation ( ) ; setStickers ( prev = > prev . filter ( st = > st . id !== s . id ) ) ; setSelectedStickerId ( null ) ; } } className = "absolute -top-3 -right-3 w-8 h-8 bg-red-500 rounded-full flex items-center justify-center text-white shadow-xl border-2 border-[#161616]" > < X size = { 16 } / > < / button > }
< / motion.div >
) ) }
{ textObjects . map ( t = > (
< motion.div key = { t . id } drag dragConstraints = { stageRef } dragMomentum = { false } initial = { { opacity : 0 , y : 10 } } animate = { { opacity : 1 , y : 0 } } className = { ` absolute z-30 cursor-grab active:cursor-grabbing p-4 group ${ selectedTextId === t . id ? 'ring-2 ring-primary bg-white/5 rounded-[2rem] backdrop-blur-md' : '' } ` } style = { { left : ` ${ t . x } % ` , top : ` ${ t . y } % ` , color : t.color , fontFamily : t.font , fontSize : ` ${ t . size } px ` , transform : ` scale( ${ t . scale } ) ` } } onClick = { ( ) = > { setSelectedTextId ( t . id ) ; setSelectedStickerId ( null ) ; setTool ( 'text' ) ; } } >
{ t . text }
{ selectedTextId === t . id && < button onClick = { ( e ) = > { e . stopPropagation ( ) ; setTextObjects ( prev = > prev . filter ( to = > to . id !== t . id ) ) ; setSelectedTextId ( null ) ; } } className = "absolute -top-3 -right-3 w-8 h-8 bg-red-500 rounded-full flex items-center justify-center text-white shadow-xl border-2 border-[#161616]" > < X size = { 16 } / > < / button > }
< / motion.div >
) ) }
< / AnimatePresence >
{ ! mediaPreview && textObjects . length === 0 && stickers . length === 0 && (
< button onClick = { ( ) = > fileInputRef . current ? . click ( ) } className = "absolute inset-0 flex flex-col items-center justify-center gap-6 text-zinc-800 hover:text-white transition-all group" >
< div className = "w-20 h-20 rounded-full border-2 border-dashed border-zinc-800 flex items-center justify-center group-hover:border-primary group-hover:bg-primary/5 group-hover:scale-110 transition-all duration-500" >
< Camera size = { 40 } className = "group-hover:rotate-12 transition-transform" / >
< / div >
< span className = "text-[9px] font-black uppercase tracking-[0.4em]" > Н а ч а т ь с о з д а н и е < / span >
< / button >
) }
< / div >
{ tool === 'crop' && mediaPreview && (
< div className = "absolute inset-0 z-[100] bg-black" >
< Cropper image = { mediaPreview } crop = { crop } zoom = { zoom } rotation = { rotation } aspect = { 9 / 16 } onCropChange = { setCrop } onZoomChange = { setZoom } onCropComplete = { ( _ , pixels ) = > setCroppedAreaPixels ( pixels ) } / >
< button onClick = { handleCropDone } className = "absolute bottom-10 left-1/2 -translate-x-1/2 px-10 py-4 bg-primary text-white rounded-full font-black text-xs uppercase z-[110] shadow-2xl hover:scale-105 active:scale-95 transition-all" > Г о т о в о < / button >
< / div >
) }
< / div >
< input ref = { fileInputRef } type = "file" accept = "image/*,video/*" className = "hidden" onChange = { handleMediaSelect } / >
< / section >
< aside className = "w-[440px] bg-[#0a0a0a] border-l border-white/5 flex flex-col overflow-y-auto" >
< div className = "p-10 space-y-12 pb-24" >
< div className = "grid grid-cols-2 gap-4" >
{ [
{ id : 'media' , icon : < Camera / > , label : 'Медиа' , action : ( ) = > fileInputRef . current ? . click ( ) } ,
{ id : 'text' , icon : < Type / > , label : 'Текст' , action : addText } ,
{ id : 'stickers' , icon : < Smile / > , label : 'Стикеры' , active : tool === 'stickers' } ,
{ id : 'brush' , icon : < Brush / > , label : 'Кисти' , active : tool === 'brush' } ,
{ id : 'filters' , icon : < SlidersHorizontal / > , label : 'Коррекция' , active : tool === 'filters' } ,
] . map ( item = > (
< button key = { item . id } onClick = { ( ) = > { if ( item . action ) item . action ( ) ; setTool ( item . id as any ) ; } } className = { ` flex flex-col items-start justify-between h-32 p-7 rounded-[2.5rem] border transition-all duration-500 ${ tool === item . id ? 'bg-primary/10 border-primary/40 text-white shadow-[0_15px_40px_-10px_rgba(var(--primary-rgb),0.3)]' : 'bg-[#121212] border-white/5 text-zinc-500 hover:text-white hover:border-white/10' } ` } >
< div className = { tool === item . id ? 'text-primary' : 'text-zinc-700' } > { item . icon } < / div >
< span className = "text-[10px] font-black uppercase tracking-widest" > { item . label } < / span >
< / button >
) ) }
< / div >
< AnimatePresence mode = "wait" >
{ tool === 'text' && selectedTextId && (
< motion.div key = "text-tool" initial = { { opacity : 0 , x : 20 } } animate = { { opacity : 1 , x : 0 } } exit = { { opacity : 0 , x : - 20 } } className = "space-y-8 bg-[#121212] p-8 rounded-[2.5rem] border border-white/5 shadow-2xl" >
< div className = "flex flex-col gap-4" >
< span className = "text-[10px] font-black uppercase text-primary tracking-widest" > К о н т е н т < / span >
< input value = { textObjects . find ( t = > t . id === selectedTextId ) ? . text } onChange = { e = > setTextObjects ( prev = > prev . map ( t = > t . id === selectedTextId ? { . . . t , text : e.target.value } : t ) ) } className = "bg-transparent border-b border-white/10 w-full py-2 text-2xl font-black outline-none italic" autoFocus / >
< / div >
< div className = "space-y-4" >
< span className = "text-[10px] font-black uppercase text-zinc-600 tracking-widest" > Т и п о г р а ф и к а < / span >
< div className = "flex flex-wrap gap-2" >
{ FONTS . map ( f = > (
< button key = { f . id } onClick = { ( ) = > setTextObjects ( prev = > prev . map ( t = > t . id === selectedTextId ? { . . . t , font : f.family } : t ) ) } className = { ` px-5 py-2 rounded-2xl text-[9px] font-black uppercase transition-all ${ textObjects . find ( t = > t . id === selectedTextId ) ? . font === f . family ? 'bg-primary text-white shadow-lg' : 'bg-white/5 text-zinc-500 hover:text-white' } ` } style = { { fontFamily : f.family } } > { f . name } < / button >
) ) }
< / div >
< / div >
< div className = "space-y-4" >
< div className = "flex justify-between text-[10px] font-black uppercase text-zinc-600" > < span > Р а з м е р < / span > < span className = "text-white" > { textObjects . find ( t = > t . id === selectedTextId ) ? . size } px < / span > < / div >
< input type = "range" min = "12" max = "150" value = { textObjects . find ( t = > t . id === selectedTextId ) ? . size } onChange = { e = > setTextObjects ( prev = > prev . map ( t = > t . id === selectedTextId ? { . . . t , size : parseInt ( e . target . value ) } : t ) ) } className = "w-full accent-primary h-1 bg-white/5 rounded-full" / >
< / div >
< div className = "flex items-center justify-between pt-6 border-t border-white/5" >
< span className = "text-[10px] font-black uppercase text-zinc-600" > Ц в е т т е к с т а < / span >
< div className = "w-12 h-12 rounded-full border-2 border-white/10 relative shadow-inner" style = { { backgroundColor : textObjects.find ( t = > t . id === selectedTextId ) ? . color } } >
< input type = "color" value = { textObjects . find ( t = > t . id === selectedTextId ) ? . color } onChange = { e = > setTextObjects ( prev = > prev . map ( t = > t . id === selectedTextId ? { . . . t , color : e.target.value } : t ) ) } className = "absolute inset-0 opacity-0 w-full h-full cursor-pointer" / >
< Pipette size = { 18 } className = "absolute inset-0 m-auto mix-blend-difference" / >
< / div >
< / div >
< / motion.div >
) }
{ tool === 'brush' && (
< motion.div key = "brush-tool" initial = { { opacity : 0 , x : 20 } } animate = { { opacity : 1 , x : 0 } } exit = { { opacity : 0 , x : - 20 } } className = "p-8 rounded-[2.5rem] bg-[#121212] border border-white/5 space-y-10 text-center shadow-2xl" >
< div className = "w-24 h-24 rounded-full mx-auto border-4 border-white/10 relative shadow-inner cursor-pointer group" style = { { backgroundColor : brushColor } } >
< input type = "color" value = { brushColor } onChange = { e = > setBrushColor ( e . target . value ) } className = "absolute inset-0 opacity-0 w-full h-full cursor-pointer" / >
< Pipette size = { 32 } className = "absolute inset-0 m-auto mix-blend-difference group-hover:scale-125 transition-transform" / >
< / div >
< div className = "space-y-4" >
< div className = "flex justify-between text-[10px] font-bold text-zinc-600 uppercase tracking-widest" > < span > Т о л щ и н а < / span > < span className = "text-white" > { brushSize } px < / span > < / div >
< input type = "range" min = "1" max = "100" value = { brushSize } onChange = { e = > setBrushSize ( parseInt ( e . target . value ) ) } className = "w-full accent-primary h-1 bg-white/5 rounded-full" / >
< / div >
< button onClick = { clearCanvas } className = "w-full py-5 text-[10px] font-black uppercase tracking-[0.2em] text-red-500 bg-red-500/10 rounded-3xl hover:bg-red-500 hover:text-white transition-all border border-red-500/20" > О ч и с т и т ь х о л с т < / button >
< / motion.div >
) }
{ tool === 'stickers' && (
< motion.div key = "sticker-tool" initial = { { opacity : 0 , x : 20 } } animate = { { opacity : 1 , x : 0 } } exit = { { opacity : 0 , x : - 20 } } className = "bg-[#121212] rounded-[2.5rem] border border-white/5 overflow-hidden shadow-2xl" >
< Picker data = { data } onEmojiSelect = { addSticker } theme = "dark" set = "native" locale = "ru" previewPosition = "none" skinTonePosition = "none" / >
< / motion.div >
) }
{ tool === 'filters' && (
< motion.div key = "filters-tool" initial = { { opacity : 0 , x : 20 } } animate = { { opacity : 1 , x : 0 } } exit = { { opacity : 0 , x : - 20 } } className = "p-8 rounded-[2.5rem] bg-[#121212] border border-white/5 space-y-8 shadow-2xl" >
< h4 className = "text-[10px] font-black uppercase tracking-widest text-primary" > М а с т е р к о р р е к ц и и < / h4 >
< div className = "space-y-6" >
{ [
{ id : 'brightness' , icon : < Sun size = { 14 } / > , label : 'Яркость' , min : 40 , max : 160 } ,
{ id : 'contrast' , icon : < Contrast size = { 14 } / > , label : 'Контраст' , min : 40 , max : 160 } ,
{ id : 'saturate' , icon : < Droplets size = { 14 } / > , label : 'Насыщенность' , min : 0 , max : 250 } ,
{ id : 'sepia' , icon : < Palette size = { 14 } / > , label : 'Сепия' , min : 0 , max : 100 } ,
] . map ( adj = > (
< div key = { adj . id } className = "space-y-3" >
< div className = "flex items-center justify-between text-[10px] font-bold text-zinc-700 uppercase" >
< div className = "flex items-center gap-2" > { adj . icon } { adj . label } < / div >
< span className = "text-white" > { adjustments [ adj . id as keyof typeof adjustments ] } % < / span >
< / div >
< input type = "range" min = { adj . min } max = { adj . max } value = { adjustments [ adj . id as keyof typeof adjustments ] } onChange = { e = > setAdjustments ( prev = > ( { . . . prev , [ adj . id ] : parseInt ( e . target . value ) } ) ) } className = "w-full accent-primary h-1 bg-white/5 rounded-full" / >
< / div >
) ) }
< / div >
< / motion.div >
) }
{ ! mediaPreview && tool === 'none' && (
< motion.div key = "bg-tool" initial = { { opacity : 0 , y : 20 } } animate = { { opacity : 1 , y : 0 } } className = "p-10 rounded-[2.5rem] bg-[#121212] flex flex-col items-center gap-8 border border-white/5 shadow-2xl" >
< div className = "flex flex-col items-center gap-2" >
< Palette size = { 24 } className = "text-primary" / >
< span className = "text-[10px] font-black uppercase text-zinc-500 tracking-widest" > Ц в е т ф о н а < / span >
< / div >
< div className = "w-28 h-28 rounded-full border-[6px] border-white/5 relative shadow-inner transition-transform hover:scale-105 active:scale-95 cursor-pointer" style = { { backgroundColor : bgColor } } >
< input type = "color" value = { bgColor } onChange = { e = > setBgColor ( e . target . value ) } className = "absolute inset-0 opacity-0 w-full h-full cursor-pointer" / >
< Pipette size = { 36 } className = "absolute inset-0 m-auto mix-blend-difference" / >
< / div >
< span className = "text-[9px] font-bold text-zinc-700 uppercase" > Н а ж м и т е д л я в ы б о р а ц в е т а < / span >
< / motion.div >
) }
< / AnimatePresence >
< div className = "pt-10 border-t border-white/5" / >
< / div >
< / aside >
< / main >
< / motion.div >
) ;
}