import React, { useState, useEffect, useRef } from 'react';
import { Rss, Filter, Plus, X, Copy, Check, ArrowRight, ShieldAlert, Settings, Radio, RefreshCw, Clock, AlertTriangle, Loader2 } from 'lucide-react';
const App = () => {
const [step, setStep] = useState(1); // 1: Input, 2: Configure, 3: Result
const [url, setUrl] = useState('');
const [channelName, setChannelName] = useState('');
const [includeKeywords, setIncludeKeywords] = useState([]);
const [excludeKeywords, setExcludeKeywords] = useState([]);
const [currentInput, setCurrentInput] = useState('');
const [filterType, setFilterType] = useState('include'); // 'include' or 'exclude'
const [isCopied, setIsCopied] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// Real Data State
const [posts, setPosts] = useState([]);
const [fetchError, setFetchError] = useState(null);
const [isFetching, setIsFetching] = useState(false);
// Update Interval State
const [updateInterval, setUpdateInterval] = useState(25);
const [lastUpdated, setLastUpdated] = useState(null);
const refreshTimerRef = useRef(null);
// Helper: Extract channel name
const parseChannelName = (input) => {
const match = input.match(/(?:t\.me\/|telegram\.me\/|@)([\w\d_]+)/);
return match ? match[1] : '';
};
const handleUrlChange = (e) => {
const val = e.target.value;
setUrl(val);
setChannelName(parseChannelName(val));
};
// --- CORE LOGIC: Fetch Real Telegram Data ---
const fetchTelegramPosts = async (channel) => {
if (!channel) return;
setIsFetching(true);
setFetchError(null);
try {
// Using a CORS proxy to bypass browser restrictions for the demo
// In production backend, you would request https://t.me/s/${channel} directly
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(`https://t.me/s/${channel}`)}`;
const response = await fetch(proxyUrl);
if (!response.ok) throw new Error('Network response was not ok');
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
// Parse Telegram Web Preview DOM
const messageNodes = doc.querySelectorAll('.tgme_widget_message_wrap');
if (messageNodes.length === 0) {
// Check if it's a valid channel page but empty or private
if (html.includes('tgme_page_title')) {
setPosts([]); // Valid channel, no posts found in preview
} else {
throw new Error('Канал не найден или является приватным');
}
} else {
const parsedPosts = Array.from(messageNodes).map(node => {
const textNode = node.querySelector('.tgme_widget_message_text');
const rawText = textNode ? textNode.innerText : '[Медиа контент]'; // Handle photo-only posts
const dateNode = node.querySelector('.tgme_widget_message_date time');
const dateStr = dateNode ? dateNode.getAttribute('datetime') : new Date().toISOString();
const linkNode = node.querySelector('.tgme_widget_message_date');
const postLink = linkNode ? linkNode.getAttribute('href') : '#';
// Extract first few words for title
const title = rawText !== '[Медиа контент]'
? rawText.split(' ').slice(0, 6).join(' ') + (rawText.split(' ').length > 6 ? '...' : '')
: 'Медиа пост';
return {
id: postLink, // Use link as ID
title: title,
content: rawText,
date: new Date(dateStr).toLocaleString('ru-RU', {
day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit'
}),
rawDate: new Date(dateStr)
};
});
// Telegram shows oldest first in DOM usually, reverse to show newest first
setPosts(parsedPosts.reverse());
setLastUpdated(new Date().toLocaleTimeString('ru-RU', {hour: '2-digit', minute:'2-digit'}));
}
} catch (err) {
console.error(err);
setFetchError("Не удалось загрузить ленту. Проверьте имя канала (он должен быть публичным).");
setPosts([]);
} finally {
setIsFetching(false);
}
};
// Step Transitions
const goToConfigStep = () => {
if (channelName) {
setStep(2);
fetchTelegramPosts(channelName);
}
};
// Auto-refresh logic for the preview
useEffect(() => {
if (step === 2 && updateInterval) {
// Clear existing timer
if (refreshTimerRef.current) clearInterval(refreshTimerRef.current);
// Set new timer (converting minutes to ms)
refreshTimerRef.current = setInterval(() => {
fetchTelegramPosts(channelName);
}, updateInterval * 60 * 1000);
}
return () => clearInterval(refreshTimerRef.current);
}, [step, updateInterval, channelName]);
const handleAddKeyword = () => {
if (!currentInput.trim()) return;
if (filterType === 'include') {
if (!includeKeywords.includes(currentInput.trim())) {
setIncludeKeywords([...includeKeywords, currentInput.trim()]);
}
} else {
if (!excludeKeywords.includes(currentInput.trim())) {
setExcludeKeywords([...excludeKeywords, currentInput.trim()]);
}
}
setCurrentInput('');
};
const removeKeyword = (word, type) => {
if (type === 'include') {
setIncludeKeywords(includeKeywords.filter(k => k !== word));
} else {
setExcludeKeywords(excludeKeywords.filter(k => k !== word));
}
};
const handleGenerate = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setStep(3);
}, 1000);
};
const getFilteredPosts = () => {
if (!posts.length) return [];
return posts.filter(post => {
const text = (post.title + " " + post.content).toLowerCase();
// Logic: Must contain AT LEAST ONE included keyword (if any exist)
const hasInclude = includeKeywords.length === 0 || includeKeywords.some(k => text.includes(k.toLowerCase()));
// Logic: Must NOT contain ANY excluded keywords
const hasExclude = excludeKeywords.some(k => text.includes(k.toLowerCase()));
return hasInclude && !hasExclude;
});
};
const generatedUrl = `http://digital-liven.com/rss/${channelName || 'channel'}?include=${includeKeywords.join(',')}&exclude=${excludeKeywords.join(',')}&interval=${updateInterval}m`;
const copyToClipboard = () => {
navigator.clipboard.writeText(generatedUrl);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
return (
{/* Header */}
{/* Step 1: Input URL */}
{step === 1 && (
RSS фиды для Telegram каналов
Создавайте RSS ленты из реальных публичных Telegram каналов. Фильтруйте контент по ключевым словам и получайте обновления.
{[
{icon:
, title: "Реальные данные", desc: "Парсинг публичных каналов (t.me/s/...) в реальном времени."},
{icon:
, title: "Умные фильтры", desc: "Включайте или исключайте посты по ключевым словам."},
{icon:
, title: "Авто-обновление", desc: "Настройка интервала опроса источника."}
].map((item, idx) => (
{item.icon}
{item.title} {item.desc}
))}
)}
{/* Step 2: Configuration */}
{step === 2 && (
setStep(1)} className="text-sm text-slate-500 hover:text-blue-600 mb-4 flex items-center gap-1">← Назад к поиску
{/* Left: Filter & Interval Controls */}
{/* Keywords Section */}
Добавить ключевые слова setFilterType('include')}
className={`flex-1 py-2 text-sm font-medium rounded-lg border transition ${filterType === 'include' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-white border-slate-200 text-slate-500 hover:bg-slate-50'}`}
>
Включать
setFilterType('exclude')}
className={`flex-1 py-2 text-sm font-medium rounded-lg border transition ${filterType === 'exclude' ? 'bg-red-50 border-red-200 text-red-700' : 'bg-white border-slate-200 text-slate-500 hover:bg-slate-50'}`}
>
Исключать
{/* Chips Display */}
{includeKeywords.length > 0 && (
Обязательно содержит
{includeKeywords.map(k => (
{k}
removeKeyword(k, 'include')} className="hover:text-green-950">
))}
)}
{excludeKeywords.length > 0 && (
Исключить, если содержит
{excludeKeywords.map(k => (
{k}
removeKeyword(k, 'exclude')} className="hover:text-red-950">
))}
)}
{includeKeywords.length === 0 && excludeKeywords.length === 0 && (
Фильтры не заданы. Будут показаны все посты канала.
)}
{/* Update Interval Section */}
Частота обновления
{updateInterval} мин
setUpdateInterval(e.target.value)}
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
5 мин 30 мин 60 мин
Сервис будет открывать t.me/s/{channelName} каждые {updateInterval} мин, считывать новые посты и фильтровать их.
{isLoading ? 'Генерация...' : 'Создать RSS Ссылку'}
{/* Right: Live Preview */}
Предпросмотр (Live)
{lastUpdated && Обновлено: {lastUpdated} }
fetchTelegramPosts(channelName)}
disabled={isFetching}
className={`p-1.5 rounded-full hover:bg-slate-200 text-slate-500 transition ${isFetching ? 'animate-spin' : ''}`}
title="Обновить данные"
>
{/* Fetching State */}
{isFetching && !posts.length && (
Загрузка данных канала...
)}
{/* Error State */}
{fetchError && (
)}
{/* Empty State / No Matches */}
{!isFetching && !fetchError && getFilteredPosts().length === 0 && (
{posts.length === 0 ? "Постов не найдено" : "Нет постов, соответствующих фильтрам"}
)}
{/* Content List */}
{!isFetching && !fetchError && getFilteredPosts().length > 0 && (
{getFilteredPosts().map((post, idx) => (
))}
)}
{!isFetching && posts.length > 0 && (
Всего загружено: {posts.length} | Показано: {getFilteredPosts().length}
)}
)}
{/* Step 3: Result */}
{step === 3 && (
Ваша RSS лента готова!
Сервис настроен на мониторинг канала @{channelName} .
{generatedUrl}
{isCopied ? : }
{isCopied ? 'Скопировано' : 'Копировать'}
setStep(1)}
className="px-6 py-2 rounded-lg border border-slate-300 text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition font-medium"
>
Создать еще
setStep(2)}
className="px-6 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition font-medium shadow-md shadow-blue-200"
>
Настроить фильтры
Как это работает?
Ссылка содержит инструкции для бэкенда: периодически (раз в {updateInterval} мин) запрашивать t.me/s/{channelName}, парсить новые посты и отдавать XML только с теми, что прошли фильтры.
)}
);
};
export default App;