new commit

This commit is contained in:
54CHA
2025-07-19 17:56:06 +03:00
commit 4153e2c00a
140 changed files with 66097 additions and 0 deletions

367
src/app/page.tsx Normal file
View File

@ -0,0 +1,367 @@
'use client';
import {
ProductDisplay,
CityPositionsTable,
PositionChart,
HistoryDisplay,
} from './components';
import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
import ClientWrapper from './ClientWrapper';
import {
ChartDataPoint,
CityPosition,
HistoryRecord,
SearchResponse,
} from '@/types';
import { FiBarChart2, FiActivity } from 'react-icons/fi';
// Data persistence removed no localStorage interaction
export default function Home() {
// Добавляем состояние для данных
const [chartData, setChartData] = useState<ChartDataPoint[]>([]);
const [cityPositions, setCityPositions] = useState<CityPosition[]>([]);
const [competitorPositions, setCompetitorPositions] = useState<CityPosition[]>([]);
const [historyData, setHistoryData] = useState<HistoryRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchPerformed, setSearchPerformed] = useState(false);
const [currentQuery, setCurrentQuery] = useState(''); // Текущий поисковый запрос
const [currentArticleId, setCurrentArticleId] = useState(''); // Текущий артикул товара
const [currentCompetitorId, setCurrentCompetitorId] = useState(''); // Текущий артикул конкурента
const [isComparisonMode, setIsComparisonMode] = useState(false); // Режим сравнения
// Helper to reset state when new search starts
const resetDataState = () => {
setChartData([]);
setCityPositions([]);
setCompetitorPositions([]);
setSearchPerformed(false);
setIsComparisonMode(false);
};
// Загрузка начальных данных истории (без localStorage)
useEffect(() => {
fetchHistory();
}, []);
// Получение данных истории
const fetchHistory = async (queryOverride?: string, articleOverride?: string) => {
try {
setIsLoading(true);
// Параметры запроса для фильтрации истории
const params = new URLSearchParams();
// Фильтруем строго по конкретному артикулу и по конкретному запросу
const articleId = articleOverride || currentArticleId;
const query = queryOverride || currentQuery;
if (articleId) {
params.append('articleId', articleId);
}
// Обязательно фильтруем по точному запросу (ключевые слова)
if (query) {
params.append('query', query);
}
const queryString = params.toString() ? `?${params.toString()}` : '';
const response = await fetch(`/api/history${queryString}`);
if (!response.ok) {
throw new Error('Ошибка при загрузке истории');
}
const data = await response.json();
setHistoryData(data);
// Если есть история, берем последнюю запись для отображения
if (data.length > 0) {
updateDisplayData(data[0]);
}
} catch (error) {
console.error('Ошибка загрузки истории:', error);
} finally {
setIsLoading(false);
}
};
// Обновление данных для отображения
const updateDisplayData = (historyItem: HistoryRecord) => {
// Обновляем графические данные
const chartDataFromHistory = historyItem.positions.map((pos) => ({
city: pos.city,
myPosition:
pos.pageRank === 1 ? pos.rank : (pos.pageRank - 1) * 100 + pos.rank,
// В истории нет данных конкурента, используем заглушку
competitorPosition:
pos.competitorRank && pos.competitorPageRank
? pos.competitorPageRank === 1
? pos.competitorRank
: (pos.competitorPageRank - 1) * 100 + pos.competitorRank
: 0,
}));
setChartData(chartDataFromHistory);
};
// Обработчик завершения поиска (обновлено под новую структуру)
const handleSearchComplete = (data: SearchResponse, searchQuery?: string) => {
// Сбрасываем предыдущие данные
resetDataState();
// Формируем позиции для таблицы - берем позиции основного товара
const myArticleId = data.myArticleId;
const myPositions = data.positions[myArticleId] || [];
setCityPositions(myPositions);
// Определяем режим сравнения и данные конкурента
const hasCompetitor = !!(data.competitorArticleId && data.positions[data.competitorArticleId]);
setIsComparisonMode(hasCompetitor);
setCurrentCompetitorId(data.competitorArticleId || '');
if (hasCompetitor) {
const competitorPositions = data.positions[data.competitorArticleId!] || [];
setCompetitorPositions(competitorPositions);
// Создаем chartData для сравнения с конкурентом
const chartDataFromResponse: ChartDataPoint[] = myPositions.map(myPos => {
const competitorPos = competitorPositions.find(cp => cp.city === myPos.city);
return {
city: myPos.city,
myPosition: myPos.position ?? 0,
competitorPosition: competitorPos?.position ?? 0,
};
});
setChartData(chartDataFromResponse);
// Save to localStorage (removed)
} else {
// Режим без конкурента - показываем только мои позиции
setCompetitorPositions([]);
const chartDataFromResponse: ChartDataPoint[] = myPositions.map(myPos => ({
city: myPos.city,
myPosition: myPos.position ?? 0,
competitorPosition: 0,
}));
setChartData(chartDataFromResponse);
// Save to localStorage (removed)
}
setSearchPerformed(true);
// Сохраняем артикул основного товара для фильтрации истории
if (data.products && data.products.length > 0) {
const mainProduct = data.products.find(p => p.article === myArticleId);
if (mainProduct) {
// Если сменился артикул, очищаем историю
if (currentArticleId !== mainProduct.article) {
setHistoryData([]);
}
setCurrentArticleId(mainProduct.article);
// Save query and article ID (removed)
if (searchQuery) setCurrentQuery(searchQuery);
}
}
// Обновляем историю после выполнения поиска, чтобы отобразить новые данные
// Устанавливаем небольшую задержку, чтобы данные успели сохраниться в БД
setTimeout(() => {
fetchHistory();
}, 300);
};
// Анимационные варианты
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 80,
},
},
};
// Заглушки для пустых состояний
const TablePlaceholder = () => (
<div className="bg-white/40 backdrop-blur-md border border-white/30 rounded-lg md:rounded-xl shadow-lg shadow-purple-900/5 p-3 md:p-4 h-full flex flex-col items-center justify-center">
<FiBarChart2 size={48} className="text-purple-400 mb-4" />
<p className="text-gray-600 text-center mb-2 font-medium">
Таблица позиций
</p>
<p className="text-gray-500 text-sm text-center">
Данные о позициях товаров по городам будут отображаться здесь
</p>
</div>
);
const ChartPlaceholder = () => (
<div className="bg-white/40 backdrop-blur-md border border-white/30 rounded-lg md:rounded-xl shadow-lg shadow-purple-900/5 p-3 md:p-4 h-full flex flex-col items-center justify-center">
<FiActivity size={48} className="text-purple-400 mb-4" />
<p className="text-gray-600 text-center mb-2 font-medium">
График позиций
</p>
<p className="text-gray-500 text-sm text-center">
Визуализация позиций товаров будет отображаться здесь
</p>
</div>
);
return (
<ClientWrapper>
<motion.main
initial="hidden"
animate="visible"
variants={containerVariants}
className="h-screen overflow-hidden p-1 md:p-2"
>
<div className="max-w-full mx-auto h-full">
{/* Data Management Controls removed (no persistence) */}
{/* Mobile Layout */}
<div className="flex flex-col gap-3 pb-6 lg:hidden overflow-hidden">
{/* Mobile: Search section */}
<motion.div
className="min-h-[180px] max-h-[250px] flex-shrink-0"
variants={itemVariants}
>
<ProductDisplay
products={[]}
onSearchComplete={handleSearchComplete}
/>
</motion.div>
{/* Mobile: Table section */}
<motion.div
className="min-h-[200px] max-h-[300px] flex-shrink-0"
variants={itemVariants}
>
{searchPerformed ? (
<CityPositionsTable
positions={cityPositions}
competitorPositions={competitorPositions}
isComparisonMode={isComparisonMode}
myArticleId={currentArticleId}
competitorArticleId={currentCompetitorId}
/>
) : (
<TablePlaceholder />
)}
</motion.div>
{/* Mobile: Chart section */}
<motion.div
className="min-h-[250px] max-h-[350px] flex-shrink-0"
variants={itemVariants}
>
{searchPerformed ? (
<PositionChart
data={chartData}
myArticleId={currentArticleId}
competitorArticleId={currentCompetitorId}
/>
) : (
<ChartPlaceholder />
)}
</motion.div>
{/* Mobile: History section */}
<motion.div
variants={itemVariants}
className="min-h-[150px] max-h-[250px] flex-shrink-0 mb-6"
>
<HistoryDisplay
history={historyData}
isLoading={isLoading}
searchPerformed={searchPerformed}
currentQuery={currentQuery}
/>
</motion.div>
</div>
{/* Desktop Layout */}
<div className="hidden lg:grid lg:grid-rows-[500px_1fr] lg:gap-1 h-full overflow-hidden">
{/* Desktop: Top section with three columns */}
<div className="grid lg:grid-cols-12 gap-2 h-full overflow-hidden">
{/* Товары и поиск - 3/12 */}
<motion.div
className="lg:col-span-3 h-full"
variants={itemVariants}
>
<ProductDisplay
products={[]}
onSearchComplete={handleSearchComplete}
/>
</motion.div>
{/* Таблица - 3/12 */}
<motion.div
className="lg:col-span-3 h-full"
variants={itemVariants}
>
{searchPerformed ? (
<CityPositionsTable
positions={cityPositions}
competitorPositions={competitorPositions}
isComparisonMode={isComparisonMode}
myArticleId={currentArticleId}
competitorArticleId={currentCompetitorId}
/>
) : (
<TablePlaceholder />
)}
</motion.div>
{/* График - 6/12 */}
<motion.div
className="lg:col-span-6 h-full"
variants={itemVariants}
>
{searchPerformed ? (
<PositionChart
data={chartData}
myArticleId={currentArticleId}
competitorArticleId={currentCompetitorId}
/>
) : (
<ChartPlaceholder />
)}
</motion.div>
</div>
{/* Desktop: Bottom section - History */}
<motion.div
variants={itemVariants}
className="h-full overflow-hidden"
>
<HistoryDisplay
history={historyData}
isLoading={isLoading}
searchPerformed={searchPerformed}
currentQuery={currentQuery}
/>
</motion.div>
</div>
</div>
</motion.main>
</ClientWrapper>
);
}