This commit is contained in:
Iliyan Angelov
2025-11-24 08:18:18 +02:00
parent 366f28677a
commit 136f75a859
133 changed files with 14977 additions and 3350 deletions

View File

@@ -1,5 +1,5 @@
"use client";
import { useState } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
@@ -9,39 +9,137 @@ const KnowledgeBase = () => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(null);
// Refs for scrolling to results
const articlesRef = useRef<HTMLDivElement>(null);
const emptyStateRef = useRef<HTMLDivElement>(null);
const articlesListRef = useRef<HTMLDivElement>(null);
// Fetch all articles (for browsing and category filtering)
const { articles: allArticles, loading: allArticlesLoading } = useKnowledgeBaseArticles();
// Fetch featured articles (for default view)
const { articles: featuredArticles, loading: featuredLoading } = useFeaturedArticles();
// Determine which articles to display
let displayArticles = featuredArticles;
let isLoading = featuredLoading;
let headerText = 'Featured Articles';
if (searchTerm) {
// If searching, filter all articles by search term
displayArticles = allArticles.filter(article =>
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
);
isLoading = allArticlesLoading;
headerText = 'Search Results';
} else if (selectedCategory) {
// If a category is selected, filter articles by that category
displayArticles = allArticles.filter(article => article.category_slug === selectedCategory);
isLoading = allArticlesLoading;
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
headerText = `${categoryName} Articles`;
}
// Determine which articles to display using useMemo for reactivity
const { displayArticles, isLoading, headerText } = useMemo(() => {
if (searchTerm) {
// If searching, filter all articles by search term
const filtered = allArticles.filter(article =>
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
);
return {
displayArticles: filtered,
isLoading: allArticlesLoading,
headerText: 'Search Results'
};
} else if (selectedCategory) {
// If a category is selected, filter articles by that category
const filtered = allArticles.filter(article => article.category_slug === selectedCategory);
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
return {
displayArticles: filtered,
isLoading: allArticlesLoading,
headerText: `${categoryName} Articles`
};
} else {
return {
displayArticles: featuredArticles,
isLoading: featuredLoading,
headerText: 'Featured Articles'
};
}
}, [searchTerm, selectedCategory, allArticles, featuredArticles, allArticlesLoading, featuredLoading, categories]);
// Helper function to find the scrollable parent container
const findScrollableParent = (element: HTMLElement | null): HTMLElement | null => {
if (!element) return null;
let parent = element.parentElement;
while (parent) {
const style = window.getComputedStyle(parent);
const overflowY = style.overflowY || style.overflow;
const maxHeight = style.maxHeight;
// Check if this element is scrollable (has overflow and max-height)
if ((overflowY === 'auto' || overflowY === 'scroll') && maxHeight && maxHeight !== 'none') {
return parent;
}
parent = parent.parentElement;
}
return null;
};
// Scroll to results when category is selected or search changes
useEffect(() => {
// Only scroll if we have a search term or selected category and articles are loaded
if ((searchTerm || selectedCategory) && !isLoading) {
// Wait for React to render the articles - use longer delay for category clicks
const scrollTimeout = setTimeout(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (articlesRef.current) {
// Try to find the articles list first (most specific), then empty state, then first article item, then section
const articlesList = articlesRef.current.querySelector('.articles-list');
const firstArticle = articlesRef.current.querySelector('.article-item');
const emptyState = articlesRef.current.querySelector('.empty-state');
const targetElement = articlesList || firstArticle || emptyState || articlesRef.current;
if (targetElement) {
// Find the scrollable parent container (the modal)
const scrollableContainer = findScrollableParent(targetElement as HTMLElement);
if (scrollableContainer) {
// Calculate position relative to the scrollable container
const containerRect = scrollableContainer.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();
// Calculate the scroll position within the container
const scrollTop = scrollableContainer.scrollTop;
const relativeTop = targetRect.top - containerRect.top;
const offset = 20; // Small offset from top of container
scrollableContainer.scrollTo({
top: scrollTop + relativeTop - offset,
behavior: 'smooth'
});
} else {
// Fallback to window scroll if no scrollable container found
const elementTop = targetElement.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementTop - 100;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
}
});
});
}, 600); // Longer delay to ensure React has fully rendered the articles
return () => clearTimeout(scrollTimeout);
}
}, [searchTerm, selectedCategory, isLoading, displayArticles.length]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
// The search is already being performed by the hook
};
const handleCategoryClick = (categorySlug: string) => {
setSelectedCategory(categorySlug);
// Clear search when category is selected
if (searchTerm) {
setSearchTerm('');
}
// Scroll will be handled by useEffect after articles render
};
const filteredCategories = selectedCategory
? categories.filter(cat => cat.slug === selectedCategory)
: categories;
@@ -95,7 +193,7 @@ const KnowledgeBase = () => {
<div key={category.id} className="col-md-6 col-lg-4">
<div
className="category-card"
onClick={() => setSelectedCategory(category.slug)}
onClick={() => handleCategoryClick(category.slug)}
style={{ borderLeftColor: category.color }}
>
<div
@@ -122,7 +220,7 @@ const KnowledgeBase = () => {
)}
{/* Featured/Search Results Articles */}
<div className="kb-articles">
<div ref={articlesRef} className="kb-articles">
<div className="articles-header">
<div className="d-flex align-items-center justify-content-between">
<h3>{headerText}</h3>
@@ -150,7 +248,7 @@ const KnowledgeBase = () => {
</div>
</div>
) : displayArticles.length === 0 ? (
<div className="empty-state">
<div ref={emptyStateRef} className="empty-state">
<i className="fa-solid fa-search empty-icon"></i>
<h4>No articles found</h4>
<p>
@@ -160,7 +258,7 @@ const KnowledgeBase = () => {
</p>
</div>
) : (
<div className="articles-list">
<div ref={articlesListRef} className="articles-list">
{Array.isArray(displayArticles) && displayArticles.map(article => (
<div
key={article.id}