|
|
|
|
@@ -305,7 +305,22 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps)
|
|
|
|
|
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);
|
|
|
|
|
// Calculate "cover" manually to avoid squashing if image ratio != 9:16
|
|
|
|
|
const targetRatio = TARGET_W / TARGET_H;
|
|
|
|
|
const imgRatio = img.width / img.height;
|
|
|
|
|
let sx, sy, sw, sh;
|
|
|
|
|
if (imgRatio > targetRatio) {
|
|
|
|
|
sh = img.height;
|
|
|
|
|
sw = img.height * targetRatio;
|
|
|
|
|
sx = (img.width - sw) / 2;
|
|
|
|
|
sy = 0;
|
|
|
|
|
} else {
|
|
|
|
|
sw = img.width;
|
|
|
|
|
sh = img.width / targetRatio;
|
|
|
|
|
sx = 0;
|
|
|
|
|
sy = (img.height - sh) / 2;
|
|
|
|
|
}
|
|
|
|
|
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, TARGET_W, TARGET_H);
|
|
|
|
|
}
|
|
|
|
|
ctx.restore();
|
|
|
|
|
}
|
|
|
|
|
@@ -359,156 +374,172 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-[200] bg-black/90 backdrop-blur-xl flex items-center justify-center lg:p-12">
|
|
|
|
|
<motion.div initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="w-full h-full lg:max-h-[850px] lg:max-w-[1200px] bg-[#070707] lg:rounded-[3rem] shadow-[0_50px_100px_-30px_rgba(0,0,0,0.8)] lg:border lg:border-white/10 flex flex-col overflow-hidden relative">
|
|
|
|
|
<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');
|
|
|
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
|
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
|
|
|
`}</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-6">
|
|
|
|
|
<header className="h-16 lg:h-20 px-4 lg:px-8 flex items-center justify-between border-b border-white/5 bg-[#0a0a0a] z-50 shadow-2xl safe-area-top">
|
|
|
|
|
<div className="flex items-center gap-3 lg:gap-6">
|
|
|
|
|
<button onClick={onClose} className="w-10 h-10 rounded-full hover:bg-white/5 flex items-center justify-center text-zinc-400 hover:text-white transition-all">
|
|
|
|
|
<X size={24} />
|
|
|
|
|
</button>
|
|
|
|
|
<h1 className="text-sm font-black uppercase tracking-[0.2em] italic text-zinc-400">{t('storiesEditor')}</h1>
|
|
|
|
|
<h1 className="text-[10px] lg:text-sm font-black uppercase tracking-[0.1em] lg:tracking-[0.2em] italic text-zinc-400 truncate max-w-[120px] lg:max-w-none">{t('storiesEditor')}</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<div className="flex items-center gap-2 lg:gap-4 overflow-x-auto no-scrollbar py-2">
|
|
|
|
|
{mediaFile?.type.startsWith('video/') && (
|
|
|
|
|
<>
|
|
|
|
|
<button onClick={() => setTool('trim')} className={`p-3 rounded-xl border transition-all ${tool === 'trim' ? 'bg-primary/10 border-primary/40 text-primary' : 'bg-white/5 border-white/10 text-zinc-400 hover:text-white'}`} title={t('trimVideo')}>
|
|
|
|
|
<span className="material-symbols-outlined">content_cut</span>
|
|
|
|
|
<span className="ml-2 text-[10px] font-black uppercase tracking-widest">{t('trim')}</span>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button onClick={() => setTool('trim')} className={`p-2 lg:p-3 rounded-xl border transition-all ${tool === 'trim' ? 'bg-primary/10 border-primary/40 text-primary' : 'bg-white/5 border-white/10 text-zinc-400 hover:text-white'}`} title={t('trimVideo')}>
|
|
|
|
|
<span className="material-symbols-outlined text-sm lg:text-base">content_cut</span>
|
|
|
|
|
<span className="ml-1 lg:ml-2 text-[8px] lg:text-[10px] font-black uppercase tracking-widest hidden sm:inline">{t('trim')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setIsMuted(!isMuted)} className={`p-3 rounded-xl border transition-all ${isMuted ? 'bg-red-500/10 border-red-500/40 text-red-500' : 'bg-white/5 border-white/10 text-zinc-400 hover:text-white'}`} title={isMuted ? t("unmuteVideo") : t("muteVideo")}>
|
|
|
|
|
{isMuted ? <Wind size={18} /> : <Droplets size={18} />}
|
|
|
|
|
<span className="ml-2 text-[10px] font-black uppercase tracking-widest">{isMuted ? t('muted') : t('unmuted')}</span>
|
|
|
|
|
<button onClick={() => setIsMuted(!isMuted)} className={`p-2 lg:p-3 rounded-xl border transition-all ${isMuted ? 'bg-red-500/10 border-red-500/40 text-red-500' : 'bg-white/5 border-white/10 text-zinc-400 hover:text-white'}`} title={isMuted ? t("unmuteVideo") : t("muteVideo")}>
|
|
|
|
|
{isMuted ? <Wind size={16} /> : <Droplets size={16} />}
|
|
|
|
|
<span className="ml-1 lg:ml-2 text-[8px] lg:text-[10px] font-black uppercase tracking-widest hidden sm:inline">{isMuted ? t('muted') : t('unmuted')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<button onClick={() => { setStickers([]); setTextObjects([]); setMediaPreview(null); setMediaFile(null); setCroppedPreview(null); clearCanvas(); setBgColor('#1e1e2e'); setIsMuted(false); }} 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">{t('reset')}</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">
|
|
|
|
|
<button onClick={() => { setStickers([]); setTextObjects([]); setMediaPreview(null); setMediaFile(null); setCroppedPreview(null); clearCanvas(); setBgColor('#1e1e2e'); setIsMuted(false); }} className="px-3 lg:px-6 py-2 lg:py-3 text-[8px] lg: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 whitespace-nowrap">{t('reset')}</button>
|
|
|
|
|
<button onClick={handlePublish} disabled={isUploading} className="px-4 lg:px-8 py-2 lg:py-3 bg-primary text-on-primary rounded-xl font-black text-[10px] lg:text-xs uppercase tracking-widest hover:scale-105 active:scale-95 disabled:opacity-50 transition-all shadow-xl shadow-primary/20 whitespace-nowrap">
|
|
|
|
|
{isUploading ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : t('publishStory')}
|
|
|
|
|
</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">
|
|
|
|
|
<main className="flex-1 flex flex-col lg:flex-row overflow-hidden min-h-0">
|
|
|
|
|
<section className="flex-1 flex items-center justify-center p-4 lg:p-12 relative bg-[#070707] min-h-0">
|
|
|
|
|
<div ref={stageRef} className="relative w-auto h-full max-h-[500px] sm:max-h-[600px] lg:max-h-none aspect-[9/16] bg-black rounded-[2.5rem] lg:rounded-[3.5rem] border-[8px] lg: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/') ? (
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center relative" style={{ background: bgColor }}>
|
|
|
|
|
<video
|
|
|
|
|
ref={editorVideoRef}
|
|
|
|
|
src={mediaPreview!}
|
|
|
|
|
className="w-full h-full object-contain"
|
|
|
|
|
autoPlay={false}
|
|
|
|
|
muted={isMuted}
|
|
|
|
|
onTimeUpdate={() => {
|
|
|
|
|
const cur = editorVideoRef.current?.currentTime || 0;
|
|
|
|
|
setCurrentPlayTime(cur);
|
|
|
|
|
if (editorVideoRef.current) {
|
|
|
|
|
if (cur >= endTime) {
|
|
|
|
|
editorVideoRef.current.pause();
|
|
|
|
|
editorVideoRef.current.currentTime = startTime;
|
|
|
|
|
setPaused(true);
|
|
|
|
|
setCurrentPlayTime(startTime);
|
|
|
|
|
} else if (cur < startTime - 0.1) {
|
|
|
|
|
editorVideoRef.current.currentTime = startTime;
|
|
|
|
|
setCurrentPlayTime(startTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onPlay={() => setPaused(false)}
|
|
|
|
|
onPause={() => setPaused(true)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const v = editorVideoRef.current;
|
|
|
|
|
if (v) {
|
|
|
|
|
if (v.currentTime < startTime || v.currentTime > endTime) {
|
|
|
|
|
v.currentTime = startTime;
|
|
|
|
|
}
|
|
|
|
|
if (v.paused) {
|
|
|
|
|
v.play().catch(() => {});
|
|
|
|
|
} else {
|
|
|
|
|
v.pause();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="absolute inset-0 z-20 flex items-center justify-center cursor-pointer group"
|
|
|
|
|
>
|
|
|
|
|
{paused && (
|
|
|
|
|
<motion.div initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="w-20 h-20 rounded-full bg-black/40 backdrop-blur-xl flex items-center justify-center border border-white/20 group-hover:bg-primary/20 group-hover:border-primary/40 transition-all">
|
|
|
|
|
<span className="material-symbols-outlined text-4xl text-white">play_arrow</span>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Bottom Controls (Stop/Seek Placeholder) */}
|
|
|
|
|
<div className="absolute bottom-10 left-0 right-0 z-30 px-10 pointer-events-none">
|
|
|
|
|
<div className="flex items-center gap-4 pointer-events-auto">
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const v = editorVideoRef.current;
|
|
|
|
|
if (v) {
|
|
|
|
|
v.pause();
|
|
|
|
|
v.currentTime = startTime;
|
|
|
|
|
setPaused(true);
|
|
|
|
|
setCurrentPlayTime(startTime);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="w-10 h-10 rounded-xl bg-black/60 backdrop-blur-md flex items-center justify-center text-white border border-white/10 hover:bg-red-500/20 hover:border-red-500/40 transition-all"
|
|
|
|
|
title={t('stop')}
|
|
|
|
|
>
|
|
|
|
|
<span className="material-symbols-outlined">stop</span>
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex-1 h-1.5 bg-white/10 rounded-full overflow-hidden backdrop-blur-md border border-white/5 font-mono">
|
|
|
|
|
<motion.div
|
|
|
|
|
className="h-full bg-primary"
|
|
|
|
|
animate={{ width: `${(( (editorVideoRef.current?.currentTime || 0) - startTime) / (endTime - startTime)) * 100}%` }}
|
|
|
|
|
transition={{ duration: 0.1, ease: 'linear' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
{(croppedPreview || mediaPreview) && (
|
|
|
|
|
<div className="absolute inset-0" style={{ filter: getFilterCss() }}>
|
|
|
|
|
{tool === 'crop' && mediaPreview && !mediaFile?.type.startsWith('video/') ? (
|
|
|
|
|
<Cropper
|
|
|
|
|
image={mediaPreview}
|
|
|
|
|
crop={crop}
|
|
|
|
|
rotation={rotation}
|
|
|
|
|
zoom={zoom}
|
|
|
|
|
aspect={9 / 16}
|
|
|
|
|
onCropChange={setCrop}
|
|
|
|
|
onRotationChange={setRotation}
|
|
|
|
|
onCropComplete={(_, croppedPixels) => setCroppedAreaPixels(croppedPixels)}
|
|
|
|
|
onZoomChange={setZoom}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{mediaFile?.type.startsWith('video/') ? (
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center relative" style={{ background: bgColor }}>
|
|
|
|
|
<video
|
|
|
|
|
ref={editorVideoRef}
|
|
|
|
|
src={mediaPreview!}
|
|
|
|
|
className="w-full h-full object-contain"
|
|
|
|
|
autoPlay={false}
|
|
|
|
|
muted={isMuted}
|
|
|
|
|
onTimeUpdate={() => {
|
|
|
|
|
const cur = editorVideoRef.current?.currentTime || 0;
|
|
|
|
|
setCurrentPlayTime(cur);
|
|
|
|
|
if (editorVideoRef.current) {
|
|
|
|
|
if (cur >= endTime) {
|
|
|
|
|
editorVideoRef.current.pause();
|
|
|
|
|
editorVideoRef.current.currentTime = startTime;
|
|
|
|
|
setPaused(true);
|
|
|
|
|
setCurrentPlayTime(startTime);
|
|
|
|
|
} else if (cur < startTime - 0.1) {
|
|
|
|
|
editorVideoRef.current.currentTime = startTime;
|
|
|
|
|
setCurrentPlayTime(startTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onPlay={() => setPaused(false)}
|
|
|
|
|
onPause={() => setPaused(true)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const v = editorVideoRef.current;
|
|
|
|
|
if (v) {
|
|
|
|
|
if (v.currentTime < startTime || v.currentTime > endTime) {
|
|
|
|
|
v.currentTime = startTime;
|
|
|
|
|
}
|
|
|
|
|
if (v.paused) {
|
|
|
|
|
v.play().catch(() => {});
|
|
|
|
|
} else {
|
|
|
|
|
v.pause();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="absolute inset-0 z-20 flex items-center justify-center cursor-pointer group"
|
|
|
|
|
>
|
|
|
|
|
{paused && (
|
|
|
|
|
<motion.div initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} className="w-16 lg:w-20 h-16 lg:h-20 rounded-full bg-black/40 backdrop-blur-xl flex items-center justify-center border border-white/20 group-hover:bg-primary/20 group-hover:border-primary/40 transition-all">
|
|
|
|
|
<span className="material-symbols-outlined text-3xl lg:text-4xl text-white">play_arrow</span>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="absolute bottom-6 lg:bottom-10 left-0 right-0 z-30 px-6 lg:px-10 pointer-events-none">
|
|
|
|
|
<div className="flex items-center gap-3 lg:gap-4 pointer-events-auto">
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const v = editorVideoRef.current;
|
|
|
|
|
if (v) {
|
|
|
|
|
v.pause();
|
|
|
|
|
v.currentTime = startTime;
|
|
|
|
|
setPaused(true);
|
|
|
|
|
setCurrentPlayTime(startTime);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="w-8 lg:w-10 h-8 lg:h-10 rounded-xl bg-black/60 backdrop-blur-md flex items-center justify-center text-white border border-white/10 hover:bg-red-500/20 hover:border-red-500/40 transition-all"
|
|
|
|
|
title={t('stop')}
|
|
|
|
|
>
|
|
|
|
|
<span className="material-symbols-outlined text-sm lg:text-base">stop</span>
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex-1 h-1 bg-white/10 rounded-full overflow-hidden backdrop-blur-md border border-white/5 font-mono">
|
|
|
|
|
<motion.div
|
|
|
|
|
className="h-full bg-primary"
|
|
|
|
|
animate={{ width: `${(( (editorVideoRef.current?.currentTime || 0) - startTime) / (endTime - startTime)) * 100}%` }}
|
|
|
|
|
transition={{ duration: 0.1, ease: 'linear' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<img src={croppedPreview || mediaPreview!} className="w-full h-full object-cover" alt="base" />
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<button onClick={e => { e.stopPropagation(); removeMedia(); }} className="absolute top-4 lg:top-8 right-4 lg:right-8 w-10 lg:w-12 h-10 lg:h-12 bg-black/60 text-white rounded-xl lg: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={18} 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');}}>
|
|
|
|
|
<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-2 lg:p-4 ${selectedStickerId === s.id ? 'ring-2 ring-primary bg-white/5 rounded-[1.5rem] lg:rounded-[2rem] backdrop-blur-sm' : ''}`} style={{ left: `${s.x}%`, top: `${s.y}%`, fontSize: `${s.scale * 30}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>}
|
|
|
|
|
{selectedStickerId === s.id && <button onClick={(e) => { e.stopPropagation(); setStickers(prev => prev.filter(st => st.id !== s.id)); setSelectedStickerId(null); }} className="absolute -top-2 -right-2 w-6 lg:w-8 h-6 lg:h-8 bg-red-500 rounded-full flex items-center justify-center text-white shadow-xl border-2 border-[#161616]"><X size={12}/></button>}
|
|
|
|
|
</motion.div>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{textObjects.map(tobj => (
|
|
|
|
|
<motion.div key={tobj.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 === tobj.id ? 'ring-2 ring-primary bg-white/5 rounded-[2rem] backdrop-blur-md' : ''}`} style={{ left: `${tobj.x}%`, top: `${tobj.y}%`, color: tobj.color, fontFamily: tobj.font, fontSize: `${tobj.size}px`, transform: `scale(${tobj.scale})` }} onClick={() => {setSelectedTextId(tobj.id); setSelectedStickerId(null); setTool('text');}}>
|
|
|
|
|
<motion.div key={tobj.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-3 lg:p-4 group ${selectedTextId === tobj.id ? 'ring-2 ring-primary bg-white/5 rounded-[1.5rem] lg:rounded-[2rem] backdrop-blur-md' : ''}`} style={{ left: `${tobj.x}%`, top: `${tobj.y}%`, color: tobj.color, fontFamily: tobj.font, fontSize: `${tobj.size / 1.5}px`, transform: `scale(${tobj.scale})` }} onClick={() => {setSelectedTextId(tobj.id); setSelectedStickerId(null); setTool('text');}}>
|
|
|
|
|
{tobj.text}
|
|
|
|
|
{selectedTextId === tobj.id && <button onClick={(e) => { e.stopPropagation(); setTextObjects(prev => prev.filter(to => to.id !== tobj.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>}
|
|
|
|
|
{selectedTextId === tobj.id && <button onClick={(e) => { e.stopPropagation(); setTextObjects(prev => prev.filter(to => to.id !== tobj.id)); setSelectedTextId(null); }} className="absolute -top-2 -right-2 w-6 lg:w-8 h-6 lg:h-8 bg-red-500 rounded-full flex items-center justify-center text-white shadow-xl border-2 border-[#161616]"><X size={12}/></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" />
|
|
|
|
|
<button onClick={() => fileInputRef.current?.click()} className="absolute inset-0 flex flex-col items-center justify-center gap-4 lg:gap-6 text-zinc-800 hover:text-white transition-all group">
|
|
|
|
|
<div className="w-16 lg:w-20 h-16 lg: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={32} className="group-hover:rotate-12 transition-transform" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-[9px] font-black uppercase tracking-[0.4em]">{t('startCreation')}</span>
|
|
|
|
|
<span className="text-[8px] lg:text-[9px] font-black uppercase tracking-[0.3em] lg:tracking-[0.4em]">{t('startCreation')}</span>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
@@ -516,83 +547,99 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps)
|
|
|
|
|
<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">
|
|
|
|
|
<aside className="lg:w-[360px] w-full bg-[#0a0a0a] border-l border-white/5 flex flex-col overflow-y-auto max-h-[40vh] lg:max-h-none">
|
|
|
|
|
<div className="p-4 lg:p-8 space-y-6 lg:space-y-10 pb-20 lg:pb-24">
|
|
|
|
|
<div className="grid grid-cols-5 lg:grid-cols-2 gap-2 lg:gap-3">
|
|
|
|
|
{[
|
|
|
|
|
{ id: 'text', icon: <Type />, label: 'Текст', action: addText },
|
|
|
|
|
{ id: 'stickers', icon: <Smile />, label: t('stickersTool'), active: tool === 'stickers' },
|
|
|
|
|
{ id: 'brush', icon: <Brush />, label: t('brushTool'), active: tool === 'brush' },
|
|
|
|
|
{ id: 'filters', icon: <SlidersHorizontal />, label: t('filtersTool'), active: tool === 'filters' },
|
|
|
|
|
{ id: 'text', icon: <Type size={18} />, label: t('textTool'), action: addText },
|
|
|
|
|
{ id: 'crop', icon: <Scissors size={18} />, label: t('cropTool'), active: tool === 'crop' },
|
|
|
|
|
{ id: 'stickers', icon: <Smile size={18} />, label: t('stickersTool'), active: tool === 'stickers' },
|
|
|
|
|
{ id: 'brush', icon: <Brush size={18} />, label: t('brushTool'), active: tool === 'brush' },
|
|
|
|
|
{ id: 'filters', icon: <SlidersHorizontal size={18} />, label: t('filtersTool'), active: tool === 'filters' },
|
|
|
|
|
].map(item => (
|
|
|
|
|
<button key={item.id} onClick={() => { if(item.id === 'text') { if(item.action) item.action(); setTool('text'); } else { 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'}`}>
|
|
|
|
|
<button key={item.id} onClick={() => { if(item.id === 'text') { if(item.action) item.action(); setTool('text'); } else { setTool(item.id as any); } }} className={`flex flex-col items-center lg:items-center justify-center h-20 lg:h-24 p-2 lg:p-4 rounded-xl lg:rounded-3xl 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.id === 'text' ? t('textTool') : item.label}</span>
|
|
|
|
|
<span className="text-[7px] lg:text-[10px] font-black uppercase tracking-widest mt-2">{item.label}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<AnimatePresence mode="wait">
|
|
|
|
|
{tool === 'crop' && (
|
|
|
|
|
<motion.div key="crop-tool" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} className="p-6 lg:p-8 rounded-3xl lg:rounded-[2.5rem] bg-[#121212] border border-white/5 space-y-6 lg:space-y-8 shadow-2xl">
|
|
|
|
|
<h4 className="text-[9px] lg:text-[10px] font-black uppercase tracking-widest text-primary">{t('cropTool')}</h4>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex justify-between text-[9px] lg:text-[10px] font-bold text-zinc-600 uppercase tracking-widest"><span>{t('zoom')}</span><span className="text-white">{zoom.toFixed(1)}x</span></div>
|
|
|
|
|
<input type="range" min="1" max="3" step="0.1" value={zoom} onChange={e => setZoom(parseFloat(e.target.value))} className="w-full accent-primary h-1 bg-white/5 rounded-full" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex justify-between text-[9px] lg:text-[10px] font-bold text-zinc-600 uppercase tracking-widest"><span>{t('rotation')}</span><span className="text-white">{rotation}°</span></div>
|
|
|
|
|
<input type="range" min="0" max="360" value={rotation} onChange={e => setRotation(parseInt(e.target.value))} className="w-full accent-primary h-1 bg-white/5 rounded-full" />
|
|
|
|
|
</div>
|
|
|
|
|
<button onClick={handleCropDone} className="w-full py-4 lg:py-5 text-[9px] lg:text-[10px] font-black uppercase tracking-[0.1em] lg:tracking-[0.2em] bg-primary text-white rounded-2xl lg:rounded-3xl hover:scale-105 active:scale-95 transition-all shadow-xl shadow-primary/20">{t('apply')}</button>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{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">{t('textContent')}</span>
|
|
|
|
|
<input value={textObjects.find(tx => tx.id === selectedTextId)?.text} onChange={e => setTextObjects(prev => prev.map(tx => tx.id === selectedTextId ? { ...tx, text: e.target.value } : tx))} className="bg-transparent border-b border-white/10 w-full py-2 text-2xl font-black outline-none italic" autoFocus />
|
|
|
|
|
<motion.div key="text-tool" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} className="space-y-4 lg:space-y-8 bg-[#121212] p-6 lg:p-8 rounded-3xl lg:rounded-[2.5rem] border border-white/5 shadow-2xl">
|
|
|
|
|
<div className="flex flex-col gap-2 lg:gap-4">
|
|
|
|
|
<span className="text-[9px] lg:text-[10px] font-black uppercase text-primary tracking-widest">{t('textContent')}</span>
|
|
|
|
|
<input value={textObjects.find(tx => tx.id === selectedTextId)?.text} onChange={e => setTextObjects(prev => prev.map(tx => tx.id === selectedTextId ? { ...tx, text: e.target.value } : tx))} className="bg-transparent border-b border-white/10 w-full py-2 text-xl lg: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">{t('typography')}</span>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<div className="space-y-3 lg:space-y-4">
|
|
|
|
|
<span className="text-[9px] lg:text-[10px] font-black uppercase text-zinc-600 tracking-widest">{t('typography')}</span>
|
|
|
|
|
<div className="flex flex-wrap gap-1.5 lg:gap-2">
|
|
|
|
|
{FONTS.map(f => (
|
|
|
|
|
<button key={f.id} onClick={() => setTextObjects(prev => prev.map(tx => tx.id === selectedTextId ? { ...tx, font: f.family } : tx))} className={`px-5 py-2 rounded-2xl text-[9px] font-black uppercase transition-all ${textObjects.find(tx => tx.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>
|
|
|
|
|
<button key={f.id} onClick={() => setTextObjects(prev => prev.map(tx => tx.id === selectedTextId ? { ...tx, font: f.family } : tx))} className={`px-4 lg:px-5 py-1.5 lg:py-2 rounded-xl lg:rounded-2xl text-[8px] lg:text-[9px] font-black uppercase transition-all ${textObjects.find(tx => tx.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>{t('textSize')}</span><span className="text-white">{textObjects.find(tx => tx.id === selectedTextId)?.size}px</span></div>
|
|
|
|
|
<div className="space-y-3 lg:space-y-4">
|
|
|
|
|
<div className="flex justify-between text-[9px] lg:text-[10px] font-black uppercase text-zinc-600"><span>{t('textSize')}</span><span className="text-white">{textObjects.find(tx => tx.id === selectedTextId)?.size}px</span></div>
|
|
|
|
|
<input type="range" min="12" max="150" value={textObjects.find(tx => tx.id === selectedTextId)?.size} onChange={e => setTextObjects(prev => prev.map(tx => tx.id === selectedTextId ? { ...tx, size: parseInt(e.target.value) } : tx))} 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">{t('textColor')}</span>
|
|
|
|
|
<div className="w-12 h-12 rounded-full border-2 border-white/10 relative shadow-inner" style={{ backgroundColor: textObjects.find(tx => tx.id === selectedTextId)?.color }}>
|
|
|
|
|
<div className="flex items-center justify-between pt-4 lg:pt-6 border-t border-white/5">
|
|
|
|
|
<span className="text-[9px] lg:text-[10px] font-black uppercase text-zinc-600">{t('textColor')}</span>
|
|
|
|
|
<div className="w-10 lg:w-12 h-10 lg:h-12 rounded-full border-2 border-white/10 relative shadow-inner" style={{ backgroundColor: textObjects.find(tx => tx.id === selectedTextId)?.color }}>
|
|
|
|
|
<input type="color" value={textObjects.find(tx => tx.id === selectedTextId)?.color} onChange={e => setTextObjects(prev => prev.map(tx => tx.id === selectedTextId ? { ...tx, color: e.target.value } : tx))} className="absolute inset-0 opacity-0 w-full h-full cursor-pointer" />
|
|
|
|
|
<Pipette size={18} className="absolute inset-0 m-auto mix-blend-difference" />
|
|
|
|
|
<Pipette size={16} 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 }}>
|
|
|
|
|
<motion.div key="brush-tool" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} className="p-6 lg:p-8 rounded-3xl lg:rounded-[2.5rem] bg-[#121212] border border-white/5 space-y-6 lg:space-y-10 text-center shadow-2xl">
|
|
|
|
|
<div className="w-16 lg:w-24 h-16 lg: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" />
|
|
|
|
|
<Pipette size={24} 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>{t('brushThickness')}</span><span className="text-white">{brushSize}px</span></div>
|
|
|
|
|
<div className="flex justify-between text-[9px] lg:text-[10px] font-bold text-zinc-600 uppercase tracking-widest"><span>{t('brushThickness')}</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">{t('clearCanvas')}</button>
|
|
|
|
|
<button onClick={clearCanvas} className="w-full py-4 lg:py-5 text-[9px] lg:text-[10px] font-black uppercase tracking-[0.1em] lg:tracking-[0.2em] text-red-500 bg-red-500/10 rounded-2xl lg:rounded-3xl hover:bg-red-500 hover:text-white transition-all border border-red-500/20">{t('clearCanvas')}</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">
|
|
|
|
|
<motion.div key="sticker-tool" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} className="bg-[#121212] rounded-3xl lg: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">{t('correctionMaster')}</h4>
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<motion.div key="filters-tool" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} className="p-6 lg:p-8 rounded-3xl lg:rounded-[2.5rem] bg-[#121212] border border-white/5 space-y-6 lg:space-y-8 shadow-2xl">
|
|
|
|
|
<h4 className="text-[9px] lg:text-[10px] font-black uppercase tracking-widest text-primary">{t('correctionMaster')}</h4>
|
|
|
|
|
<div className="space-y-4 lg:space-y-6">
|
|
|
|
|
{[
|
|
|
|
|
{ id: 'brightness', icon: <Sun size={14}/>, label: t('brightness'), min: 40, max: 160 },
|
|
|
|
|
{ id: 'contrast', icon: <Contrast size={14}/>, label: t('contrast'), min: 40, max: 160 },
|
|
|
|
|
{ id: 'saturate', icon: <Droplets size={14}/>, label: t('saturation'), min: 0, max: 250 },
|
|
|
|
|
{ id: 'sepia', icon: <Palette size={14}/>, label: t('sepia'), 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 key={adj.id} className="space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between text-[8px] lg: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>
|
|
|
|
|
@@ -604,20 +651,20 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps)
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!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">
|
|
|
|
|
<motion.div key="bg-tool" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6 lg:p-10 rounded-3xl lg:rounded-[2.5rem] bg-[#121212] flex flex-col items-center gap-6 lg: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">{t('bgColor')}</span>
|
|
|
|
|
<Palette size={20} className="text-primary" />
|
|
|
|
|
<span className="text-[9px] lg:text-[10px] font-black uppercase text-zinc-500 tracking-widest">{t('bgColor')}</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 }}>
|
|
|
|
|
<div className="w-20 lg:w-28 h-20 lg: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" />
|
|
|
|
|
<Pipette size={24} className="absolute inset-0 m-auto mix-blend-difference" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-[9px] font-bold text-zinc-700 uppercase">{t('clickToChooseColor')}</span>
|
|
|
|
|
<span className="text-[8px] lg:text-[9px] font-bold text-zinc-700 uppercase">{t('clickToChooseColor')}</span>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
<div className="pt-10 border-t border-white/5" />
|
|
|
|
|
<div className="pt-6 lg:pt-10 border-t border-white/5" />
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</main>
|
|
|
|
|
@@ -957,6 +1004,7 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps)
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
</motion.div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|