forked from admin/zeropost-tool
feat: channel history page — published posts with search
This commit is contained in:
@@ -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 || []} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import {
|
import {
|
||||||
ArrowLeft, Sparkles, Wand2, Copy, Check, Loader2, Settings,
|
ArrowLeft, Sparkles, Wand2, Copy, Check, Loader2, Settings,
|
||||||
Image as ImageIcon, RefreshCw, Scissors, Maximize2, Zap, Heart,
|
Image as ImageIcon, RefreshCw, Scissors, Maximize2, Zap, Heart,
|
||||||
MessageSquare, Pencil, X, ChevronDown, Send, Clock, Trash2
|
MessageSquare, Pencil, X, ChevronDown, Send, Clock, Trash2, History
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const GOAL_LABELS = {
|
const GOAL_LABELS = {
|
||||||
@@ -276,6 +276,10 @@ export default function ChannelView({ channel }) {
|
|||||||
</div>
|
</div>
|
||||||
{channel.niche && <p className="text-sm text-gray-500 max-w-2xl">{channel.niche}</p>}
|
{channel.niche && <p className="text-sm text-gray-500 max-w-2xl">{channel.niche}</p>}
|
||||||
</div>
|
</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">
|
<Link href={`/channels/${channel.id}/edit`} className="btn-ghost text-sm">
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
Настройки
|
Настройки
|
||||||
|
|||||||
Reference in New Issue
Block a user