This commit is contained in:
Iliyan Angelov
2025-10-13 22:47:06 +03:00
parent 5ad9cbe3a6
commit dfcaebaf8c
12 changed files with 3233 additions and 373 deletions

View File

@@ -15,7 +15,11 @@ const SupportCenterPage = () => {
<Header /> <Header />
<main> <main>
<SupportCenterHero onFeatureClick={setActiveModal} /> <SupportCenterHero onFeatureClick={setActiveModal} />
<SupportCenterContent activeModal={activeModal} onClose={() => setActiveModal(null)} /> <SupportCenterContent
activeModal={activeModal}
onClose={() => setActiveModal(null)}
onOpenModal={setActiveModal}
/>
</main> </main>
<Footer /> <Footer />
</div> </div>

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -3,7 +3,11 @@ import { useState, FormEvent } from 'react';
import { createTicket, CreateTicketData } from '@/lib/api/supportService'; import { createTicket, CreateTicketData } from '@/lib/api/supportService';
import { useTicketCategories } from '@/lib/hooks/useSupport'; import { useTicketCategories } from '@/lib/hooks/useSupport';
const CreateTicketForm = () => { interface CreateTicketFormProps {
onOpenStatusCheck?: () => void;
}
const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
const { categories, loading: categoriesLoading } = useTicketCategories(); const { categories, loading: categoriesLoading } = useTicketCategories();
const [formData, setFormData] = useState<CreateTicketData>({ const [formData, setFormData] = useState<CreateTicketData>({
@@ -21,6 +25,7 @@ const CreateTicketForm = () => {
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
const [submitSuccess, setSubmitSuccess] = useState(false); const [submitSuccess, setSubmitSuccess] = useState(false);
const [ticketNumber, setTicketNumber] = useState<string>(''); const [ticketNumber, setTicketNumber] = useState<string>('');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const handleInputChange = ( const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
@@ -30,10 +35,46 @@ const CreateTicketForm = () => {
...prev, ...prev,
[name]: name === 'category' ? (value ? parseInt(value) : undefined) : value [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>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true); setIsSubmitting(true);
setSubmitError(null); setSubmitError(null);
setSubmitSuccess(false); setSubmitSuccess(false);
@@ -54,6 +95,7 @@ const CreateTicketForm = () => {
company: '', company: '',
category: undefined category: undefined
}); });
setFieldErrors({});
} catch (error: any) { } catch (error: any) {
console.error('Ticket creation error:', error); console.error('Ticket creation error:', error);
@@ -88,226 +130,334 @@ const CreateTicketForm = () => {
<i className="fa-solid fa-circle-check"></i> <i className="fa-solid fa-circle-check"></i>
</div> </div>
<h3>Ticket Created Successfully!</h3> <h3>Ticket Created Successfully!</h3>
<p className="ticket-number">Your ticket number: <strong>{ticketNumber}</strong></p> <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"> <p className="ticket-info">
We've received your support request and will respond as soon as possible. We've received your support request and will respond as soon as possible.
Please save your ticket number for future reference. Please save your ticket number for future reference.
</p> </p>
<button <div className="success-actions">
className="btn btn-primary" <button
onClick={() => setSubmitSuccess(false)} className="btn btn-primary"
> onClick={() => setSubmitSuccess(false)}
Submit Another Ticket >
</button> <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> </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 ( return (
<div className="create-ticket-form"> <div className="create-ticket-form enterprise-form">
<div className="row justify-content-center"> <div className="row justify-content-center">
<div className="col-12 col-lg-10 col-xl-8"> <div className="col-12">
<div className="form-header"> <div className="form-header-enterprise">
<div className="form-header-icon">
<i className="fa-solid fa-ticket"></i>
</div>
<h2>Submit a Support Ticket</h2> <h2>Submit a Support Ticket</h2>
<p>Fill out the form below and our team will get back to you shortly.</p> <p>Fill out the form below and our dedicated support team will get back to you within 24 hours.</p>
<div style={{ <div className="info-banner">
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%)', <i className="fa-solid fa-shield-check"></i>
border: '1px solid rgba(59, 130, 246, 0.3)', <div>
borderRadius: '8px', <strong>Secure & Confidential</strong>
padding: '12px 16px', <span>All tickets are encrypted and handled with enterprise-grade security standards.</span>
marginTop: '1rem', </div>
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>
</div> </div>
{submitError && ( {submitError && (
<div className="alert alert-danger" role="alert"> <div className="alert-enterprise alert-error">
<i className="fa-solid fa-triangle-exclamation me-2"></i> <i className="fa-solid fa-triangle-exclamation"></i>
{submitError} <div>
<strong>Submission Error</strong>
<p>{submitError}</p>
</div>
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="support-form"> <form onSubmit={handleSubmit} className="support-form-enterprise">
<div className="row g-4"> {/* Personal Information */}
{/* Personal Information */} <div className="form-section">
<div className="col-12"> <div className="section-header">
<h4 className="form-section-title">Personal Information</h4> <i className="fa-solid fa-user"></i>
<h3>Personal Information</h3>
</div> </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="col-md-6">
<div className="form-group"> <div className="form-group-enterprise">
<label htmlFor="user_name"> <label htmlFor="user_email" className="form-label-enterprise">
Full Name <span className="required">*</span> <span>Email Address</span>
</label> <span className="required-badge">Required</span>
<input </label>
type="text" <div className="input-with-icon">
id="user_name" <i className="fa-solid fa-envelope"></i>
name="user_name" <input
value={formData.user_name} type="email"
onChange={handleInputChange} id="user_email"
required name="user_email"
className="form-control" value={formData.user_email}
placeholder="John Doe" 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> </div>
</div>
<div className="col-md-6"> {/* Ticket Details */}
<div className="form-group"> <div className="form-section">
<label htmlFor="user_email"> <div className="section-header">
Email Address <span className="required">*</span> <i className="fa-solid fa-clipboard-list"></i>
</label> <h3>Ticket Details</h3>
<input </div>
type="email"
id="user_email" <div className="row g-4">
name="user_email" <div className="col-md-6">
value={formData.user_email} <div className="form-group-enterprise">
onChange={handleInputChange} <label htmlFor="ticket_type" className="form-label-enterprise">
required <span>Issue Type</span>
className="form-control" <span className="required-badge">Required</span>
placeholder="john@company.com" </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> </div>
</div>
<div className="col-md-6"> {/* Submit Section */}
<div className="form-group"> <div className="form-submit-section">
<label htmlFor="user_phone">Phone Number</label> <button
<input type="submit"
type="tel" className="btn-submit-enterprise"
id="user_phone" disabled={isSubmitting}
name="user_phone" >
value={formData.user_phone} {isSubmitting ? (
onChange={handleInputChange} <>
className="form-control" <span className="spinner-enterprise"></span>
placeholder="+1 (555) 123-4567" <span>Submitting Ticket...</span>
/> </>
</div> ) : (
</div> <>
<i className="fa-solid fa-paper-plane"></i>
<div className="col-md-6"> <span>Submit Ticket</span>
<div className="form-group"> <i className="fa-solid fa-arrow-right"></i>
<label htmlFor="company">Company Name</label> </>
<input )}
type="text" </button>
id="company" <p className="submit-note">
name="company" <i className="fa-solid fa-clock"></i>
value={formData.company} Average response time: <strong>2-4 hours</strong>
onChange={handleInputChange} </p>
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> </div>
</form> </form>
</div> </div>

View File

@@ -9,9 +9,10 @@ type ModalType = 'create' | 'knowledge' | 'status' | null;
interface SupportCenterContentProps { interface SupportCenterContentProps {
activeModal: ModalType; activeModal: ModalType;
onClose: () => void; onClose: () => void;
onOpenModal?: (type: ModalType) => void;
} }
const SupportCenterContent = ({ activeModal, onClose }: SupportCenterContentProps) => { const SupportCenterContent = ({ activeModal, onClose, onOpenModal }: SupportCenterContentProps) => {
// Close modal on escape key // Close modal on escape key
useEffect(() => { useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
@@ -115,7 +116,13 @@ const SupportCenterContent = ({ activeModal, onClose }: SupportCenterContentProp
{/* Modal Body */} {/* Modal Body */}
<div className="support-modal-body" style={{ padding: '40px' }}> <div className="support-modal-body" style={{ padding: '40px' }}>
{activeModal === 'create' && <CreateTicketForm />} {activeModal === 'create' && (
<CreateTicketForm onOpenStatusCheck={() => {
if (onOpenModal) {
onOpenModal('status');
}
}} />
)}
{activeModal === 'knowledge' && <KnowledgeBase />} {activeModal === 'knowledge' && <KnowledgeBase />}
{activeModal === 'status' && <TicketStatusCheck />} {activeModal === 'status' && <TicketStatusCheck />}
</div> </div>

View File

@@ -39,122 +39,233 @@ const TicketStatusCheck = () => {
}); });
}; };
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 ( return (
<div className="ticket-status-check"> <div className="ticket-status-check enterprise-status-check">
<div className="row justify-content-center"> <div className="row justify-content-center">
<div className="col-12 col-lg-8"> <div className="col-12">
<div className="form-header text-center"> <div className="status-header-enterprise">
<div className="status-header-icon">
<i className="fa-solid fa-magnifying-glass"></i>
</div>
<h2>Check Ticket Status</h2> <h2>Check Ticket Status</h2>
<p>Enter your ticket number to view the current status and details of your support request.</p> <p>Track your support request in real-time with instant status updates</p>
</div> </div>
<form onSubmit={handleSubmit} className="status-search-form"> <form onSubmit={handleSubmit} className="status-search-form-enterprise">
<div className="search-input-group"> <div className="search-container">
<input <div className="search-input-wrapper">
type="text" <i className="fa-solid fa-ticket search-icon"></i>
value={ticketNumber} <input
onChange={(e) => setTicketNumber(e.target.value)} type="text"
placeholder="Enter your ticket number (e.g., TKT-20231015-ABCDE)" value={ticketNumber}
required onChange={(e) => setTicketNumber(e.target.value.toUpperCase())}
className="form-control" 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 <button
type="submit" type="submit"
className="btn btn-primary" className="btn-search-enterprise"
disabled={isSearching} disabled={isSearching}
> >
{isSearching ? ( {isSearching ? (
<> <>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> <span className="spinner-enterprise"></span>
Searching... <span>Searching...</span>
</> </>
) : ( ) : (
<> <>
<i className="fa-solid fa-search me-2"></i> <i className="fa-solid fa-search"></i>
Check Status <span>Search</span>
</> </>
)} )}
</button> </button>
</div> </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> </form>
{searchError && ( {searchError && (
<div className="alert alert-danger mt-4" role="alert"> <div className="alert-enterprise alert-error">
<i className="fa-solid fa-triangle-exclamation me-2"></i> <i className="fa-solid fa-exclamation-circle"></i>
{searchError} <div>
<strong>Ticket Not Found</strong>
<p>{searchError}</p>
</div>
</div> </div>
)} )}
{ticket && ( {ticket && (
<div className="ticket-details"> <div className="ticket-details-enterprise">
<div className="ticket-header"> {/* Header Section */}
<div className="ticket-number"> <div className="ticket-header-enterprise">
<i className="fa-solid fa-ticket me-2"></i> <div className="ticket-number-section">
{ticket.ticket_number} <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>
<div <div
className="ticket-status-badge" className="status-badge-enterprise"
style={{ backgroundColor: ticket.status_color }} style={{
backgroundColor: ticket.status_color || '#6366f1',
boxShadow: `0 0 20px ${ticket.status_color}33`
}}
> >
{ticket.status_name} <i className={`fa-solid ${getStatusIcon(ticket.status_name)}`}></i>
<span>{ticket.status_name}</span>
</div> </div>
</div> </div>
<div className="ticket-info"> {/* Info Cards Section */}
<h3>{ticket.title}</h3> <div className="ticket-info-cards">
<div className="ticket-meta"> <div className="info-card">
<div className="meta-item"> <i className="fa-solid fa-calendar-plus"></i>
<i className="fa-solid fa-calendar me-2"></i> <div>
<strong>Created:</strong> {formatDate(ticket.created_at)} <span className="card-label">Created</span>
<span className="card-value">{getRelativeTime(ticket.created_at)}</span>
</div> </div>
<div className="meta-item"> </div>
<i className="fa-solid fa-clock me-2"></i> <div className="info-card">
<strong>Last Updated:</strong> {formatDate(ticket.updated_at)} <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>
<div className="meta-item"> {ticket.priority_name && (
<i className="fa-solid fa-flag me-2"></i> <div className="info-card">
<strong>Priority:</strong> <i className={`fa-solid ${getPriorityIcon(ticket.priority_name)}`}></i>
<div>
<span className="card-label">Priority</span>
<span <span
className="priority-badge ms-2" className="card-value priority-value"
style={{ backgroundColor: ticket.priority_color }} style={{ color: ticket.priority_color }}
> >
{ticket.priority_name} {ticket.priority_name}
</span> </span>
</div> </div>
)} </div>
{ticket.category_name && ( )}
<div className="meta-item"> {ticket.category_name && (
<i className="fa-solid fa-folder me-2"></i> <div className="info-card">
<strong>Category:</strong> {ticket.category_name} <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>
</div> )}
</div>
<div className="ticket-description"> {/* Main Content */}
<h4>Description</h4> <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> <p>{ticket.description}</p>
</div> </div>
{ticket.messages && ticket.messages.length > 0 && ( {/* Messages Section */}
<div className="ticket-messages"> {ticket.messages && ticket.messages.filter(msg => !msg.is_internal).length > 0 && (
<h4>Messages ({ticket.messages.length})</h4> <div className="messages-section-enterprise">
<div className="messages-list"> <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 {ticket.messages
.filter(msg => !msg.is_internal) .filter(msg => !msg.is_internal)
.map((message, index) => ( .map((message) => (
<div key={message.id} className="message-item"> <div key={message.id} className="message-card-enterprise">
<div className="message-header"> <div className="message-avatar">
<div className="message-author"> <i className="fa-solid fa-user"></i>
<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>
<div className="message-content"> <div className="message-content-wrapper">
{message.content} <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>
))} ))}
@@ -162,22 +273,22 @@ const TicketStatusCheck = () => {
</div> </div>
)} )}
{/* Activity Timeline */}
{ticket.activities && ticket.activities.length > 0 && ( {ticket.activities && ticket.activities.length > 0 && (
<div className="ticket-timeline"> <div className="timeline-section-enterprise">
<h4>Activity Timeline</h4> <h4>
<div className="timeline-list"> <i className="fa-solid fa-list-check"></i>
Activity Timeline
</h4>
<div className="timeline-items">
{ticket.activities.slice(0, 5).map((activity, index) => ( {ticket.activities.slice(0, 5).map((activity, index) => (
<div key={activity.id} className="timeline-item"> <div key={activity.id} className="timeline-item-enterprise">
<div className="timeline-icon"> <div className="timeline-marker">
<i className="fa-solid fa-circle"></i> <i className="fa-solid fa-circle"></i>
</div> </div>
<div className="timeline-content"> <div className="timeline-content-wrapper">
<div className="timeline-description"> <div className="timeline-text">{activity.description}</div>
{activity.description} <div className="timeline-time">{getRelativeTime(activity.created_at)}</div>
</div>
<div className="timeline-date">
{formatDate(activity.created_at)}
</div>
</div> </div>
</div> </div>
))} ))}
@@ -185,6 +296,21 @@ const TicketStatusCheck = () => {
</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>

View File

@@ -60,8 +60,8 @@ const Footer = () => {
<Image <Image
src={logoSrc} src={logoSrc}
alt="Logo" alt="Logo"
width={160} width={120}
height={120} height={90}
style={{ style={{
width: 'auto', width: 'auto',
height: 'auto' height: 'auto'

View File

@@ -800,23 +800,30 @@
.cta-primary { .cta-primary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
background: linear-gradient(135deg, #1e40af, #0ea5e9); background: linear-gradient(135deg, #1a365d, #2563eb);
color: white; color: #ffffff !important;
padding: 14px 28px; padding: 16px 32px;
border-radius: 8px; border-radius: 6px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 6px 24px rgba(30, 64, 175, 0.3); box-shadow: 0 4px 14px rgba(26, 54, 93, 0.25);
font-size: 15px; font-size: 15px;
letter-spacing: 0.3px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
span, i {
color: #ffffff !important;
}
&:hover { &:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.4); box-shadow: 0 6px 20px rgba(26, 54, 93, 0.35);
background: linear-gradient(135deg, #1e40af, #3b82f6);
} }
&:active { &:active {
@@ -824,7 +831,7 @@
} }
i { i {
font-size: 15px; font-size: 14px;
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
@@ -836,25 +843,31 @@
.cta-secondary { .cta-secondary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
background: transparent; background: rgba(255, 255, 255, 0.05);
color: white; color: #ffffff !important;
padding: 14px 28px; padding: 16px 32px;
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 8px; border-radius: 6px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
transition: all 0.3s ease; transition: all 0.3s ease;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
font-size: 15px; font-size: 15px;
letter-spacing: 0.3px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
span, i {
color: #ffffff !important;
}
&:hover { &:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.4); border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 14px rgba(255, 255, 255, 0.1);
} }
&:active { &:active {
@@ -862,7 +875,7 @@
} }
i { i {
font-size: 15px; font-size: 14px;
} }
} }
} }

View File

@@ -54,7 +54,7 @@
img { img {
filter: brightness(0) invert(1); // Make logo white for dark footer filter: brightness(0) invert(1); // Make logo white for dark footer
transition: all 0.3s ease; transition: all 0.3s ease;
max-height: 120px; max-height: 90px;
width: auto; width: auto;
} }
@@ -479,7 +479,7 @@
} }
.footer-logo img { .footer-logo img {
max-height: 100px; max-height: 75px;
} }
.security-badges-left, .security-badges-left,
@@ -511,7 +511,7 @@
} }
.footer-logo img { .footer-logo img {
max-height: 80px; max-height: 60px;
} }
.security-badges-left, .security-badges-left,
@@ -540,7 +540,7 @@
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.footer-logo-section { .footer-logo-section {
.footer-logo img { .footer-logo img {
max-height: 60px; max-height: 45px;
} }
.security-badges-left, .security-badges-left,

View File

@@ -71,13 +71,16 @@
.navbar__dropdown-label { .navbar__dropdown-label {
position: relative; position: relative;
padding-right: 25px; padding-right: 25px;
padding: 10px 25px 10px 15px;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&::after { &::after {
content: "\f107"; content: "\f107";
font-family: "Font Awesome 6 Free"; font-family: "Font Awesome 6 Free";
font-weight: 900; font-weight: 900;
position: absolute; position: absolute;
right: 0; right: 8px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
font-size: 12px; font-size: 12px;
@@ -87,6 +90,17 @@
&:hover { &:hover {
color: var(--enterprise-gold); color: var(--enterprise-gold);
background: rgba(255, 255, 255, 0.05);
&::after {
color: var(--enterprise-gold);
transform: translateY(-50%) rotate(180deg);
}
}
&.navbar__item-active {
color: var(--enterprise-gold);
background: rgba(212, 175, 55, 0.1);
&::after { &::after {
color: var(--enterprise-gold); color: var(--enterprise-gold);
@@ -101,71 +115,103 @@
} }
} }
&:hover .navbar__dropdown-label::after { &:hover .navbar__dropdown-label {
transform: translateY(-50%) rotate(180deg); color: var(--enterprise-gold);
background: rgba(255, 255, 255, 0.05);
&::after {
transform: translateY(-50%) rotate(180deg);
color: var(--enterprise-gold);
}
} }
} }
.navbar__sub-menu { .navbar__sub-menu {
position: absolute; position: absolute !important;
top: calc(100% + 8px); top: calc(100% + 12px) !important;
left: 50%; left: 50% !important;
transform: translateX(-50%); transform: translateX(-50%) !important;
background: rgba(0, 0, 0, 0.92); background: #1a1a1a !important;
border-radius: 8px; border-radius: 12px;
box-shadow: box-shadow:
0 20px 60px rgba(0, 0, 0, 0.5), 0 25px 70px rgba(0, 0, 0, 0.7),
0 8px 25px rgba(0, 0, 0, 0.3), 0 10px 35px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.08); 0 0 0 1px rgba(255, 255, 255, 0.2) !important;
padding: 4px 0; padding: 12px !important;
min-width: 240px; min-width: 280px !important;
max-width: 320px;
max-height: 500px;
overflow-y: auto;
overflow-x: hidden;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateX(-50%) translateY(-10px) scale(0.96);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000; z-index: 9999 !important;
border: 1px solid rgba(255, 255, 255, 0.12); backdrop-filter: blur(25px) saturate(180%);
backdrop-filter: blur(20px);
// Custom scrollbar styles
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
margin: 8px 0;
}
&::-webkit-scrollbar-thumb {
background: rgba(212, 175, 55, 0.4);
border-radius: 10px;
&:hover {
background: rgba(212, 175, 55, 0.6);
}
}
&.show { &.show {
opacity: 1; opacity: 1 !important;
visibility: visible; visibility: visible !important;
transform: translateX(-50%) translateY(0) scale(1); transform: translateX(-50%) translateY(0) scale(1) !important;
display: block !important;
} }
&::before { &::before {
content: ""; content: "";
position: absolute; position: absolute;
top: -7px; top: -8px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 0; width: 0;
height: 0; height: 0;
border-left: 7px solid transparent; border-left: 8px solid transparent;
border-right: 7px solid transparent; border-right: 8px solid transparent;
border-bottom: 7px solid rgba(0, 0, 0, 0.92); border-bottom: 8px solid #1a1a1a;
filter: drop-shadow(0 -2px 6px rgba(0, 0, 0, 0.4)); filter: drop-shadow(0 -3px 8px rgba(0, 0, 0, 0.5));
} }
li { li {
list-style: none; list-style: none;
margin: 1px 6px; margin: 2px 0;
border-radius: 6px; border-radius: 8px;
overflow: hidden; overflow: visible;
opacity: 1 !important;
visibility: visible !important;
a { a {
display: flex; display: block !important;
align-items: center; padding: 14px 20px !important;
padding: 10px 16px; color: #ffffff !important;
color: rgba(255, 255, 255, 0.85); font-size: 14px !important;
font-size: 13px; font-weight: 500 !important;
font-weight: 500; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
text-decoration: none; text-decoration: none !important;
border-radius: 6px; border-radius: 8px;
letter-spacing: 0.2px; letter-spacing: 0.3px;
line-height: 1.4; line-height: 1.5;
opacity: 1 !important;
visibility: visible !important;
&::before { &::before {
content: ""; content: "";
@@ -173,30 +219,33 @@
left: 0; left: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 2px; width: 3px;
background: linear-gradient(135deg, var(--enterprise-gold), #f4d03f); background: linear-gradient(135deg, var(--enterprise-gold), #f4d03f);
transform: scaleY(0); transform: scaleY(0);
transition: transform 0.25s ease; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 0 1px 1px 0; border-radius: 0 2px 2px 0;
} }
&::after { &::after {
content: ""; content: "";
position: absolute; position: absolute;
right: 16px; right: 20px;
opacity: 0; opacity: 0;
transform: translateX(-3px); transform: translateX(-5px);
transition: all 0.25s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--enterprise-gold); color: var(--enterprise-gold);
font-weight: 600; font-weight: 700;
font-size: 10px; font-size: 14px;
} }
&:hover { &:hover {
color: var(--enterprise-gold); color: var(--enterprise-gold);
background: rgba(212, 175, 55, 0.12); background: linear-gradient(135deg, rgba(212, 175, 55, 0.18), rgba(212, 175, 55, 0.08));
transform: translateX(3px); transform: translateX(4px);
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.15); box-shadow:
0 4px 12px rgba(212, 175, 55, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
padding-right: 35px;
&::before { &::before {
transform: scaleY(1); transform: scaleY(1);
@@ -210,9 +259,11 @@
&.active-current-sub { &.active-current-sub {
color: var(--enterprise-gold); color: var(--enterprise-gold);
background: rgba(212, 175, 55, 0.18); background: linear-gradient(135deg, rgba(212, 175, 55, 0.22), rgba(212, 175, 55, 0.12));
font-weight: 600; font-weight: 600;
box-shadow: 0 1px 6px rgba(212, 175, 55, 0.25); box-shadow:
0 2px 10px rgba(212, 175, 55, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
&::before { &::before {
transform: scaleY(1); transform: scaleY(1);
@@ -222,34 +273,34 @@
// Loading and error states // Loading and error states
.text-muted { .text-muted {
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.65) !important;
font-style: italic; font-style: italic;
font-size: 12px; font-size: 13px;
padding: 10px 16px; padding: 14px 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
letter-spacing: 0.2px; letter-spacing: 0.3px;
&::before { &::before {
content: ""; content: "";
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
font-size: 10px; font-size: 12px;
} }
} }
.text-danger { .text-danger {
color: #ff6b6b; color: #ff6b6b !important;
font-size: 12px; font-size: 13px;
padding: 10px 16px; padding: 14px 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
letter-spacing: 0.2px; letter-spacing: 0.3px;
&::before { &::before {
content: "⚠️"; content: "⚠️";
font-size: 10px; font-size: 12px;
} }
} }
} }
@@ -259,6 +310,17 @@
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@keyframes fadeInSlideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.navbar__options { .navbar__options {
display: flex; display: flex;
@@ -349,21 +411,102 @@
} }
} }
} }
.navbar__item--has-children {
.navbar__dropdown-label {
&:hover {
color: var(--primary-color);
background: rgba(0, 0, 0, 0.04);
&::after {
color: var(--primary-color);
}
}
&.navbar__item-active {
color: var(--primary-color);
background: rgba(212, 175, 55, 0.12);
&::after {
color: var(--primary-color);
}
}
}
&:hover .navbar__dropdown-label {
color: var(--primary-color);
background: rgba(0, 0, 0, 0.04);
&::after {
color: var(--primary-color);
}
}
}
.navbar__sub-menu { .navbar__sub-menu {
background-color: white; background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.12);
box-shadow:
0 25px 70px rgba(0, 0, 0, 0.15),
0 10px 35px rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(25px) saturate(180%);
&::before { &::before {
border-bottom-color: white; border-bottom-color: #ffffff;
}
// Custom scrollbar for light mode
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba(212, 175, 55, 0.5);
&:hover {
background: rgba(212, 175, 55, 0.7);
}
} }
li a { li a {
color: var(--secondary-color); display: block !important;
color: #222222 !important;
opacity: 1 !important;
visibility: visible !important;
&:hover { &::before {
background: linear-gradient(135deg, var(--primary-color), var(--enterprise-gold));
}
&::after {
color: var(--primary-color); color: var(--primary-color);
} }
&:hover {
color: var(--primary-color) !important;
background: linear-gradient(135deg, rgba(212, 175, 55, 0.15), rgba(212, 175, 55, 0.08));
box-shadow:
0 4px 12px rgba(212, 175, 55, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
&.active-current-sub {
color: var(--primary-color) !important;
background: linear-gradient(135deg, rgba(212, 175, 55, 0.2), rgba(212, 175, 55, 0.1));
box-shadow:
0 2px 10px rgba(212, 175, 55, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
}
// Loading and error states for light mode
.text-muted {
color: rgba(0, 0, 0, 0.55) !important;
}
.text-danger {
color: #dc3545 !important;
} }
} }
@@ -936,6 +1079,20 @@
} }
// Responsive styles for enterprise header // Responsive styles for enterprise header
@media (max-width: 1199.98px) {
.tp-header {
.navbar__sub-menu {
min-width: 240px;
max-width: 280px;
li a {
padding: 12px 18px;
font-size: 13px;
}
}
}
}
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.tp-header { .tp-header {
.navbar__menu { .navbar__menu {

File diff suppressed because it is too large Load Diff