update
This commit is contained in:
300
gnx-react/components/pages/support/CreateTicketForm.tsx
Normal file
300
gnx-react/components/pages/support/CreateTicketForm.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
|
||||
import { useTicketCategories } from '@/lib/hooks/useSupport';
|
||||
|
||||
const CreateTicketForm = () => {
|
||||
const { categories, loading: categoriesLoading } = useTicketCategories();
|
||||
|
||||
const [formData, setFormData] = useState<CreateTicketData>({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [ticketNumber, setTicketNumber] = useState<string>('');
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: name === 'category' ? (value ? parseInt(value) : undefined) : value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setSubmitError(null);
|
||||
setSubmitSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await createTicket(formData);
|
||||
setTicketNumber(response.ticket_number);
|
||||
setSubmitSuccess(true);
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
} catch (error: any) {
|
||||
setSubmitError(error.message || 'Failed to submit ticket. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (submitSuccess) {
|
||||
return (
|
||||
<div className="ticket-success">
|
||||
<div className="success-icon">
|
||||
<i className="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
<h3>Ticket Created Successfully!</h3>
|
||||
<p className="ticket-number">Your ticket number: <strong>{ticketNumber}</strong></p>
|
||||
<p className="ticket-info">
|
||||
We've received your support request and will respond as soon as possible.
|
||||
Please save your ticket number for future reference.
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setSubmitSuccess(false)}
|
||||
>
|
||||
Submit Another Ticket
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="create-ticket-form">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="form-header">
|
||||
<h2>Submit a Support Ticket</h2>
|
||||
<p>Fill out the form below and our team will get back to you shortly.</p>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
marginTop: '1rem',
|
||||
fontSize: '0.95rem',
|
||||
color: '#1e293b'
|
||||
}}>
|
||||
<strong>ℹ️ Note:</strong> Only registered email addresses can submit tickets.
|
||||
If your email is not registered, please contact support@gnxsoft.com
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<i className="fa-solid fa-triangle-exclamation me-2"></i>
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="support-form">
|
||||
<div className="row g-4">
|
||||
{/* Personal Information */}
|
||||
<div className="col-12">
|
||||
<h4 className="form-section-title">Personal Information</h4>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="user_name">
|
||||
Full Name <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="user_name"
|
||||
name="user_name"
|
||||
value={formData.user_name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="user_email">
|
||||
Email Address <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="user_email"
|
||||
name="user_email"
|
||||
value={formData.user_email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
placeholder="john@company.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="user_phone">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="user_phone"
|
||||
name="user_phone"
|
||||
value={formData.user_phone}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="company">Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="Your Company Inc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ticket Details */}
|
||||
<div className="col-12">
|
||||
<h4 className="form-section-title">Ticket Details</h4>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="ticket_type">
|
||||
Issue Type <span className="required">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="ticket_type"
|
||||
name="ticket_type"
|
||||
value={formData.ticket_type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
>
|
||||
<option value="general">General Inquiry</option>
|
||||
<option value="technical">Technical Issue</option>
|
||||
<option value="billing">Billing Question</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
<option value="bug_report">Bug Report</option>
|
||||
<option value="account">Account Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="category">Category</label>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
value={formData.category || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
disabled={categoriesLoading}
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
{Array.isArray(categories) && categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group">
|
||||
<label htmlFor="title">
|
||||
Subject <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
placeholder="Brief description of your issue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">
|
||||
Description <span className="required">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
rows={6}
|
||||
placeholder="Please provide detailed information about your issue..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-paper-plane me-2"></i>
|
||||
Submit Ticket
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTicketForm;
|
||||
|
||||
217
gnx-react/components/pages/support/KnowledgeBase.tsx
Normal file
217
gnx-react/components/pages/support/KnowledgeBase.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
|
||||
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
|
||||
|
||||
const KnowledgeBase = () => {
|
||||
const { categories, loading: categoriesLoading } = useKnowledgeBaseCategories();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(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`;
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// The search is already being performed by the hook
|
||||
};
|
||||
|
||||
const filteredCategories = selectedCategory
|
||||
? categories.filter(cat => cat.slug === selectedCategory)
|
||||
: categories;
|
||||
|
||||
return (
|
||||
<div className="knowledge-base">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10">
|
||||
<div className="form-header text-center">
|
||||
<h2>Knowledge Base</h2>
|
||||
<p>Find answers to frequently asked questions and explore our documentation.</p>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<form onSubmit={handleSearch} className="kb-search-form">
|
||||
<div className="search-input-group">
|
||||
<i className="fa-solid fa-search search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search articles, topics, or keywords..."
|
||||
className="form-control"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
className="clear-search"
|
||||
onClick={() => setSearchTerm('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Categories */}
|
||||
{!searchTerm && (
|
||||
<div className="kb-categories">
|
||||
<h3>Browse by Category</h3>
|
||||
{categoriesLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-4">
|
||||
{Array.isArray(categories) && categories.map(category => (
|
||||
<div key={category.id} className="col-md-6 col-lg-4">
|
||||
<div
|
||||
className="category-card"
|
||||
onClick={() => setSelectedCategory(category.slug)}
|
||||
style={{ borderLeftColor: category.color }}
|
||||
>
|
||||
<div
|
||||
className="category-icon"
|
||||
style={{ color: category.color }}
|
||||
>
|
||||
<i className={`fa-solid ${category.icon}`}></i>
|
||||
</div>
|
||||
<div className="category-content">
|
||||
<h4>{category.name}</h4>
|
||||
<p>{category.description}</p>
|
||||
<div className="category-meta">
|
||||
<span className="article-count">
|
||||
{category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Featured/Search Results Articles */}
|
||||
<div className="kb-articles">
|
||||
<div className="articles-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h3>{headerText}</h3>
|
||||
{selectedCategory && !searchTerm && (
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to All Articles
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<p className="search-info">
|
||||
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : displayArticles.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<i className="fa-solid fa-search empty-icon"></i>
|
||||
<h4>No articles found</h4>
|
||||
<p>
|
||||
{searchTerm
|
||||
? `We couldn't find any articles matching "${searchTerm}". Try different keywords.`
|
||||
: 'No articles available at the moment.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="articles-list">
|
||||
{Array.isArray(displayArticles) && displayArticles.map(article => (
|
||||
<div
|
||||
key={article.id}
|
||||
className="article-item"
|
||||
onClick={() => setSelectedArticleSlug(article.slug)}
|
||||
>
|
||||
<div className="article-header">
|
||||
<h4>{article.title}</h4>
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="article-summary">{article.summary}</p>
|
||||
<div className="article-meta">
|
||||
<span className="article-category">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-thumbs-up me-1"></i>
|
||||
{article.helpful_count} helpful
|
||||
</span>
|
||||
</div>
|
||||
<button className="article-read-more">
|
||||
Read More <i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Modal */}
|
||||
{selectedArticleSlug && (
|
||||
<KnowledgeBaseArticleModal
|
||||
slug={selectedArticleSlug}
|
||||
onClose={() => setSelectedArticleSlug(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBase;
|
||||
|
||||
139
gnx-react/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
139
gnx-react/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useKnowledgeBaseArticle } from '@/lib/hooks/useSupport';
|
||||
import { markArticleHelpful } from '@/lib/api/supportService';
|
||||
|
||||
interface KnowledgeBaseArticleModalProps {
|
||||
slug: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalProps) => {
|
||||
const { article, loading, error } = useKnowledgeBaseArticle(slug);
|
||||
const [feedbackGiven, setFeedbackGiven] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent body scroll when modal is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFeedback = async (helpful: boolean) => {
|
||||
if (!article || feedbackGiven) return;
|
||||
|
||||
try {
|
||||
await markArticleHelpful(slug, helpful);
|
||||
setFeedbackGiven(true);
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="kb-modal-overlay" onClick={onClose}>
|
||||
<div className="kb-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close modal">
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
|
||||
<div className="modal-content">
|
||||
{loading && (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p>Loading article...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-state">
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<h3>Error Loading Article</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{article && (
|
||||
<>
|
||||
<div className="article-header">
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
<h2>{article.title}</h2>
|
||||
<div className="article-meta">
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-calendar me-1"></i>
|
||||
{formatDate(article.published_at || article.created_at)}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="article-body">
|
||||
<div
|
||||
className="article-content"
|
||||
dangerouslySetInnerHTML={{ __html: article.content || article.summary }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="article-footer">
|
||||
<div className="article-feedback">
|
||||
<h4>Was this article helpful?</h4>
|
||||
{feedbackGiven ? (
|
||||
<p className="feedback-thanks">
|
||||
<i className="fa-solid fa-check-circle me-2"></i>
|
||||
Thank you for your feedback!
|
||||
</p>
|
||||
) : (
|
||||
<div className="feedback-buttons">
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={() => handleFeedback(true)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-up me-2"></i>
|
||||
Yes ({article.helpful_count})
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
onClick={() => handleFeedback(false)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-down me-2"></i>
|
||||
No ({article.not_helpful_count})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseArticleModal;
|
||||
|
||||
55
gnx-react/components/pages/support/SupportCenterContent.tsx
Normal file
55
gnx-react/components/pages/support/SupportCenterContent.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import CreateTicketForm from './CreateTicketForm';
|
||||
import KnowledgeBase from './KnowledgeBase';
|
||||
import TicketStatusCheck from './TicketStatusCheck';
|
||||
|
||||
type TabType = 'create' | 'knowledge' | 'status';
|
||||
|
||||
const SupportCenterContent = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('create');
|
||||
|
||||
return (
|
||||
<section className="support-center-content section-padding">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
{/* Tab Navigation */}
|
||||
<div className="support-tabs">
|
||||
<ul className="support-tabs__nav">
|
||||
<li className={activeTab === 'create' ? 'active' : ''}>
|
||||
<button onClick={() => setActiveTab('create')}>
|
||||
<i className="fa-solid fa-ticket me-2"></i>
|
||||
Submit a Ticket
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeTab === 'knowledge' ? 'active' : ''}>
|
||||
<button onClick={() => setActiveTab('knowledge')}>
|
||||
<i className="fa-solid fa-book me-2"></i>
|
||||
Knowledge Base
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeTab === 'status' ? 'active' : ''}>
|
||||
<button onClick={() => setActiveTab('status')}>
|
||||
<i className="fa-solid fa-search me-2"></i>
|
||||
Check Ticket Status
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="support-tabs__content">
|
||||
{activeTab === 'create' && <CreateTicketForm />}
|
||||
{activeTab === 'knowledge' && <KnowledgeBase />}
|
||||
{activeTab === 'status' && <TicketStatusCheck />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterContent;
|
||||
|
||||
91
gnx-react/components/pages/support/SupportCenterHero.tsx
Normal file
91
gnx-react/components/pages/support/SupportCenterHero.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
const SupportCenterHero = () => {
|
||||
return (
|
||||
<section className="support-hero">
|
||||
{/* Animated Background */}
|
||||
<div className="hero-background">
|
||||
{/* Floating Support Icons */}
|
||||
<div className="floating-tech tech-1">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-2">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-3">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-4">
|
||||
<i className="fa-solid fa-comments"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-5">
|
||||
<i className="fa-solid fa-life-ring"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-6">
|
||||
<i className="fa-solid fa-user-shield"></i>
|
||||
</div>
|
||||
|
||||
{/* Grid Pattern */}
|
||||
<div className="grid-overlay"></div>
|
||||
|
||||
{/* Animated Gradient Orbs */}
|
||||
<div className="gradient-orb orb-1"></div>
|
||||
<div className="gradient-orb orb-2"></div>
|
||||
<div className="gradient-orb orb-3"></div>
|
||||
|
||||
{/* Video Overlay */}
|
||||
<div className="video-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="support-hero__content text-center">
|
||||
<h1 className="support-hero__title">
|
||||
Support Center
|
||||
</h1>
|
||||
<p className="support-hero__subtitle">
|
||||
Get expert assistance whenever you need it. Our dedicated support team is here to help you succeed.
|
||||
</p>
|
||||
|
||||
<div className="support-hero__features">
|
||||
<div className="row g-4 justify-content-center">
|
||||
<div className="col-md-4">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<h3>Submit Tickets</h3>
|
||||
<p>Create and track support requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<h3>Knowledge Base</h3>
|
||||
<p>Find answers to common questions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-search"></i>
|
||||
</div>
|
||||
<h3>Track Status</h3>
|
||||
<p>Monitor your ticket progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterHero;
|
||||
|
||||
197
gnx-react/components/pages/support/TicketStatusCheck.tsx
Normal file
197
gnx-react/components/pages/support/TicketStatusCheck.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
|
||||
|
||||
const TicketStatusCheck = () => {
|
||||
const [ticketNumber, setTicketNumber] = useState('');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const [ticket, setTicket] = useState<SupportTicket | null>(null);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSearching(true);
|
||||
setSearchError(null);
|
||||
setTicket(null);
|
||||
|
||||
try {
|
||||
const response = await checkTicketStatus(ticketNumber);
|
||||
setTicket(response);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Ticket not found') {
|
||||
setSearchError('Ticket not found. Please check your ticket number and try again.');
|
||||
} else {
|
||||
setSearchError('An error occurred while searching. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ticket-status-check">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-8">
|
||||
<div className="form-header text-center">
|
||||
<h2>Check Ticket Status</h2>
|
||||
<p>Enter your ticket number to view the current status and details of your support request.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="status-search-form">
|
||||
<div className="search-input-group">
|
||||
<input
|
||||
type="text"
|
||||
value={ticketNumber}
|
||||
onChange={(e) => setTicketNumber(e.target.value)}
|
||||
placeholder="Enter your ticket number (e.g., TKT-20231015-ABCDE)"
|
||||
required
|
||||
className="form-control"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={isSearching}
|
||||
>
|
||||
{isSearching ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Searching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-search me-2"></i>
|
||||
Check Status
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{searchError && (
|
||||
<div className="alert alert-danger mt-4" role="alert">
|
||||
<i className="fa-solid fa-triangle-exclamation me-2"></i>
|
||||
{searchError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ticket && (
|
||||
<div className="ticket-details">
|
||||
<div className="ticket-header">
|
||||
<div className="ticket-number">
|
||||
<i className="fa-solid fa-ticket me-2"></i>
|
||||
{ticket.ticket_number}
|
||||
</div>
|
||||
<div
|
||||
className="ticket-status-badge"
|
||||
style={{ backgroundColor: ticket.status_color }}
|
||||
>
|
||||
{ticket.status_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ticket-info">
|
||||
<h3>{ticket.title}</h3>
|
||||
<div className="ticket-meta">
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-calendar me-2"></i>
|
||||
<strong>Created:</strong> {formatDate(ticket.created_at)}
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-clock me-2"></i>
|
||||
<strong>Last Updated:</strong> {formatDate(ticket.updated_at)}
|
||||
</div>
|
||||
{ticket.priority_name && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-flag me-2"></i>
|
||||
<strong>Priority:</strong>
|
||||
<span
|
||||
className="priority-badge ms-2"
|
||||
style={{ backgroundColor: ticket.priority_color }}
|
||||
>
|
||||
{ticket.priority_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{ticket.category_name && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-folder me-2"></i>
|
||||
<strong>Category:</strong> {ticket.category_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ticket-description">
|
||||
<h4>Description</h4>
|
||||
<p>{ticket.description}</p>
|
||||
</div>
|
||||
|
||||
{ticket.messages && ticket.messages.length > 0 && (
|
||||
<div className="ticket-messages">
|
||||
<h4>Messages ({ticket.messages.length})</h4>
|
||||
<div className="messages-list">
|
||||
{ticket.messages
|
||||
.filter(msg => !msg.is_internal)
|
||||
.map((message, index) => (
|
||||
<div key={message.id} className="message-item">
|
||||
<div className="message-header">
|
||||
<div className="message-author">
|
||||
<i className="fa-solid fa-user-circle me-2"></i>
|
||||
{message.author_name || message.author_email}
|
||||
</div>
|
||||
<div className="message-date">
|
||||
{formatDate(message.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ticket.activities && ticket.activities.length > 0 && (
|
||||
<div className="ticket-timeline">
|
||||
<h4>Activity Timeline</h4>
|
||||
<div className="timeline-list">
|
||||
{ticket.activities.slice(0, 5).map((activity, index) => (
|
||||
<div key={activity.id} className="timeline-item">
|
||||
<div className="timeline-icon">
|
||||
<i className="fa-solid fa-circle"></i>
|
||||
</div>
|
||||
<div className="timeline-content">
|
||||
<div className="timeline-description">
|
||||
{activity.description}
|
||||
</div>
|
||||
<div className="timeline-date">
|
||||
{formatDate(activity.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TicketStatusCheck;
|
||||
|
||||
Reference in New Issue
Block a user