This commit is contained in:
Iliyan Angelov
2025-11-24 03:52:08 +02:00
parent dfcaebaf8c
commit 366f28677a
18241 changed files with 865352 additions and 567 deletions

View File

@@ -0,0 +1,470 @@
"use client";
import { useState, FormEvent } from 'react';
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
import { useTicketCategories } from '@/lib/hooks/useSupport';
interface CreateTicketFormProps {
onOpenStatusCheck?: () => void;
}
const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
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 [fieldErrors, setFieldErrors] = useState<Record<string, 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
}));
// Clear field error when user starts typing
if (fieldErrors[name]) {
setFieldErrors(prev => ({ ...prev, [name]: '' }));
}
};
const validateForm = () => {
const errors: Record<string, string> = {};
if (!formData.user_name.trim()) {
errors.user_name = 'Full name is required';
}
if (!formData.user_email.trim()) {
errors.user_email = 'Email address is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.user_email)) {
errors.user_email = 'Please enter a valid email address';
}
if (!formData.title.trim()) {
errors.title = 'Subject is required';
}
if (!formData.description.trim()) {
errors.description = 'Description is required';
} else if (formData.description.trim().length < 10) {
errors.description = 'Please provide a more detailed description (minimum 10 characters)';
}
setFieldErrors(errors);
return Object.keys(errors).length === 0;
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) {
return;
}
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
});
setFieldErrors({});
} catch (error: any) {
console.error('Ticket creation error:', error);
// Provide user-friendly error messages based on error type
let errorMessage = 'Failed to submit ticket. Please try again.';
if (error.message) {
if (error.message.includes('email')) {
errorMessage = 'There was an issue with your email address. Please check and try again.';
} else if (error.message.includes('network') || error.message.includes('fetch')) {
errorMessage = 'Network error. Please check your connection and try again.';
} else if (error.message.includes('validation')) {
errorMessage = 'Please check all required fields and try again.';
} else if (error.message.includes('server') || error.message.includes('500')) {
errorMessage = 'Server error. Our team has been notified. Please try again later.';
} else {
// Use the actual error message if it's user-friendly
errorMessage = error.message;
}
}
setSubmitError(errorMessage);
} 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>
<div className="ticket-number-container">
<div className="ticket-number-label">Your Ticket Number</div>
<div className="ticket-number-value">{ticketNumber}</div>
<button
className="btn-copy-ticket"
onClick={() => {
navigator.clipboard.writeText(ticketNumber);
const btn = document.querySelector('.btn-copy-ticket');
if (btn) {
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy';
}, 2000);
}
}}
>
Copy
</button>
</div>
<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>
<div className="success-actions">
<button
className="btn btn-primary"
onClick={() => setSubmitSuccess(false)}
>
<i className="fa-solid fa-plus me-2"></i>
Submit Another Ticket
</button>
{onOpenStatusCheck && (
<button
className="btn btn-outline"
onClick={onOpenStatusCheck}
>
<i className="fa-solid fa-search me-2"></i>
Check Ticket Status
</button>
)}
</div>
</div>
);
}
const issueTypeIcons: Record<string, string> = {
general: 'fa-info-circle',
technical: 'fa-tools',
billing: 'fa-credit-card',
feature_request: 'fa-lightbulb',
bug_report: 'fa-bug',
account: 'fa-user-circle'
};
return (
<div className="create-ticket-form enterprise-form">
<div className="row justify-content-center">
<div className="col-12">
<div className="form-header-enterprise">
<div className="form-header-icon">
<i className="fa-solid fa-ticket"></i>
</div>
<h2>Submit a Support Ticket</h2>
<p>Fill out the form below and our dedicated support team will get back to you within 24 hours.</p>
<div className="info-banner">
<i className="fa-solid fa-shield-check"></i>
<div>
<strong>Secure & Confidential</strong>
<span>All tickets are encrypted and handled with enterprise-grade security standards.</span>
</div>
</div>
</div>
{submitError && (
<div className="alert-enterprise alert-error">
<i className="fa-solid fa-triangle-exclamation"></i>
<div>
<strong>Submission Error</strong>
<p>{submitError}</p>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="support-form-enterprise">
{/* Personal Information */}
<div className="form-section">
<div className="section-header">
<i className="fa-solid fa-user"></i>
<h3>Personal Information</h3>
</div>
<div className="row g-4">
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="user_name" className="form-label-enterprise">
<span>Full Name</span>
<span className="required-badge">Required</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-user"></i>
<input
type="text"
id="user_name"
name="user_name"
value={formData.user_name}
onChange={handleInputChange}
required
className={`form-control-enterprise ${fieldErrors.user_name ? 'error' : ''}`}
placeholder="Enter your full name"
/>
</div>
{fieldErrors.user_name && (
<span className="field-error">{fieldErrors.user_name}</span>
)}
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="user_email" className="form-label-enterprise">
<span>Email Address</span>
<span className="required-badge">Required</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-envelope"></i>
<input
type="email"
id="user_email"
name="user_email"
value={formData.user_email}
onChange={handleInputChange}
required
className={`form-control-enterprise ${fieldErrors.user_email ? 'error' : ''}`}
placeholder="your.email@company.com"
/>
</div>
{fieldErrors.user_email && (
<span className="field-error">{fieldErrors.user_email}</span>
)}
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="user_phone" className="form-label-enterprise">
<span>Phone Number</span>
<span className="optional-badge">Optional</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-phone"></i>
<input
type="tel"
id="user_phone"
name="user_phone"
value={formData.user_phone}
onChange={handleInputChange}
className="form-control-enterprise"
placeholder="+1 (555) 123-4567"
/>
</div>
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="company" className="form-label-enterprise">
<span>Company Name</span>
<span className="optional-badge">Optional</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-building"></i>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleInputChange}
className="form-control-enterprise"
placeholder="Your Company Inc."
/>
</div>
</div>
</div>
</div>
</div>
{/* Ticket Details */}
<div className="form-section">
<div className="section-header">
<i className="fa-solid fa-clipboard-list"></i>
<h3>Ticket Details</h3>
</div>
<div className="row g-4">
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="ticket_type" className="form-label-enterprise">
<span>Issue Type</span>
<span className="required-badge">Required</span>
</label>
<div className="select-with-icon">
<i className={`fa-solid ${issueTypeIcons[formData.ticket_type] || 'fa-tag'}`}></i>
<select
id="ticket_type"
name="ticket_type"
value={formData.ticket_type}
onChange={handleInputChange}
required
className="form-control-enterprise select-enterprise"
>
<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>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="category" className="form-label-enterprise">
<span>Category</span>
<span className="optional-badge">Optional</span>
</label>
<div className="select-with-icon">
<i className="fa-solid fa-folder"></i>
<select
id="category"
name="category"
value={formData.category || ''}
onChange={handleInputChange}
className="form-control-enterprise select-enterprise"
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>
<div className="col-12">
<div className="form-group-enterprise">
<label htmlFor="title" className="form-label-enterprise">
<span>Subject</span>
<span className="required-badge">Required</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-heading"></i>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleInputChange}
required
className={`form-control-enterprise ${fieldErrors.title ? 'error' : ''}`}
placeholder="Brief, descriptive subject line"
/>
</div>
{fieldErrors.title && (
<span className="field-error">{fieldErrors.title}</span>
)}
</div>
</div>
<div className="col-12">
<div className="form-group-enterprise">
<label htmlFor="description" className="form-label-enterprise">
<span>Description</span>
<span className="required-badge">Required</span>
<span className="char-count">{formData.description.length} characters</span>
</label>
<div className="textarea-wrapper">
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleInputChange}
required
className={`form-control-enterprise textarea-enterprise ${fieldErrors.description ? 'error' : ''}`}
rows={6}
placeholder="Please provide detailed information about your issue, including any error messages, steps to reproduce, or relevant context..."
/>
<div className="textarea-footer">
<i className="fa-solid fa-lightbulb"></i>
<span>Tip: More details help us resolve your issue faster</span>
</div>
</div>
{fieldErrors.description && (
<span className="field-error">{fieldErrors.description}</span>
)}
</div>
</div>
</div>
</div>
{/* Submit Section */}
<div className="form-submit-section">
<button
type="submit"
className="btn-submit-enterprise"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<span className="spinner-enterprise"></span>
<span>Submitting Ticket...</span>
</>
) : (
<>
<i className="fa-solid fa-paper-plane"></i>
<span>Submit Ticket</span>
<i className="fa-solid fa-arrow-right"></i>
</>
)}
</button>
<p className="submit-note">
<i className="fa-solid fa-clock"></i>
Average response time: <strong>2-4 hours</strong>
</p>
</div>
</form>
</div>
</div>
</div>
);
};
export default CreateTicketForm;

View 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;

View File

@@ -0,0 +1,138 @@
"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) {
}
};
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;

View File

@@ -0,0 +1,174 @@
"use client";
import { useEffect } from 'react';
import CreateTicketForm from './CreateTicketForm';
import KnowledgeBase from './KnowledgeBase';
import TicketStatusCheck from './TicketStatusCheck';
type ModalType = 'create' | 'knowledge' | 'status' | null;
interface SupportCenterContentProps {
activeModal: ModalType;
onClose: () => void;
onOpenModal?: (type: ModalType) => void;
}
const SupportCenterContent = ({ activeModal, onClose, onOpenModal }: SupportCenterContentProps) => {
// Close modal on escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && activeModal) {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [activeModal, onClose]);
// Prevent body scroll when modal is open
useEffect(() => {
if (activeModal) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [activeModal]);
if (!activeModal) return null;
return (
<>
{/* Modal Overlay */}
<div
className="support-modal-overlay"
onClick={onClose}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
zIndex: 9998,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
backdropFilter: 'blur(5px)',
}}
>
{/* Modal Content */}
<div
className="support-modal-content"
onClick={(e) => e.stopPropagation()}
style={{
backgroundColor: '#fff',
borderRadius: '12px',
maxWidth: '1000px',
width: '100%',
maxHeight: '90vh',
overflow: 'auto',
position: 'relative',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
animation: 'modalSlideIn 0.3s ease-out',
}}
>
{/* Close Button */}
<button
onClick={onClose}
className="support-modal-close"
aria-label="Close modal"
style={{
position: 'sticky',
top: '20px',
right: '20px',
float: 'right',
background: '#f3f4f6',
border: 'none',
borderRadius: '50%',
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontSize: '20px',
color: '#374151',
transition: 'all 0.2s',
zIndex: 10,
marginBottom: '-40px',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e5e7eb';
e.currentTarget.style.transform = 'scale(1.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f3f4f6';
e.currentTarget.style.transform = 'scale(1)';
}}
>
<i className="fa-solid fa-times"></i>
</button>
{/* Modal Body */}
<div className="support-modal-body" style={{ padding: '40px' }}>
{activeModal === 'create' && (
<CreateTicketForm onOpenStatusCheck={() => {
if (onOpenModal) {
onOpenModal('status');
}
}} />
)}
{activeModal === 'knowledge' && <KnowledgeBase />}
{activeModal === 'status' && <TicketStatusCheck />}
</div>
</div>
</div>
{/* Modal Animation Keyframes */}
<style jsx>{`
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.support-modal-content::-webkit-scrollbar {
width: 8px;
}
.support-modal-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.support-modal-content::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.support-modal-content::-webkit-scrollbar-thumb:hover {
background: #555;
}
@media (max-width: 768px) {
.support-modal-body {
padding: 20px !important;
}
}
`}</style>
</>
);
};
export default SupportCenterContent;

View File

@@ -0,0 +1,151 @@
"use client";
type ModalType = 'create' | 'knowledge' | 'status' | null;
interface SupportCenterHeroProps {
onFeatureClick: (type: ModalType) => void;
}
const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
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-3 g-md-4 g-lg-5 justify-content-center">
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<div
className="feature-item clickable"
onClick={() => onFeatureClick('create')}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('create')}
>
<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-12 col-sm-6 col-md-6 col-lg-4">
<div
className="feature-item clickable"
onClick={() => onFeatureClick('knowledge')}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('knowledge')}
>
<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-12 col-sm-6 col-md-6 col-lg-4">
<div
className="feature-item clickable"
onClick={() => onFeatureClick('status')}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('status')}
>
<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 className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
href="/policy?type=privacy"
className="feature-item clickable link-item"
>
<div className="feature-icon">
<i className="fa-solid fa-shield-halved"></i>
</div>
<h3>Privacy Policy</h3>
<p>Learn about data protection</p>
</a>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
href="/policy?type=terms"
className="feature-item clickable link-item"
>
<div className="feature-icon">
<i className="fa-solid fa-file-contract"></i>
</div>
<h3>Terms of Use</h3>
<p>Review our service terms</p>
</a>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
href="/policy?type=support"
className="feature-item clickable link-item"
>
<div className="feature-icon">
<i className="fa-solid fa-headset"></i>
</div>
<h3>Support Policy</h3>
<p>Understand our support coverage</p>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default SupportCenterHero;

View File

@@ -0,0 +1,323 @@
"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'
});
};
const getRelativeTime = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
return formatDate(dateString);
};
const getStatusIcon = (statusName: string) => {
const status = statusName.toLowerCase();
if (status.includes('open') || status.includes('new')) return 'fa-inbox';
if (status.includes('progress') || status.includes('working')) return 'fa-spinner';
if (status.includes('pending') || status.includes('waiting')) return 'fa-clock';
if (status.includes('resolved') || status.includes('closed')) return 'fa-check-circle';
return 'fa-ticket';
};
const getPriorityIcon = (priorityName: string) => {
const priority = priorityName.toLowerCase();
if (priority.includes('urgent') || priority.includes('critical')) return 'fa-exclamation-triangle';
if (priority.includes('high')) return 'fa-arrow-up';
if (priority.includes('medium')) return 'fa-minus';
if (priority.includes('low')) return 'fa-arrow-down';
return 'fa-flag';
};
return (
<div className="ticket-status-check enterprise-status-check">
<div className="row justify-content-center">
<div className="col-12">
<div className="status-header-enterprise">
<div className="status-header-icon">
<i className="fa-solid fa-magnifying-glass"></i>
</div>
<h2>Check Ticket Status</h2>
<p>Track your support request in real-time with instant status updates</p>
</div>
<form onSubmit={handleSubmit} className="status-search-form-enterprise">
<div className="search-container">
<div className="search-input-wrapper">
<i className="fa-solid fa-ticket search-icon"></i>
<input
type="text"
value={ticketNumber}
onChange={(e) => setTicketNumber(e.target.value.toUpperCase())}
placeholder="TKT-YYYYMMDD-XXXXX"
required
className="search-input-enterprise"
pattern="TKT-\d{8}-[A-Z0-9]{5}"
title="Please enter a valid ticket number (e.g., TKT-20231015-ABCDE)"
/>
{ticketNumber && (
<button
type="button"
className="clear-btn"
onClick={() => {
setTicketNumber('');
setTicket(null);
setSearchError(null);
}}
>
<i className="fa-solid fa-times"></i>
</button>
)}
</div>
<button
type="submit"
className="btn-search-enterprise"
disabled={isSearching}
>
{isSearching ? (
<>
<span className="spinner-enterprise"></span>
<span>Searching...</span>
</>
) : (
<>
<i className="fa-solid fa-search"></i>
<span>Search</span>
</>
)}
</button>
</div>
<p className="search-hint">
<i className="fa-solid fa-info-circle"></i>
Enter your ticket number exactly as it appears in your confirmation email
</p>
</form>
{searchError && (
<div className="alert-enterprise alert-error">
<i className="fa-solid fa-exclamation-circle"></i>
<div>
<strong>Ticket Not Found</strong>
<p>{searchError}</p>
</div>
</div>
)}
{ticket && (
<div className="ticket-details-enterprise">
{/* Header Section */}
<div className="ticket-header-enterprise">
<div className="ticket-number-section">
<span className="ticket-label">Ticket Number</span>
<div className="ticket-number-display">
<i className="fa-solid fa-ticket"></i>
<span>{ticket.ticket_number}</span>
<button
className="btn-copy-inline"
onClick={() => {
navigator.clipboard.writeText(ticket.ticket_number);
const btn = document.querySelector('.btn-copy-inline');
if (btn) {
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
setTimeout(() => {
btn.innerHTML = originalHTML;
}, 2000);
}
}}
>
<i className="fa-solid fa-copy"></i>
</button>
</div>
</div>
<div
className="status-badge-enterprise"
style={{
backgroundColor: ticket.status_color || '#6366f1',
boxShadow: `0 0 20px ${ticket.status_color}33`
}}
>
<i className={`fa-solid ${getStatusIcon(ticket.status_name)}`}></i>
<span>{ticket.status_name}</span>
</div>
</div>
{/* Info Cards Section */}
<div className="ticket-info-cards">
<div className="info-card">
<i className="fa-solid fa-calendar-plus"></i>
<div>
<span className="card-label">Created</span>
<span className="card-value">{getRelativeTime(ticket.created_at)}</span>
</div>
</div>
<div className="info-card">
<i className="fa-solid fa-clock"></i>
<div>
<span className="card-label">Last Updated</span>
<span className="card-value">{getRelativeTime(ticket.updated_at)}</span>
</div>
</div>
{ticket.priority_name && (
<div className="info-card">
<i className={`fa-solid ${getPriorityIcon(ticket.priority_name)}`}></i>
<div>
<span className="card-label">Priority</span>
<span
className="card-value priority-value"
style={{ color: ticket.priority_color }}
>
{ticket.priority_name}
</span>
</div>
</div>
)}
{ticket.category_name && (
<div className="info-card">
<i className="fa-solid fa-folder-open"></i>
<div>
<span className="card-label">Category</span>
<span className="card-value">{ticket.category_name}</span>
</div>
</div>
)}
</div>
{/* Main Content */}
<div className="ticket-content-section">
<div className="content-header">
<h3>{ticket.title}</h3>
<div className="content-meta">
<span><i className="fa-solid fa-user"></i> {ticket.user_name}</span>
{ticket.company && (
<span><i className="fa-solid fa-building"></i> {ticket.company}</span>
)}
</div>
</div>
<div className="description-section">
<h4><i className="fa-solid fa-align-left"></i> Description</h4>
<p>{ticket.description}</p>
</div>
{/* Messages Section */}
{ticket.messages && ticket.messages.filter(msg => !msg.is_internal).length > 0 && (
<div className="messages-section-enterprise">
<h4>
<i className="fa-solid fa-comments"></i>
Conversation
<span className="count-badge">{ticket.messages.filter(msg => !msg.is_internal).length}</span>
</h4>
<div className="messages-list-enterprise">
{ticket.messages
.filter(msg => !msg.is_internal)
.map((message) => (
<div key={message.id} className="message-card-enterprise">
<div className="message-avatar">
<i className="fa-solid fa-user"></i>
</div>
<div className="message-content-wrapper">
<div className="message-header-enterprise">
<span className="message-author">{message.author_name || message.author_email}</span>
<span className="message-time">{getRelativeTime(message.created_at)}</span>
</div>
<div className="message-text">
{message.content}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Activity Timeline */}
{ticket.activities && ticket.activities.length > 0 && (
<div className="timeline-section-enterprise">
<h4>
<i className="fa-solid fa-list-check"></i>
Activity Timeline
</h4>
<div className="timeline-items">
{ticket.activities.slice(0, 5).map((activity, index) => (
<div key={activity.id} className="timeline-item-enterprise">
<div className="timeline-marker">
<i className="fa-solid fa-circle"></i>
</div>
<div className="timeline-content-wrapper">
<div className="timeline-text">{activity.description}</div>
<div className="timeline-time">{getRelativeTime(activity.created_at)}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* Footer Actions */}
<div className="ticket-footer-enterprise">
<div className="footer-info">
<i className="fa-solid fa-shield-check"></i>
<span>This ticket is securely tracked and monitored by our support team</span>
</div>
<button
className="btn-refresh"
onClick={() => handleSubmit({ preventDefault: () => {} } as FormEvent<HTMLFormElement>)}
>
<i className="fa-solid fa-rotate"></i>
Refresh Status
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default TicketStatusCheck;