update
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useState, FormEvent, useEffect, useRef } from 'react';
|
||||
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
|
||||
import { useTicketCategories } from '@/lib/hooks/useSupport';
|
||||
|
||||
@@ -26,6 +26,49 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [ticketNumber, setTicketNumber] = useState<string>('');
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Refs for scrolling to messages
|
||||
const errorRef = useRef<HTMLDivElement>(null);
|
||||
const successRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
// Scroll to error/success messages when they appear
|
||||
useEffect(() => {
|
||||
if (submitError && errorRef.current) {
|
||||
setTimeout(() => {
|
||||
errorRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [submitError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (submitSuccess && successRef.current) {
|
||||
setTimeout(() => {
|
||||
successRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [submitSuccess]);
|
||||
|
||||
// Scroll to first validation error
|
||||
useEffect(() => {
|
||||
if (Object.keys(fieldErrors).length > 0 && formRef.current) {
|
||||
const firstErrorField = formRef.current.querySelector('.field-error');
|
||||
if (firstErrorField) {
|
||||
setTimeout(() => {
|
||||
firstErrorField.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [fieldErrors]);
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
@@ -72,6 +115,16 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
// Scroll to first error after validation
|
||||
setTimeout(() => {
|
||||
const firstErrorField = formRef.current?.querySelector('.field-error');
|
||||
if (firstErrorField) {
|
||||
firstErrorField.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,7 +178,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
|
||||
if (submitSuccess) {
|
||||
return (
|
||||
<div className="ticket-success">
|
||||
<div className="ticket-success" ref={successRef}>
|
||||
<div className="success-icon">
|
||||
<i className="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
@@ -204,7 +257,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<div className="alert-enterprise alert-error" ref={errorRef}>
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<div>
|
||||
<strong>Submission Error</strong>
|
||||
@@ -213,7 +266,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="support-form-enterprise">
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="support-form-enterprise">
|
||||
{/* Personal Information */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useState, FormEvent, useEffect, useRef } from 'react';
|
||||
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
|
||||
|
||||
const TicketStatusCheck = () => {
|
||||
@@ -7,6 +7,34 @@ const TicketStatusCheck = () => {
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const [ticket, setTicket] = useState<SupportTicket | null>(null);
|
||||
|
||||
// Refs for scrolling to results
|
||||
const errorRef = useRef<HTMLDivElement>(null);
|
||||
const ticketRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll to error when it appears
|
||||
useEffect(() => {
|
||||
if (searchError && errorRef.current) {
|
||||
setTimeout(() => {
|
||||
errorRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [searchError]);
|
||||
|
||||
// Scroll to ticket results when they appear
|
||||
useEffect(() => {
|
||||
if (ticket && ticketRef.current) {
|
||||
setTimeout(() => {
|
||||
ticketRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [ticket]);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -136,7 +164,7 @@ const TicketStatusCheck = () => {
|
||||
</form>
|
||||
|
||||
{searchError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<div ref={errorRef} className="alert-enterprise alert-error">
|
||||
<i className="fa-solid fa-exclamation-circle"></i>
|
||||
<div>
|
||||
<strong>Ticket Not Found</strong>
|
||||
@@ -146,7 +174,7 @@ const TicketStatusCheck = () => {
|
||||
)}
|
||||
|
||||
{ticket && (
|
||||
<div className="ticket-details-enterprise">
|
||||
<div ref={ticketRef} className="ticket-details-enterprise">
|
||||
{/* Header Section */}
|
||||
<div className="ticket-header-enterprise">
|
||||
<div className="ticket-number-section">
|
||||
|
||||
Reference in New Issue
Block a user