Минимальная адаптация под мобилку

This commit is contained in:
Халимов Рустам
2026-04-07 12:29:41 +03:00
parent 852efa090e
commit 02043d4d97
13 changed files with 309 additions and 180 deletions

View File

@@ -2,6 +2,8 @@
<html lang="ru" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>Knot Messenger</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

View File

@@ -248,6 +248,7 @@ const translations = {
you: 'вы',
// Stories Editor
cropVideo: 'Обрезка видео',
cropTool: 'Обрезка',
trimVideo: 'Обрезать видео',
trim: 'Обрезать',
reset: 'Сбросить',
@@ -279,6 +280,8 @@ const translations = {
unmuteVideo: 'Включить звук',
interactive: 'Интерактив',
apply: 'Применить',
zoom: 'Масштаб',
rotation: 'Поворот',
// User profile
mediaTab: 'Медиа',
gifs: 'GIF',
@@ -622,6 +625,7 @@ const translations = {
you: 'you',
// Stories Editor
cropVideo: 'Video Crop',
cropTool: 'Crop',
trimVideo: 'Trim Video',
trim: 'Trim',
reset: 'Reset',
@@ -653,6 +657,8 @@ const translations = {
unmuteVideo: 'Unmute Video',
interactive: 'Interactive',
apply: 'Apply',
zoom: 'Zoom',
rotation: 'Rotation',
// User profile
mediaTab: 'Media',
gifs: 'GIF',

View File

@@ -99,8 +99,16 @@ export default function ImageLightbox({ url, images, initialIndex = 0, onClose }
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ duration: 0.2 }}
drag={gallery && total > 1 ? "x" : false}
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.4}
onDragEnd={(_, info) => {
const swipeThreshold = 50;
if (info.offset.x > swipeThreshold) goPrev();
else if (info.offset.x < -swipeThreshold) goNext();
}}
onClick={(e) => e.stopPropagation()}
className="absolute inset-x-0 inset-y-12 flex items-center justify-center p-4"
className="absolute inset-x-0 inset-y-12 flex items-center justify-center p-4 touch-none"
>
{(currentType === 'video' || currentType === 'gif') ? (
<video

View File

@@ -16,41 +16,43 @@ export default function GlobalNavBar({ activeTab, onTabChange }: GlobalNavBarPro
];
return (
<nav className="fixed left-0 top-0 h-full z-40 bg-surface-container w-20 flex flex-col items-center py-6 slide-on-ice border-none shadow-xl">
<div className="mb-10 flex flex-col items-center">
<nav
className="lg:fixed lg:left-0 lg:top-0 lg:h-[100dvh] lg:w-20 w-full h-16 fixed bottom-0 left-0 bg-surface-container-low border-t lg:border-t-0 lg:border-r border-white/5 flex lg:flex-col flex-row items-center justify-around lg:justify-start lg:py-8 lg:gap-4 z-50 transition-all safe-area-bottom"
>
<div className="hidden lg:flex mb-10 flex-col items-center">
<span className="text-2xl font-black text-primary tracking-tighter italic knot-logo-spin">Knot</span>
</div>
<div className="flex flex-col gap-6 w-full px-2">
<div className="flex flex-row lg:flex-col gap-2 lg:gap-6 w-full lg:px-2 items-center justify-around lg:justify-start">
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => onTabChange(item.id)}
className={`flex flex-col items-center justify-center p-3 rounded-xl transition-all duration-300 scale-95 active:scale-90 ${
className={`flex flex-col items-center justify-center p-2 lg:p-3 rounded-xl transition-all duration-300 scale-95 lg:scale-100 active:scale-90 flex-1 lg:flex-none ${
activeTab === item.id
? 'bg-primary/10 text-primary'
: 'text-on-surface-variant hover:bg-surface-container-high hover:text-primary'
}`}
>
<span
className={`material-symbols-outlined mb-1 ${activeTab === item.id ? 'fill-1' : ''}`}
className={`material-symbols-outlined mb-0.5 lg:mb-1 ${activeTab === item.id ? 'fill-1' : ''}`}
style={{ fontVariationSettings: `'FILL' ${activeTab === item.id ? 1 : 0}` }}
>
{item.icon}
</span>
<span className="text-[10px] font-medium">{item.label}</span>
<span className="text-[9px] lg:text-[10px] font-bold uppercase tracking-wider">{item.label}</span>
</button>
))}
</div>
<div
className="mt-auto group cursor-pointer relative"
className="lg:mt-auto group cursor-pointer relative flex items-center justify-center px-4 lg:px-0"
onClick={() => onTabChange('settings')}
>
<div className="absolute -inset-1 bg-primary/20 rounded-2xl opacity-0 group-hover:opacity-100 blur transition-opacity" />
<img
alt="User Profile"
className="relative w-10 h-10 rounded-2xl border-2 border-outline-variant/10 hover:border-primary/50 transition-colors object-cover"
className="relative w-8 h-8 lg:w-10 lg:h-10 rounded-2xl border-2 border-outline-variant/10 hover:border-primary/50 transition-colors object-cover"
src={user?.avatar || `https://ui-avatars.com/api/?name=${user?.username || 'user'}&background=3096e5&color=fff`}
/>
</div>

View File

@@ -662,7 +662,7 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile, onOpenTelegra
animate={{ x: 0, opacity: 1 }}
exit={{ x: -320, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="fixed left-3 top-3 bottom-3 w-[340px] max-w-[calc(100vw-24px)] bg-surface-secondary/95 backdrop-blur-3xl shadow-[0_0_100px_rgba(0,0,0,0.5)] border border-border/50 rounded-3xl z-50 flex flex-col overflow-hidden"
className="fixed inset-0 lg:left-3 lg:top-3 lg:bottom-3 w-full lg:w-[340px] lg:max-w-[calc(100vw-24px)] bg-surface-secondary/95 backdrop-blur-3xl shadow-[0_0_100px_rgba(0,0,0,0.5)] border-none lg:border lg:border-border/50 lg:rounded-3xl z-50 flex flex-col overflow-hidden"
>
<AnimatePresence mode="wait" custom={slideDir}>
{view === 'main' && renderMain()}

View File

@@ -200,7 +200,7 @@ export default function Sidebar() {
</div>
{/* Список чатов */}
<div className="flex-1 overflow-y-auto px-2 custom-scrollbar">
<div className="flex-1 overflow-y-auto px-2 custom-scrollbar pb-28 lg:pb-4">
{filteredChats.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-on-surface-variant/40 gap-3 px-6">
<span className="material-symbols-outlined text-5xl opacity-20">chat_bubble</span>
@@ -218,10 +218,10 @@ export default function Sidebar() {
{/* Float Action Button equivalent for Web */}
<button
onClick={() => setShowNewChat(true)}
className="absolute bottom-6 right-6 w-14 h-14 rounded-2xl bg-primary text-black shadow-lg shadow-primary/20 flex items-center justify-center hover:scale-105 active:scale-95 transition-transform slide-on-ice"
className="absolute bottom-20 lg:bottom-10 right-4 lg:right-10 w-12 h-12 lg:w-16 lg:h-16 rounded-2xl lg:rounded-3xl bg-primary text-black shadow-[0_10px_30px_-5px_rgba(var(--primary-rgb),0.4)] flex items-center justify-center hover:scale-105 active:scale-95 transition-all slide-on-ice z-30"
title={t('newChat')}
>
<span className="material-symbols-outlined text-2xl">add</span>
<span className="material-symbols-outlined text-2xl lg:text-3xl">add</span>
</button>
</div>

View File

@@ -267,4 +267,31 @@ input:-webkit-autofill:active {
border-radius: 12px;
position: relative;
z-index: 10;
}
/* Mobile & Safe Area Utilities */
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@supports (padding: env(safe-area-inset-top)) {
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
}

View File

@@ -373,11 +373,13 @@ export default function ChatPage() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="h-screen w-screen flex bg-surface-dim overflow-hidden antialiased font-body selection:bg-primary/30"
className="h-[100dvh] w-screen flex flex-col lg:flex-row bg-surface-dim overflow-hidden"
>
<GlobalNavBar activeTab={activeTab} onTabChange={setActiveTab} />
<div className={`${activeChat ? 'hidden lg:block' : 'block'}`}>
<GlobalNavBar activeTab={activeTab} onTabChange={setActiveTab} />
</div>
<main className="ml-20 flex-1 flex flex-row relative h-full">
<main className={`lg:ml-20 flex-1 flex flex-row relative h-full safe-area-bottom`}>
{activeTab === 'chats' ? (
<>
{/* Chat List (Sidebar) */}

View File

@@ -900,13 +900,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
openSearch();
}
}}
className="p-2 rounded-xl hover:bg-surface-container-highest/40 transition-all duration-300 text-on-surface-variant hover:text-primary slide-on-ice"
className="hidden lg:flex p-2 rounded-xl hover:bg-surface-container-highest/40 transition-all duration-300 text-on-surface-variant hover:text-primary slide-on-ice"
>
{showSearch ? <span className="material-symbols-outlined">close</span> : <span className="material-symbols-outlined">search</span>}
</button>
{!isFavorites && config?.webRtc?.enabled && (
<>
<div className="hidden lg:flex items-center gap-1.5">
<button
onClick={() => {
if (chat.type === 'personal' && otherMember) {
@@ -931,7 +931,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
<span className="material-symbols-outlined">videocam</span>
</button>
)}
</>
</div>
)}
<div className="relative" ref={topMenuRef}>
@@ -957,6 +957,40 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
<Search size={16} />
{t('searchMessages')}
</button>
{!isFavorites && config?.webRtc?.enabled && (
<>
<button
onClick={() => {
setShowTopMenu(false);
if (chat.type === 'personal' && otherMember) {
onStartCall?.(otherMember.user, 'voice');
} else if (chat.type === 'group') {
onStartGroupCall?.(chat.id, chat.name || 'Group', 'voice');
}
}}
className="lg:hidden flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Phone size={16} />
{t('call')}
</button>
{config?.webRtc?.enableVideoCalls && (
<button
onClick={() => {
setShowTopMenu(false);
if (chat.type === 'personal' && otherMember) {
onStartCall?.(otherMember.user, 'video');
} else if (chat.type === 'group') {
onStartGroupCall?.(chat.id, chat.name || 'Group', 'video');
}
}}
className="lg:hidden flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Video size={16} />
{t('videoCall')}
</button>
)}
</>
)}
{!isFavorites && chat.type === 'personal' && otherMember && (
<button
onClick={() => {
@@ -1337,7 +1371,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
</AnimatePresence>
</div>
<footer className="flex-shrink-0 bg-surface-container-lowest/40 backdrop-blur-xl border-t border-white/5 pb-safe relative z-50">
<footer className="flex-shrink-0 bg-surface-container-lowest/40 backdrop-blur-xl border-t border-white/5 safe-area-bottom relative z-50">
<MessageInput chatId={activeChat} />
</footer>
</>

View File

@@ -338,7 +338,7 @@ export default function GroupSettings({ chat, onClose, onGoToMessage }: GroupSet
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 50, scale: 0.95 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="fixed right-3 top-3 bottom-3 w-[900px] max-w-[calc(100%-24px)] bg-surface-secondary/90 backdrop-blur-3xl shadow-2xl shadow-black/80 border border-white/5 rounded-[2rem] z-50 flex flex-col overflow-hidden ring-1 ring-white/10"
className="fixed inset-0 lg:inset-auto lg:right-3 lg:top-3 lg:bottom-3 w-full lg:w-[900px] lg:max-w-[calc(100%-24px)] bg-surface-secondary/90 backdrop-blur-3xl shadow-2xl shadow-black/80 border-none lg:border lg:border-white/5 lg:rounded-[2rem] z-50 flex flex-col overflow-hidden ring-1 ring-white/10"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border/40">

View File

@@ -124,10 +124,10 @@ export default function NewChatModal({ onClose, onOpenTelegramImport }: NewChatM
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
className="fixed inset-0 z-50 flex items-center justify-center lg:p-4"
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<div className="w-full max-w-md rounded-[2.5rem] bg-surface-container/60 backdrop-blur-3xl shadow-[0_48px_80px_-16px_rgba(0,0,0,0.7)] border border-white/5 overflow-hidden flex flex-col" role="dialog" aria-modal="true" aria-label={t('newChat')}>
<div className="w-full lg:max-w-md h-full lg:h-auto lg:rounded-[2.5rem] bg-surface-container/60 backdrop-blur-3xl shadow-[0_48px_80px_-16px_rgba(0,0,0,0.7)] border-none lg:border lg:border-white/5 overflow-hidden flex flex-col" role="dialog" aria-modal="true" aria-label={t('newChat')}>
{/* Шапка */}
<div className="flex items-center justify-between p-7 pb-4">
<div className="flex items-center gap-4">

View File

@@ -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>
);
}

View File

@@ -212,7 +212,7 @@ export default function UserProfile({ userId, onClose, onMessage, isSelf: isSelf
exit={{ opacity: 0, scale: 0.98, y: 15 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
onClick={(e) => e.stopPropagation()}
className="relative w-full max-w-[850px] h-[85vh] bg-[#0a0a0a] rounded-[3rem] border border-white/10 shadow-2xl overflow-hidden flex flex-col backdrop-blur-3xl"
className="relative w-full lg:max-w-[850px] h-full lg:h-[85vh] bg-[#0a0a0a] lg:rounded-[3rem] border-none lg:border lg:border-white/10 shadow-2xl overflow-hidden flex flex-col backdrop-blur-3xl"
>
<div className="flex items-center justify-between px-8 py-5 border-b border-white/5 bg-white/[0.01]">
<span className="text-sm font-black uppercase tracking-[0.3em] text-white/50">{t('userProfileUpper')}</span>