Compare commits

..

2 Commits

Author SHA1 Message Date
Alexey Pavlov 33c11049f1 merge: resolve ChannelView icon conflict, keep History + Search/Camera/ExternalLink/Link2 2026-06-15 10:28:42 +03:00
Alexey Pavlov 5be51d88f7 feat: channel history page — published posts with search 2026-06-15 10:28:07 +03:00
3 changed files with 178 additions and 1 deletions
+28
View File
@@ -0,0 +1,28 @@
import { notFound, redirect } from 'next/navigation';
import { requireUser } from '@/lib/session';
import { engine } from '@/lib/engine';
import Header from '@/components/Header';
import ChannelHistory from '@/components/ChannelHistory';
export default async function ChannelHistoryPage({ params }) {
const user = await requireUser();
if (!user) redirect('/login');
const { id } = await params;
let channel, posts;
try {
channel = await engine.getChannel(user.id, id);
posts = await engine.listUserPosts(user.id, { channel_id: id, status: 'published', limit: 100 });
} catch {
notFound();
}
if (!channel) notFound();
return (
<>
<Header user={user} />
<ChannelHistory channel={channel} posts={posts || []} />
</>
);
}
+144
View File
@@ -0,0 +1,144 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { ArrowLeft, Clock, Image as ImageIcon, Copy, Check, Search } from 'lucide-react';
function timeAgo(dateStr) {
if (!dateStr) return '';
const diff = Date.now() - new Date(dateStr).getTime();
const m = Math.floor(diff / 60000);
if (m < 60) return `${m} мин назад`;
const h = Math.floor(m / 60);
if (h < 24) return `${h} ч назад`;
const d = Math.floor(h / 24);
if (d < 30) return `${d} дн назад`;
return new Date(dateStr).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
}
function PostCard({ p }) {
const [copied, setCopied] = useState(false);
const [expanded, setExpanded] = useState(false);
const preview = p.content?.slice(0, 200) || '';
const isLong = (p.content?.length || 0) > 200;
function copy() {
navigator.clipboard.writeText(p.content || '');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-4 flex flex-col gap-3">
{/* Шапка */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs text-gray-400">
<Clock size={12} />
<span>{timeAgo(p.published_at || p.updated_at || p.created_at)}</span>
</div>
<button
onClick={copy}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
>
{copied ? <Check size={13} className="text-green-500" /> : <Copy size={13} />}
{copied ? 'Скопировано' : 'Копировать'}
</button>
</div>
{/* Картинка */}
{p.image_url && (
<img
src={p.image_url}
alt=""
className="w-full rounded-lg object-cover max-h-48"
/>
)}
{!p.image_url && (
<div className="w-full h-10 rounded-lg bg-gray-50 dark:bg-gray-800 flex items-center gap-2 px-3">
<ImageIcon size={14} className="text-gray-300" />
<span className="text-xs text-gray-300">Без изображения</span>
</div>
)}
{/* Текст */}
<div className="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap leading-relaxed">
{expanded ? p.content : preview}
{isLong && !expanded && <span className="text-gray-400"></span>}
</div>
{isLong && (
<button
onClick={() => setExpanded(v => !v)}
className="text-xs text-indigo-500 hover:text-indigo-700 text-left"
>
{expanded ? 'Свернуть' : 'Показать полностью'}
</button>
)}
</div>
);
}
export default function ChannelHistory({ channel, posts }) {
const [query, setQuery] = useState('');
const filtered = query.trim()
? posts.filter(p => p.content?.toLowerCase().includes(query.toLowerCase()))
: posts;
return (
<main className="max-w-2xl mx-auto px-4 py-8">
{/* Навигация */}
<div className="flex items-center gap-3 mb-6">
<Link
href={`/channels/${channel.id}`}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-800 dark:hover:text-gray-200 transition-colors"
>
<ArrowLeft size={16} />
{channel.name}
</Link>
<span className="text-gray-300 dark:text-gray-700">/</span>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">История публикаций</span>
</div>
{/* Статистика + поиск */}
<div className="flex items-center justify-between gap-4 mb-6">
<p className="text-sm text-gray-500">
{posts.length === 0
? 'Публикаций пока нет'
: `${posts.length} ${posts.length === 1 ? 'публикация' : posts.length < 5 ? 'публикации' : 'публикаций'}`}
</p>
{posts.length > 0 && (
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Поиск по тексту"
value={query}
onChange={e => setQuery(e.target.value)}
className="pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-300 w-48"
/>
</div>
)}
</div>
{/* Список постов */}
{filtered.length === 0 && query ? (
<p className="text-center text-sm text-gray-400 py-12">Ничего не найдено</p>
) : filtered.length === 0 ? (
<div className="text-center py-16">
<Clock size={32} className="mx-auto text-gray-200 dark:text-gray-700 mb-3" />
<p className="text-sm text-gray-400">Опубликованных постов пока нет</p>
<Link
href={`/channels/${channel.id}`}
className="mt-4 inline-block text-sm text-indigo-500 hover:text-indigo-700"
>
Создать первый пост
</Link>
</div>
) : (
<div className="flex flex-col gap-4">
{filtered.map(p => <PostCard key={p.id} p={p} />)}
</div>
)}
</main>
);
}
+6 -1
View File
@@ -4,7 +4,8 @@ import Link from 'next/link';
import {
ArrowLeft, Sparkles, Wand2, Copy, Check, Loader2, Settings,
Image as ImageIcon, RefreshCw, Scissors, Maximize2, Zap, Heart,
MessageSquare, Pencil, X, Send, Clock, Search, Camera, ExternalLink, Link2
MessageSquare, Pencil, X, ChevronDown, Send, Clock, Trash2, History,
Search, Camera, ExternalLink, Link2
} from 'lucide-react';
import PhotoSearchModal from './PhotoSearchModal';
import PostPreview from './PostPreview';
@@ -351,6 +352,10 @@ export default function ChannelView({ channel }) {
</div>
{channel.niche && <p className="text-sm text-gray-500 max-w-2xl">{channel.niche}</p>}
</div>
<Link href={`/channels/${channel.id}/history`} className="btn-ghost text-sm">
<History className="w-4 h-4" />
История
</Link>
<Link href={`/channels/${channel.id}/edit`} className="btn-ghost text-sm">
<Settings className="w-4 h-4" />
Настройки