updates
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
"""add_privacy_terms_refunds_to_page_type_enum
|
||||
|
||||
Revision ID: 0e2dc5df18c3
|
||||
Revises: f2a3b4c5d6e7
|
||||
Create Date: 2025-11-21 10:25:07.463477
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0e2dc5df18c3'
|
||||
down_revision = 'f2a3b4c5d6e7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# For MySQL/MariaDB, we need to alter the enum column to add new values
|
||||
# First, modify the column to allow the new enum values
|
||||
op.execute("""
|
||||
ALTER TABLE page_contents
|
||||
MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo', 'privacy', 'terms', 'refunds')
|
||||
NOT NULL
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove the new enum values
|
||||
op.execute("""
|
||||
ALTER TABLE page_contents
|
||||
MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo')
|
||||
NOT NULL
|
||||
""")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""add_cancellation_accessibility_faq_to_page_type_enum
|
||||
|
||||
Revision ID: 9bb08492a382
|
||||
Revises: 0e2dc5df18c3
|
||||
Create Date: 2025-11-21 10:39:56.040401
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9bb08492a382'
|
||||
down_revision = '0e2dc5df18c3'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add new enum values: cancellation, accessibility, faq
|
||||
op.execute("""
|
||||
ALTER TABLE page_contents
|
||||
MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo', 'privacy', 'terms', 'refunds', 'cancellation', 'accessibility', 'faq')
|
||||
NOT NULL
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove the new enum values
|
||||
op.execute("""
|
||||
ALTER TABLE page_contents
|
||||
MODIFY COLUMN page_type ENUM('home', 'contact', 'about', 'footer', 'seo', 'privacy', 'terms', 'refunds')
|
||||
NOT NULL
|
||||
""")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
517
Backend/seed_policy_pages.py
Executable file
517
Backend/seed_policy_pages.py
Executable file
@@ -0,0 +1,517 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.models.page_content import PageContent, PageType
|
||||
from datetime import datetime
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
return db
|
||||
finally:
|
||||
pass
|
||||
|
||||
def seed_privacy_policy(db: Session):
|
||||
print("=" * 80)
|
||||
print("SEEDING PRIVACY POLICY PAGE CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
privacy_content = """
|
||||
<h2>Introduction</h2>
|
||||
<p>At our hotel, we are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website or use our services.</p>
|
||||
|
||||
<h2>Information We Collect</h2>
|
||||
<p>We collect information that you provide directly to us, including:</p>
|
||||
<ul>
|
||||
<li>Personal identification information (name, email address, phone number)</li>
|
||||
<li>Payment information (credit card details, billing address)</li>
|
||||
<li>Booking and reservation details</li>
|
||||
<li>Preferences and special requests</li>
|
||||
</ul>
|
||||
|
||||
<h2>How We Use Your Information</h2>
|
||||
<p>We use the information we collect to:</p>
|
||||
<ul>
|
||||
<li>Process and manage your bookings and reservations</li>
|
||||
<li>Communicate with you about your bookings and our services</li>
|
||||
<li>Improve our services and customer experience</li>
|
||||
<li>Send you promotional materials (with your consent)</li>
|
||||
<li>Comply with legal obligations</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data Security</h2>
|
||||
<p>We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction.</p>
|
||||
|
||||
<h2>Your Rights</h2>
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<li>Access your personal information</li>
|
||||
<li>Correct inaccurate information</li>
|
||||
<li>Request deletion of your information</li>
|
||||
<li>Object to processing of your information</li>
|
||||
<li>Data portability</li>
|
||||
</ul>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>If you have any questions about this Privacy Policy, please contact us at privacy@hotel.com.</p>
|
||||
|
||||
<p><strong>Last updated:</strong> """ + datetime.now().strftime("%B %d, %Y") + """</p>
|
||||
"""
|
||||
|
||||
privacy_data = {
|
||||
"title": "Privacy Policy",
|
||||
"subtitle": "Your privacy is important to us",
|
||||
"description": "Learn how we collect, use, and protect your personal information.",
|
||||
"content": privacy_content,
|
||||
"meta_title": "Privacy Policy - Luxury Hotel | Data Protection & Privacy",
|
||||
"meta_description": "Read our privacy policy to understand how we collect, use, and protect your personal information when you use our hotel booking services."
|
||||
}
|
||||
|
||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.PRIVACY).first()
|
||||
|
||||
if existing:
|
||||
for key, value in privacy_data.items():
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
print("✓ Updated existing privacy policy page content")
|
||||
else:
|
||||
new_content = PageContent(
|
||||
page_type=PageType.PRIVACY,
|
||||
**privacy_data
|
||||
)
|
||||
db.add(new_content)
|
||||
print("✓ Created new privacy policy page content")
|
||||
|
||||
db.commit()
|
||||
print("\n✅ Privacy policy page content seeded successfully!")
|
||||
print("=" * 80)
|
||||
|
||||
def seed_terms_conditions(db: Session):
|
||||
print("=" * 80)
|
||||
print("SEEDING TERMS & CONDITIONS PAGE CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
terms_content = """
|
||||
<h2>Agreement to Terms</h2>
|
||||
<p>By accessing and using our hotel's website and services, you accept and agree to be bound by the terms and provision of this agreement.</p>
|
||||
|
||||
<h2>Booking Terms</h2>
|
||||
<p>When making a reservation with us, you agree to:</p>
|
||||
<ul>
|
||||
<li>Provide accurate and complete information</li>
|
||||
<li>Be responsible for all charges incurred during your stay</li>
|
||||
<li>Comply with hotel policies and regulations</li>
|
||||
<li>Respect other guests and hotel property</li>
|
||||
</ul>
|
||||
|
||||
<h2>Payment Terms</h2>
|
||||
<p>Payment terms include:</p>
|
||||
<ul>
|
||||
<li>A 20% deposit is required to secure your booking</li>
|
||||
<li>The remaining balance is due on arrival at the hotel</li>
|
||||
<li>All prices are subject to applicable taxes and fees</li>
|
||||
<li>Payment methods accepted: credit cards, debit cards, and cash</li>
|
||||
</ul>
|
||||
|
||||
<h2>Cancellation Policy</h2>
|
||||
<p>Our cancellation policy is as follows:</p>
|
||||
<ul>
|
||||
<li>Cancellations made 48 hours or more before check-in: Full refund of deposit</li>
|
||||
<li>Cancellations made less than 48 hours before check-in: Deposit is non-refundable</li>
|
||||
<li>No-shows: Full booking amount is charged</li>
|
||||
</ul>
|
||||
|
||||
<h2>Check-in and Check-out</h2>
|
||||
<p>Standard check-in time is 3:00 PM and check-out time is 11:00 AM. Early check-in and late check-out may be available upon request and subject to availability.</p>
|
||||
|
||||
<h2>Guest Responsibilities</h2>
|
||||
<p>Guests are responsible for:</p>
|
||||
<ul>
|
||||
<li>Any damage to hotel property</li>
|
||||
<li>Compliance with all hotel rules and regulations</li>
|
||||
<li>Respecting quiet hours and other guests</li>
|
||||
<li>Proper use of hotel facilities</li>
|
||||
</ul>
|
||||
|
||||
<h2>Limitation of Liability</h2>
|
||||
<p>The hotel shall not be liable for any loss, damage, or injury to persons or property during your stay, except where such loss, damage, or injury is caused by our negligence.</p>
|
||||
|
||||
<h2>Modifications to Terms</h2>
|
||||
<p>We reserve the right to modify these terms at any time. Changes will be effective immediately upon posting on our website.</p>
|
||||
|
||||
<h2>Contact Information</h2>
|
||||
<p>For questions about these terms, please contact us at legal@hotel.com.</p>
|
||||
|
||||
<p><strong>Last updated:</strong> """ + datetime.now().strftime("%B %d, %Y") + """</p>
|
||||
"""
|
||||
|
||||
terms_data = {
|
||||
"title": "Terms & Conditions",
|
||||
"subtitle": "Please read these terms carefully",
|
||||
"description": "Terms and conditions governing your use of our hotel booking services.",
|
||||
"content": terms_content,
|
||||
"meta_title": "Terms & Conditions - Luxury Hotel | Booking Terms & Policies",
|
||||
"meta_description": "Read our terms and conditions to understand the rules and policies governing your bookings and stay at our luxury hotel."
|
||||
}
|
||||
|
||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.TERMS).first()
|
||||
|
||||
if existing:
|
||||
for key, value in terms_data.items():
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
print("✓ Updated existing terms & conditions page content")
|
||||
else:
|
||||
new_content = PageContent(
|
||||
page_type=PageType.TERMS,
|
||||
**terms_data
|
||||
)
|
||||
db.add(new_content)
|
||||
print("✓ Created new terms & conditions page content")
|
||||
|
||||
db.commit()
|
||||
print("\n✅ Terms & conditions page content seeded successfully!")
|
||||
print("=" * 80)
|
||||
|
||||
def seed_refunds_policy(db: Session):
|
||||
print("=" * 80)
|
||||
print("SEEDING REFUNDS POLICY PAGE CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
refunds_content = """
|
||||
<h2>Refund Policy Overview</h2>
|
||||
<p>At our hotel, we understand that plans can change. This policy outlines our refund procedures and timelines for various scenarios.</p>
|
||||
|
||||
<h2>Booking Cancellations</h2>
|
||||
<p>Refunds for cancelled bookings are processed as follows:</p>
|
||||
<ul>
|
||||
<li><strong>More than 48 hours before check-in:</strong> Full refund of the 20% deposit</li>
|
||||
<li><strong>48 hours or less before check-in:</strong> Deposit is non-refundable</li>
|
||||
<li><strong>No-show:</strong> No refund will be issued</li>
|
||||
</ul>
|
||||
|
||||
<h2>Early Check-out</h2>
|
||||
<p>If you check out earlier than your reserved departure date:</p>
|
||||
<ul>
|
||||
<li>You will be charged for the nights you stayed</li>
|
||||
<li>The remaining 80% balance for unused nights will be refunded</li>
|
||||
<li>Refunds are processed within 5-7 business days</li>
|
||||
</ul>
|
||||
|
||||
<h2>Service Issues</h2>
|
||||
<p>If you experience service issues during your stay:</p>
|
||||
<ul>
|
||||
<li>Please report the issue immediately to our front desk</li>
|
||||
<li>We will make every effort to resolve the issue promptly</li>
|
||||
<li>If the issue cannot be resolved, partial or full refunds may be considered on a case-by-case basis</li>
|
||||
</ul>
|
||||
|
||||
<h2>Refund Processing Time</h2>
|
||||
<p>Refunds are typically processed within:</p>
|
||||
<ul>
|
||||
<li><strong>Credit/Debit Cards:</strong> 5-7 business days</li>
|
||||
<li><strong>Bank Transfers:</strong> 7-10 business days</li>
|
||||
<li><strong>Cash Payments:</strong> Refunds will be processed via bank transfer</li>
|
||||
</ul>
|
||||
|
||||
<h2>Refund Method</h2>
|
||||
<p>Refunds will be issued to the original payment method used for the booking. If the original payment method is no longer available, please contact us to arrange an alternative refund method.</p>
|
||||
|
||||
<h2>Non-Refundable Bookings</h2>
|
||||
<p>Some special offers or promotional rates may be non-refundable. This will be clearly stated at the time of booking.</p>
|
||||
|
||||
<h2>Force Majeure</h2>
|
||||
<p>In cases of force majeure (natural disasters, pandemics, government restrictions, etc.), we will work with you to reschedule your booking or provide appropriate refunds based on the circumstances.</p>
|
||||
|
||||
<h2>Dispute Resolution</h2>
|
||||
<p>If you have concerns about a refund decision, please contact our customer service team. We are committed to resolving all disputes fairly and promptly.</p>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>For refund inquiries, please contact us at refunds@hotel.com or call our customer service line.</p>
|
||||
|
||||
<p><strong>Last updated:</strong> """ + datetime.now().strftime("%B %d, %Y") + """</p>
|
||||
"""
|
||||
|
||||
refunds_data = {
|
||||
"title": "Refunds Policy",
|
||||
"subtitle": "Our commitment to fair refunds",
|
||||
"description": "Learn about our refund policies and procedures for bookings and cancellations.",
|
||||
"content": refunds_content,
|
||||
"meta_title": "Refunds Policy - Luxury Hotel | Cancellation & Refund Terms",
|
||||
"meta_description": "Understand our refund policy, including cancellation terms, processing times, and how to request refunds for your hotel bookings."
|
||||
}
|
||||
|
||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.REFUNDS).first()
|
||||
|
||||
if existing:
|
||||
for key, value in refunds_data.items():
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
print("✓ Updated existing refunds policy page content")
|
||||
else:
|
||||
new_content = PageContent(
|
||||
page_type=PageType.REFUNDS,
|
||||
**refunds_data
|
||||
)
|
||||
db.add(new_content)
|
||||
print("✓ Created new refunds policy page content")
|
||||
|
||||
db.commit()
|
||||
print("\n✅ Refunds policy page content seeded successfully!")
|
||||
print("=" * 80)
|
||||
|
||||
def seed_cancellation_policy(db: Session):
|
||||
print("=" * 80)
|
||||
print("SEEDING CANCELLATION POLICY PAGE CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
cancellation_content = """
|
||||
<h2>Cancellation Policy Overview</h2>
|
||||
<p>We understand that plans can change. This policy outlines our cancellation procedures and fees.</p>
|
||||
|
||||
<h2>Standard Cancellation Terms</h2>
|
||||
<p>For standard bookings, the following cancellation terms apply:</p>
|
||||
<ul>
|
||||
<li><strong>More than 48 hours before check-in:</strong> Full refund of the 20% deposit</li>
|
||||
<li><strong>48 hours or less before check-in:</strong> Deposit is non-refundable</li>
|
||||
<li><strong>No-show:</strong> No refund will be issued</li>
|
||||
</ul>
|
||||
|
||||
<h2>Special Rate Bookings</h2>
|
||||
<p>Some special rates or promotional offers may have different cancellation terms. Please review your booking confirmation for specific details.</p>
|
||||
|
||||
<h2>How to Cancel</h2>
|
||||
<p>To cancel your booking:</p>
|
||||
<ul>
|
||||
<li>Log into your account and visit "My Bookings"</li>
|
||||
<li>Select the booking you wish to cancel</li>
|
||||
<li>Click "Cancel Booking" and confirm</li>
|
||||
<li>Or contact us directly via phone or email</li>
|
||||
</ul>
|
||||
|
||||
<h2>Refund Processing</h2>
|
||||
<p>Refunds will be processed to the original payment method within 5-10 business days after cancellation confirmation.</p>
|
||||
|
||||
<p><strong>Last updated:</strong> """ + datetime.now().strftime("%B %d, %Y") + """</p>
|
||||
"""
|
||||
|
||||
cancellation_data = {
|
||||
"title": "Cancellation Policy",
|
||||
"subtitle": "Flexible cancellation options for your peace of mind",
|
||||
"description": "Learn about our cancellation policy, fees, and refund procedures.",
|
||||
"content": cancellation_content,
|
||||
"meta_title": "Cancellation Policy - Luxury Hotel | Booking Cancellation Terms",
|
||||
"meta_description": "Review our cancellation policy to understand cancellation fees, refund procedures, and terms for modifying or canceling your hotel booking."
|
||||
}
|
||||
|
||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.CANCELLATION).first()
|
||||
|
||||
if existing:
|
||||
for key, value in cancellation_data.items():
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
print("✓ Updated existing cancellation policy page content")
|
||||
else:
|
||||
new_content = PageContent(
|
||||
page_type=PageType.CANCELLATION,
|
||||
**cancellation_data
|
||||
)
|
||||
db.add(new_content)
|
||||
print("✓ Created new cancellation policy page content")
|
||||
|
||||
db.commit()
|
||||
print("\n✅ Cancellation policy page content seeded successfully!")
|
||||
print("=" * 80)
|
||||
|
||||
def seed_accessibility_policy(db: Session):
|
||||
print("=" * 80)
|
||||
print("SEEDING ACCESSIBILITY PAGE CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
accessibility_content = """
|
||||
<h2>Our Commitment to Accessibility</h2>
|
||||
<p>We are committed to ensuring that our hotel and website are accessible to all guests, regardless of ability. We strive to provide an inclusive experience for everyone.</p>
|
||||
|
||||
<h2>Hotel Accessibility Features</h2>
|
||||
<p>Our hotel offers the following accessibility features:</p>
|
||||
<ul>
|
||||
<li>Wheelchair-accessible rooms with roll-in showers</li>
|
||||
<li>Accessible parking spaces</li>
|
||||
<li>Elevator access to all floors</li>
|
||||
<li>Ramp access to main entrances</li>
|
||||
<li>Accessible public restrooms</li>
|
||||
<li>Visual and auditory emergency alarms</li>
|
||||
<li>Service animal friendly</li>
|
||||
</ul>
|
||||
|
||||
<h2>Website Accessibility</h2>
|
||||
<p>We are continuously working to improve the accessibility of our website. Our website includes:</p>
|
||||
<ul>
|
||||
<li>Keyboard navigation support</li>
|
||||
<li>Screen reader compatibility</li>
|
||||
<li>Alt text for images</li>
|
||||
<li>High contrast options</li>
|
||||
<li>Resizable text</li>
|
||||
</ul>
|
||||
|
||||
<h2>Requesting Accommodations</h2>
|
||||
<p>If you require specific accommodations during your stay, please contact us at least 48 hours before your arrival. We will do our best to accommodate your needs.</p>
|
||||
|
||||
<h2>Feedback</h2>
|
||||
<p>We welcome feedback on how we can improve accessibility. Please contact us with your suggestions or concerns.</p>
|
||||
|
||||
<p><strong>Last updated:</strong> """ + datetime.now().strftime("%B %d, %Y") + """</p>
|
||||
"""
|
||||
|
||||
accessibility_data = {
|
||||
"title": "Accessibility",
|
||||
"subtitle": "Committed to providing an inclusive experience for all guests",
|
||||
"description": "Learn about our accessibility features and accommodations.",
|
||||
"content": accessibility_content,
|
||||
"meta_title": "Accessibility - Luxury Hotel | Accessible Accommodations",
|
||||
"meta_description": "Discover our commitment to accessibility, including accessible rooms, facilities, and website features for guests with disabilities."
|
||||
}
|
||||
|
||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.ACCESSIBILITY).first()
|
||||
|
||||
if existing:
|
||||
for key, value in accessibility_data.items():
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
print("✓ Updated existing accessibility page content")
|
||||
else:
|
||||
new_content = PageContent(
|
||||
page_type=PageType.ACCESSIBILITY,
|
||||
**accessibility_data
|
||||
)
|
||||
db.add(new_content)
|
||||
print("✓ Created new accessibility page content")
|
||||
|
||||
db.commit()
|
||||
print("\n✅ Accessibility page content seeded successfully!")
|
||||
print("=" * 80)
|
||||
|
||||
def seed_faq_page(db: Session):
|
||||
print("=" * 80)
|
||||
print("SEEDING FAQ PAGE CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
faq_content = """
|
||||
<h2>Frequently Asked Questions</h2>
|
||||
<p>Find answers to common questions about our hotel, bookings, and services.</p>
|
||||
|
||||
<h2>Booking & Reservations</h2>
|
||||
<h3>How do I make a reservation?</h3>
|
||||
<p>You can make a reservation online through our website, by phone, or by email. Simply select your dates, choose your room, and complete the booking process.</p>
|
||||
|
||||
<h3>What is the deposit requirement?</h3>
|
||||
<p>A 20% deposit is required to secure your booking. The remaining balance is due upon arrival at the hotel.</p>
|
||||
|
||||
<h3>Can I modify my booking?</h3>
|
||||
<p>Yes, you can modify your booking by logging into your account and visiting "My Bookings", or by contacting us directly. Changes are subject to availability and may incur fees.</p>
|
||||
|
||||
<h2>Check-in & Check-out</h2>
|
||||
<h3>What are your check-in and check-out times?</h3>
|
||||
<p>Check-in is from 3:00 PM, and check-out is by 11:00 AM. Early check-in and late check-out may be available upon request, subject to availability.</p>
|
||||
|
||||
<h3>Do you offer early check-in or late check-out?</h3>
|
||||
<p>Early check-in and late check-out are available upon request, subject to availability. Additional fees may apply.</p>
|
||||
|
||||
<h2>Payment & Cancellation</h2>
|
||||
<h3>What payment methods do you accept?</h3>
|
||||
<p>We accept major credit cards, debit cards, and bank transfers. Payment can be made online or at the hotel.</p>
|
||||
|
||||
<h3>What is your cancellation policy?</h3>
|
||||
<p>For cancellations made more than 48 hours before check-in, the deposit is fully refundable. Cancellations made 48 hours or less before check-in are non-refundable. Please see our Cancellation Policy page for full details.</p>
|
||||
|
||||
<h2>Hotel Services & Amenities</h2>
|
||||
<h3>What amenities are included?</h3>
|
||||
<p>Our hotel offers complimentary Wi-Fi, parking, fitness center access, and more. Please check the room details for specific amenities.</p>
|
||||
|
||||
<h3>Do you have parking available?</h3>
|
||||
<p>Yes, we offer complimentary parking for all guests. Valet parking is also available for an additional fee.</p>
|
||||
|
||||
<h3>Is Wi-Fi available?</h3>
|
||||
<p>Yes, complimentary high-speed Wi-Fi is available throughout the hotel.</p>
|
||||
|
||||
<h2>Special Requests</h2>
|
||||
<h3>Can I request a specific room?</h3>
|
||||
<p>Yes, you can make special requests when booking. We will do our best to accommodate your preferences, subject to availability.</p>
|
||||
|
||||
<h3>Do you accommodate dietary restrictions?</h3>
|
||||
<p>Yes, please inform us of any dietary restrictions or allergies when making your reservation, and we will do our best to accommodate your needs.</p>
|
||||
|
||||
<h2>Contact & Support</h2>
|
||||
<h3>How can I contact the hotel?</h3>
|
||||
<p>You can contact us by phone, email, or through our website's contact form. Our team is available 24/7 to assist you.</p>
|
||||
|
||||
<p><strong>Last updated:</strong> """ + datetime.now().strftime("%B %d, %Y") + """</p>
|
||||
"""
|
||||
|
||||
faq_data = {
|
||||
"title": "Frequently Asked Questions",
|
||||
"subtitle": "Find answers to common questions",
|
||||
"description": "Get answers to frequently asked questions about bookings, services, and policies.",
|
||||
"content": faq_content,
|
||||
"meta_title": "FAQ - Luxury Hotel | Frequently Asked Questions",
|
||||
"meta_description": "Find answers to common questions about hotel bookings, check-in, payment, cancellation, amenities, and more."
|
||||
}
|
||||
|
||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.FAQ).first()
|
||||
|
||||
if existing:
|
||||
for key, value in faq_data.items():
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
print("✓ Updated existing FAQ page content")
|
||||
else:
|
||||
new_content = PageContent(
|
||||
page_type=PageType.FAQ,
|
||||
**faq_data
|
||||
)
|
||||
db.add(new_content)
|
||||
print("✓ Created new FAQ page content")
|
||||
|
||||
db.commit()
|
||||
print("\n✅ FAQ page content seeded successfully!")
|
||||
print("=" * 80)
|
||||
|
||||
def main():
|
||||
db = get_db()
|
||||
try:
|
||||
print("\n")
|
||||
seed_privacy_policy(db)
|
||||
print("\n")
|
||||
seed_terms_conditions(db)
|
||||
print("\n")
|
||||
seed_refunds_policy(db)
|
||||
print("\n")
|
||||
seed_cancellation_policy(db)
|
||||
print("\n")
|
||||
seed_accessibility_policy(db)
|
||||
print("\n")
|
||||
seed_faq_page(db)
|
||||
print("\n")
|
||||
print("=" * 80)
|
||||
print("✅ ALL POLICY PAGES SEEDED SUCCESSFULLY!")
|
||||
print("=" * 80)
|
||||
print("\n")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Binary file not shown.
@@ -91,10 +91,8 @@ async def health_check(db: Session=Depends(get_db)):
|
||||
async def metrics():
|
||||
return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()}
|
||||
app.include_router(auth_routes.router, prefix='/api')
|
||||
app.include_router(privacy_routes.router, prefix='/api')
|
||||
app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes
|
||||
from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes, privacy_routes, terms_routes, refunds_routes, cancellation_routes, accessibility_routes, faq_routes
|
||||
app.include_router(room_routes.router, prefix='/api')
|
||||
app.include_router(booking_routes.router, prefix='/api')
|
||||
app.include_router(payment_routes.router, prefix='/api')
|
||||
@@ -115,6 +113,12 @@ app.include_router(home_routes.router, prefix='/api')
|
||||
app.include_router(about_routes.router, prefix='/api')
|
||||
app.include_router(contact_content_routes.router, prefix='/api')
|
||||
app.include_router(footer_routes.router, prefix='/api')
|
||||
app.include_router(privacy_routes.router, prefix='/api')
|
||||
app.include_router(terms_routes.router, prefix='/api')
|
||||
app.include_router(refunds_routes.router, prefix='/api')
|
||||
app.include_router(cancellation_routes.router, prefix='/api')
|
||||
app.include_router(accessibility_routes.router, prefix='/api')
|
||||
app.include_router(faq_routes.router, prefix='/api')
|
||||
app.include_router(chat_routes.router, prefix='/api')
|
||||
app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
@@ -136,6 +140,12 @@ app.include_router(home_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(terms_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(refunds_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(cancellation_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(accessibility_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(faq_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(chat_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(page_content_routes.router, prefix='/api')
|
||||
app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
Binary file not shown.
@@ -10,11 +10,17 @@ class PageType(str, enum.Enum):
|
||||
ABOUT = 'about'
|
||||
FOOTER = 'footer'
|
||||
SEO = 'seo'
|
||||
PRIVACY = 'privacy'
|
||||
TERMS = 'terms'
|
||||
REFUNDS = 'refunds'
|
||||
CANCELLATION = 'cancellation'
|
||||
ACCESSIBILITY = 'accessibility'
|
||||
FAQ = 'faq'
|
||||
|
||||
class PageContent(Base):
|
||||
__tablename__ = 'page_contents'
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
page_type = Column(SQLEnum(PageType), nullable=False, unique=True, index=True)
|
||||
page_type = Column(SQLEnum(PageType, values_callable=lambda x: [e.value for e in x], native_enum=False, length=50), nullable=False, unique=True, index=True)
|
||||
title = Column(String(500), nullable=True)
|
||||
subtitle = Column(String(1000), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Backend/src/routes/__pycache__/faq_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/faq_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
Backend/src/routes/__pycache__/refunds_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/refunds_routes.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Backend/src/routes/__pycache__/terms_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/terms_routes.cpython-312.pyc
Normal file
Binary file not shown.
44
Backend/src/routes/accessibility_routes.py
Normal file
44
Backend/src/routes/accessibility_routes.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/accessibility', tags=['accessibility'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
return {
|
||||
'id': content.id,
|
||||
'page_type': content.page_type.value,
|
||||
'title': content.title,
|
||||
'subtitle': content.subtitle,
|
||||
'description': content.description,
|
||||
'content': content.content,
|
||||
'meta_title': content.meta_title,
|
||||
'meta_description': content.meta_description,
|
||||
'meta_keywords': content.meta_keywords,
|
||||
'og_title': content.og_title,
|
||||
'og_description': content.og_description,
|
||||
'og_image': content.og_image,
|
||||
'canonical_url': content.canonical_url,
|
||||
'is_active': content.is_active,
|
||||
'created_at': content.created_at.isoformat() if content.created_at else None,
|
||||
'updated_at': content.updated_at.isoformat() if content.updated_at else None
|
||||
}
|
||||
|
||||
@router.get('/')
|
||||
async def get_accessibility_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.ACCESSIBILITY).first()
|
||||
if not content:
|
||||
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
|
||||
if not content.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Accessibility page is currently disabled')
|
||||
content_dict = serialize_page_content(content)
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching accessibility content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching accessibility content: {str(e)}')
|
||||
|
||||
44
Backend/src/routes/cancellation_routes.py
Normal file
44
Backend/src/routes/cancellation_routes.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/cancellation', tags=['cancellation'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
return {
|
||||
'id': content.id,
|
||||
'page_type': content.page_type.value,
|
||||
'title': content.title,
|
||||
'subtitle': content.subtitle,
|
||||
'description': content.description,
|
||||
'content': content.content,
|
||||
'meta_title': content.meta_title,
|
||||
'meta_description': content.meta_description,
|
||||
'meta_keywords': content.meta_keywords,
|
||||
'og_title': content.og_title,
|
||||
'og_description': content.og_description,
|
||||
'og_image': content.og_image,
|
||||
'canonical_url': content.canonical_url,
|
||||
'is_active': content.is_active,
|
||||
'created_at': content.created_at.isoformat() if content.created_at else None,
|
||||
'updated_at': content.updated_at.isoformat() if content.updated_at else None
|
||||
}
|
||||
|
||||
@router.get('/')
|
||||
async def get_cancellation_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.CANCELLATION).first()
|
||||
if not content:
|
||||
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
|
||||
if not content.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Cancellation policy page is currently disabled')
|
||||
content_dict = serialize_page_content(content)
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching cancellation content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching cancellation content: {str(e)}')
|
||||
|
||||
44
Backend/src/routes/faq_routes.py
Normal file
44
Backend/src/routes/faq_routes.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/faq', tags=['faq'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
return {
|
||||
'id': content.id,
|
||||
'page_type': content.page_type.value,
|
||||
'title': content.title,
|
||||
'subtitle': content.subtitle,
|
||||
'description': content.description,
|
||||
'content': content.content,
|
||||
'meta_title': content.meta_title,
|
||||
'meta_description': content.meta_description,
|
||||
'meta_keywords': content.meta_keywords,
|
||||
'og_title': content.og_title,
|
||||
'og_description': content.og_description,
|
||||
'og_image': content.og_image,
|
||||
'canonical_url': content.canonical_url,
|
||||
'is_active': content.is_active,
|
||||
'created_at': content.created_at.isoformat() if content.created_at else None,
|
||||
'updated_at': content.updated_at.isoformat() if content.updated_at else None
|
||||
}
|
||||
|
||||
@router.get('/')
|
||||
async def get_faq_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.FAQ).first()
|
||||
if not content:
|
||||
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
|
||||
if not content.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='FAQ page is currently disabled')
|
||||
content_dict = serialize_page_content(content)
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching FAQ content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching FAQ content: {str(e)}')
|
||||
|
||||
@@ -1,39 +1,110 @@
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from datetime import datetime
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..config.settings import settings
|
||||
from ..middleware.cookie_consent import COOKIE_CONSENT_COOKIE_NAME, _parse_consent_cookie
|
||||
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
||||
from ..schemas.privacy import CookieCategoryPreferences, CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest
|
||||
from ..models.page_content import PageContent, PageType
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
from ..schemas.privacy import CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest, CookieCategoryPreferences
|
||||
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/privacy', tags=['privacy'])
|
||||
|
||||
@router.get('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
|
||||
async def get_cookie_consent(request: Request) -> CookieConsentResponse:
|
||||
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
||||
consent = _parse_consent_cookie(raw_cookie)
|
||||
consent.categories.necessary = True
|
||||
return CookieConsentResponse(data=consent)
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
return {
|
||||
'id': content.id,
|
||||
'page_type': content.page_type.value,
|
||||
'title': content.title,
|
||||
'subtitle': content.subtitle,
|
||||
'description': content.description,
|
||||
'content': content.content,
|
||||
'meta_title': content.meta_title,
|
||||
'meta_description': content.meta_description,
|
||||
'meta_keywords': content.meta_keywords,
|
||||
'og_title': content.og_title,
|
||||
'og_description': content.og_description,
|
||||
'og_image': content.og_image,
|
||||
'canonical_url': content.canonical_url,
|
||||
'is_active': content.is_active,
|
||||
'created_at': content.created_at.isoformat() if content.created_at else None,
|
||||
'updated_at': content.updated_at.isoformat() if content.updated_at else None
|
||||
}
|
||||
|
||||
@router.post('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
|
||||
async def update_cookie_consent(request: UpdateCookieConsentRequest, response: Response) -> CookieConsentResponse:
|
||||
existing_raw = response.headers.get('cookie')
|
||||
categories = CookieCategoryPreferences()
|
||||
if request.analytics is not None:
|
||||
categories.analytics = request.analytics
|
||||
if request.marketing is not None:
|
||||
categories.marketing = request.marketing
|
||||
if request.preferences is not None:
|
||||
categories.preferences = request.preferences
|
||||
categories.necessary = True
|
||||
consent = CookieConsent(categories=categories, has_decided=True)
|
||||
response.set_cookie(key=COOKIE_CONSENT_COOKIE_NAME, value=consent.model_dump_json(), httponly=True, secure=settings.is_production, samesite='lax', max_age=365 * 24 * 60 * 60, path='/')
|
||||
logger.info('Cookie consent updated: analytics=%s, marketing=%s, preferences=%s', consent.categories.analytics, consent.categories.marketing, consent.categories.preferences)
|
||||
return CookieConsentResponse(data=consent)
|
||||
@router.get('/')
|
||||
async def get_privacy_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.PRIVACY).first()
|
||||
if not content:
|
||||
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
|
||||
if not content.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Privacy policy page is currently disabled')
|
||||
content_dict = serialize_page_content(content)
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching privacy content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching privacy content: {str(e)}')
|
||||
|
||||
@router.get('/config', response_model=PublicPrivacyConfigResponse, status_code=status.HTTP_200_OK)
|
||||
async def get_public_privacy_config(db: Session=Depends(get_db)) -> PublicPrivacyConfigResponse:
|
||||
config = privacy_admin_service.get_public_privacy_config(db)
|
||||
return PublicPrivacyConfigResponse(data=config)
|
||||
@router.get('/cookie-consent', response_model=CookieConsentResponse)
|
||||
async def get_cookie_consent(db: Session=Depends(get_db)):
|
||||
"""
|
||||
Get the default cookie consent structure.
|
||||
Note: Actual consent is stored client-side (localStorage), this endpoint provides the default structure.
|
||||
"""
|
||||
try:
|
||||
# Return default consent structure
|
||||
# The actual consent state is managed client-side
|
||||
consent = CookieConsent(
|
||||
version=1,
|
||||
updated_at=datetime.utcnow(),
|
||||
has_decided=False,
|
||||
categories=CookieCategoryPreferences(
|
||||
necessary=True, # Always true
|
||||
analytics=False,
|
||||
marketing=False,
|
||||
preferences=False
|
||||
)
|
||||
)
|
||||
return CookieConsentResponse(status='success', data=consent)
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching cookie consent: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching cookie consent: {str(e)}')
|
||||
|
||||
@router.post('/cookie-consent', response_model=CookieConsentResponse)
|
||||
async def update_cookie_consent(payload: UpdateCookieConsentRequest, db: Session=Depends(get_db)):
|
||||
"""
|
||||
Update cookie consent preferences.
|
||||
Note: This endpoint acknowledges the consent update. Actual consent is stored client-side.
|
||||
"""
|
||||
try:
|
||||
# Create updated consent structure
|
||||
consent = CookieConsent(
|
||||
version=1,
|
||||
updated_at=datetime.utcnow(),
|
||||
has_decided=True,
|
||||
categories=CookieCategoryPreferences(
|
||||
necessary=True, # Always true
|
||||
analytics=payload.analytics if payload.analytics is not None else False,
|
||||
marketing=payload.marketing if payload.marketing is not None else False,
|
||||
preferences=payload.preferences if payload.preferences is not None else False
|
||||
)
|
||||
)
|
||||
return CookieConsentResponse(status='success', data=consent)
|
||||
except Exception as e:
|
||||
logger.error(f'Error updating cookie consent: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error updating cookie consent: {str(e)}')
|
||||
|
||||
@router.get('/config', response_model=PublicPrivacyConfigResponse)
|
||||
async def get_public_privacy_config(db: Session=Depends(get_db)):
|
||||
"""
|
||||
Get public privacy configuration including cookie policy settings and integration configs.
|
||||
This endpoint is public and does not require authentication.
|
||||
"""
|
||||
try:
|
||||
config = privacy_admin_service.get_public_privacy_config(db)
|
||||
return PublicPrivacyConfigResponse(status='success', data=config)
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching public privacy config: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching public privacy config: {str(e)}')
|
||||
|
||||
45
Backend/src/routes/refunds_routes.py
Normal file
45
Backend/src/routes/refunds_routes.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/refunds', tags=['refunds'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
return {
|
||||
'id': content.id,
|
||||
'page_type': content.page_type.value,
|
||||
'title': content.title,
|
||||
'subtitle': content.subtitle,
|
||||
'description': content.description,
|
||||
'content': content.content,
|
||||
'meta_title': content.meta_title,
|
||||
'meta_description': content.meta_description,
|
||||
'meta_keywords': content.meta_keywords,
|
||||
'og_title': content.og_title,
|
||||
'og_description': content.og_description,
|
||||
'og_image': content.og_image,
|
||||
'canonical_url': content.canonical_url,
|
||||
'is_active': content.is_active,
|
||||
'created_at': content.created_at.isoformat() if content.created_at else None,
|
||||
'updated_at': content.updated_at.isoformat() if content.updated_at else None
|
||||
}
|
||||
|
||||
@router.get('/')
|
||||
async def get_refunds_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.REFUNDS).first()
|
||||
if not content:
|
||||
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
|
||||
if not content.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Refunds policy page is currently disabled')
|
||||
content_dict = serialize_page_content(content)
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching refunds content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching refunds content: {str(e)}')
|
||||
|
||||
45
Backend/src/routes/terms_routes.py
Normal file
45
Backend/src/routes/terms_routes.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/terms', tags=['terms'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
return {
|
||||
'id': content.id,
|
||||
'page_type': content.page_type.value,
|
||||
'title': content.title,
|
||||
'subtitle': content.subtitle,
|
||||
'description': content.description,
|
||||
'content': content.content,
|
||||
'meta_title': content.meta_title,
|
||||
'meta_description': content.meta_description,
|
||||
'meta_keywords': content.meta_keywords,
|
||||
'og_title': content.og_title,
|
||||
'og_description': content.og_description,
|
||||
'og_image': content.og_image,
|
||||
'canonical_url': content.canonical_url,
|
||||
'is_active': content.is_active,
|
||||
'created_at': content.created_at.isoformat() if content.created_at else None,
|
||||
'updated_at': content.updated_at.isoformat() if content.updated_at else None
|
||||
}
|
||||
|
||||
@router.get('/')
|
||||
async def get_terms_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.TERMS).first()
|
||||
if not content:
|
||||
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
|
||||
if not content.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Terms & Conditions page is currently disabled')
|
||||
content_dict = serialize_page_content(content)
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching terms content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching terms content: {str(e)}')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CompanySettingsProvider } from './contexts/CompanySettingsContext';
|
||||
import { AuthModalProvider } from './contexts/AuthModalContext';
|
||||
import OfflineIndicator from './components/common/OfflineIndicator';
|
||||
import CookieConsentBanner from './components/common/CookieConsentBanner';
|
||||
import CookiePreferencesModal from './components/common/CookiePreferencesModal';
|
||||
import AnalyticsLoader from './components/common/AnalyticsLoader';
|
||||
import Loading from './components/common/Loading';
|
||||
import ScrollToTop from './components/common/ScrollToTop';
|
||||
@@ -53,6 +54,12 @@ const InvoicePage = lazy(() => import('./pages/customer/InvoicePage'));
|
||||
const ProfilePage = lazy(() => import('./pages/customer/ProfilePage'));
|
||||
const AboutPage = lazy(() => import('./pages/AboutPage'));
|
||||
const ContactPage = lazy(() => import('./pages/ContactPage'));
|
||||
const PrivacyPolicyPage = lazy(() => import('./pages/PrivacyPolicyPage'));
|
||||
const TermsPage = lazy(() => import('./pages/TermsPage'));
|
||||
const RefundsPolicyPage = lazy(() => import('./pages/RefundsPolicyPage'));
|
||||
const CancellationPolicyPage = lazy(() => import('./pages/CancellationPolicyPage'));
|
||||
const AccessibilityPage = lazy(() => import('./pages/AccessibilityPage'));
|
||||
const FAQPage = lazy(() => import('./pages/FAQPage'));
|
||||
|
||||
const AdminDashboardPage = lazy(() => import('./pages/admin/DashboardPage'));
|
||||
const InvoiceManagementPage = lazy(() => import('./pages/admin/InvoiceManagementPage'));
|
||||
@@ -188,6 +195,30 @@ function App() {
|
||||
path="contact"
|
||||
element={<ContactPage />}
|
||||
/>
|
||||
<Route
|
||||
path="privacy"
|
||||
element={<PrivacyPolicyPage />}
|
||||
/>
|
||||
<Route
|
||||
path="terms"
|
||||
element={<TermsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="refunds"
|
||||
element={<RefundsPolicyPage />}
|
||||
/>
|
||||
<Route
|
||||
path="cancellation"
|
||||
element={<CancellationPolicyPage />}
|
||||
/>
|
||||
<Route
|
||||
path="accessibility"
|
||||
element={<AccessibilityPage />}
|
||||
/>
|
||||
<Route
|
||||
path="faq"
|
||||
element={<FAQPage />}
|
||||
/>
|
||||
|
||||
{}
|
||||
<Route
|
||||
@@ -381,6 +412,7 @@ function App() {
|
||||
/>
|
||||
<OfflineIndicator />
|
||||
<CookieConsentBanner />
|
||||
<CookiePreferencesModal />
|
||||
<AnalyticsLoader />
|
||||
<AuthModalManager />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useCookieConsent } from '../../contexts/CookieConsentContext';
|
||||
|
||||
const CookiePreferencesLink: React.FC = () => {
|
||||
const { hasDecided } = useCookieConsent();
|
||||
|
||||
if (!hasDecided) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
window.dispatchEvent(new CustomEvent('open-cookie-preferences'));
|
||||
};
|
||||
|
||||
@@ -17,9 +9,9 @@ const CookiePreferencesLink: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="text-xs font-medium text-gray-500 underline hover:text-gray-700"
|
||||
className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide text-gray-700"
|
||||
>
|
||||
Cookie preferences
|
||||
Cookie Preferences
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
223
Frontend/src/components/common/CookiePreferencesModal.tsx
Normal file
223
Frontend/src/components/common/CookiePreferencesModal.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useCookieConsent } from '../../contexts/CookieConsentContext';
|
||||
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||
|
||||
const CookiePreferencesModal: React.FC = () => {
|
||||
const { consent, updateConsent } = useCookieConsent();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [analyticsChecked, setAnalyticsChecked] = useState(false);
|
||||
const [marketingChecked, setMarketingChecked] = useState(false);
|
||||
const [preferencesChecked, setPreferencesChecked] = useState(false);
|
||||
const modalRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickOutside(modalRef, () => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (consent) {
|
||||
setAnalyticsChecked(consent.categories.analytics);
|
||||
setMarketingChecked(consent.categories.marketing);
|
||||
setPreferencesChecked(consent.categories.preferences);
|
||||
}
|
||||
}, [consent]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOpenPreferences = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
window.addEventListener('open-cookie-preferences', handleOpenPreferences);
|
||||
return () => {
|
||||
window.removeEventListener('open-cookie-preferences', handleOpenPreferences);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleAcceptAll = async () => {
|
||||
await updateConsent({
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
preferences: true,
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleRejectNonEssential = async () => {
|
||||
await updateConsent({
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
preferences: false,
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleSaveSelection = async () => {
|
||||
await updateConsent({
|
||||
analytics: analyticsChecked,
|
||||
marketing: marketingChecked,
|
||||
preferences: preferencesChecked,
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/70 backdrop-blur-sm transition-opacity"
|
||||
onClick={() => setIsOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="relative transform overflow-hidden rounded-2xl bg-gradient-to-br from-zinc-950/95 via-zinc-900/95 to-black/95 text-left shadow-2xl border border-[#d4af37]/30 transition-all sm:my-8 sm:w-full sm:max-w-2xl"
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="absolute right-4 top-4 z-10 rounded-full p-2 text-gray-400 hover:bg-zinc-800/50 hover:text-white transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="px-6 py-6 sm:px-8 sm:py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-black/60 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.16em] text-[#d4af37]/90 ring-1 ring-[#d4af37]/30 mb-4">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#d4af37]" />
|
||||
Privacy Suite
|
||||
</div>
|
||||
<h2 className="text-2xl font-elegant font-bold text-white mb-2">
|
||||
Cookie Preferences
|
||||
</h2>
|
||||
<p className="text-sm text-zinc-300">
|
||||
Manage your cookie preferences. You can enable or disable different types of cookies below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cookie Categories */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="rounded-xl bg-black/40 p-4 ring-1 ring-zinc-800/80 backdrop-blur-md">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 h-5 w-5 rounded border border-[#d4af37]/50 bg-[#d4af37]/20 flex items-center justify-center">
|
||||
<span className="text-[#d4af37] text-xs">✓</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-zinc-50 mb-1">Strictly necessary</p>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Essential for security, authentication, and core booking flows. These are always enabled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-black/40 p-4 ring-1 ring-zinc-800/80 backdrop-blur-md">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
id="cookie-analytics-modal"
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-5 w-5 rounded border-zinc-500 bg-black/40 text-[#d4af37] focus:ring-[#d4af37] cursor-pointer"
|
||||
checked={analyticsChecked}
|
||||
onChange={(e) => setAnalyticsChecked(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="cookie-analytics-modal" className="flex-1 cursor-pointer">
|
||||
<p className="font-semibold text-zinc-50 mb-1">Analytics</p>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Anonymous insights that help us refine performance and guest experience throughout the site.
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-black/40 p-4 ring-1 ring-zinc-800/80 backdrop-blur-md">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
id="cookie-marketing-modal"
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-5 w-5 rounded border-zinc-500 bg-black/40 text-[#d4af37] focus:ring-[#d4af37] cursor-pointer"
|
||||
checked={marketingChecked}
|
||||
onChange={(e) => setMarketingChecked(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="cookie-marketing-modal" className="flex-1 cursor-pointer">
|
||||
<p className="font-semibold text-zinc-50 mb-1">Tailored offers</p>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Allow us to present bespoke promotions and experiences aligned with your interests.
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-black/40 p-4 ring-1 ring-zinc-800/80 backdrop-blur-md">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
id="cookie-preferences-modal"
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-5 w-5 rounded border-zinc-500 bg-black/40 text-[#d4af37] focus:ring-[#d4af37] cursor-pointer"
|
||||
checked={preferencesChecked}
|
||||
onChange={(e) => setPreferencesChecked(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="cookie-preferences-modal" className="flex-1 cursor-pointer">
|
||||
<p className="font-semibold text-zinc-50 mb-1">Comfort preferences</p>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Remember your choices such as language, currency, and layout for a smoother return visit.
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t border-zinc-800/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAcceptAll}
|
||||
className="flex-1 inline-flex items-center justify-center rounded-full bg-gradient-to-r from-[#d4af37] via-[#f2cf74] to-[#d4af37] px-6 py-3 text-sm font-semibold uppercase tracking-[0.16em] text-black shadow-lg transition hover:from-[#f8e4a6] hover:via-[#ffe6a3] hover:to-[#f2cf74] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#d4af37]/70"
|
||||
>
|
||||
Accept all
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRejectNonEssential}
|
||||
className="flex-1 inline-flex items-center justify-center rounded-full border border-zinc-600/80 bg-black/40 px-6 py-3 text-sm font-semibold uppercase tracking-[0.16em] text-zinc-100 shadow-lg transition hover:border-zinc-400 hover:bg-black/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500/70"
|
||||
>
|
||||
Essential only
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveSelection}
|
||||
className="flex-1 inline-flex items-center justify-center rounded-full border border-[#d4af37]/60 bg-zinc-900/80 px-6 py-3 text-sm font-semibold uppercase tracking-[0.16em] text-[#d4af37] shadow-lg transition hover:border-[#d4af37] hover:bg-zinc-800/80 hover:text-[#f5e9c6] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#d4af37]/70"
|
||||
>
|
||||
Save selection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookiePreferencesModal;
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useCompanySettings } from '../../contexts/CompanySettingsContext';
|
||||
const Footer: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [enabledPages, setEnabledPages] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
@@ -44,11 +45,41 @@ const Footer: React.FC = () => {
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching footer content:', err);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const checkEnabledPages = async () => {
|
||||
const enabled = new Set<string>();
|
||||
const policyPages = [
|
||||
{ type: 'privacy', url: '/privacy', service: () => pageContentService.getPrivacyContent() },
|
||||
{ type: 'terms', url: '/terms', service: () => pageContentService.getTermsContent() },
|
||||
{ type: 'refunds', url: '/refunds', service: () => pageContentService.getRefundsContent() },
|
||||
{ type: 'cancellation', url: '/cancellation', service: () => pageContentService.getCancellationContent() },
|
||||
{ type: 'accessibility', url: '/accessibility', service: () => pageContentService.getAccessibilityContent() },
|
||||
{ type: 'faq', url: '/faq', service: () => pageContentService.getFAQContent() },
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
policyPages.map(async (page) => {
|
||||
try {
|
||||
const response = await page.service();
|
||||
if (response.status === 'success' && response.data?.page_content?.is_active) {
|
||||
enabled.add(page.url);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// If 404, page is disabled, don't add to enabled set
|
||||
if (err.response?.status !== 404) {
|
||||
console.error(`Error checking ${page.type} page:`, err);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setEnabledPages(enabled);
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
checkEnabledPages();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -97,6 +128,9 @@ const Footer: React.FC = () => {
|
||||
{ label: 'FAQ', url: '/faq' },
|
||||
{ label: 'Terms of Service', url: '/terms' },
|
||||
{ label: 'Privacy Policy', url: '/privacy' },
|
||||
{ label: 'Refunds Policy', url: '/refunds' },
|
||||
{ label: 'Cancellation Policy', url: '/cancellation' },
|
||||
{ label: 'Accessibility', url: '/accessibility' },
|
||||
{ label: 'Contact Us', url: '/contact' }
|
||||
];
|
||||
|
||||
@@ -104,10 +138,18 @@ const Footer: React.FC = () => {
|
||||
? pageContent.footer_links.quick_links
|
||||
: defaultQuickLinks;
|
||||
|
||||
const supportLinks = pageContent?.footer_links?.support_links && pageContent.footer_links.support_links.length > 0
|
||||
const allSupportLinks = pageContent?.footer_links?.support_links && pageContent.footer_links.support_links.length > 0
|
||||
? pageContent.footer_links.support_links
|
||||
: defaultSupportLinks;
|
||||
|
||||
// Filter support links to only show enabled policy pages
|
||||
const supportLinks = allSupportLinks.filter((link) => {
|
||||
// Always show Contact Us
|
||||
if (link.url === '/contact') return true;
|
||||
// Only show policy pages if they are enabled
|
||||
return enabledPages.has(link.url);
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className="relative bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-black text-gray-300 overflow-hidden">
|
||||
{/* Top Gold Accent Line */}
|
||||
@@ -370,9 +412,11 @@ const Footer: React.FC = () => {
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 sm:space-x-6 text-xs sm:text-sm text-gray-600">
|
||||
<span className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Privacy</span>
|
||||
<Link to="/privacy" className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Privacy</Link>
|
||||
<span className="text-gray-700">•</span>
|
||||
<span className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Terms</span>
|
||||
<Link to="/terms" className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Terms</Link>
|
||||
<span className="text-gray-700">•</span>
|
||||
<Link to="/refunds" className="hover:text-[#d4af37] transition-colors cursor-pointer font-light tracking-wide">Refunds</Link>
|
||||
<span className="text-gray-700">•</span>
|
||||
<CookiePreferencesLink />
|
||||
</div>
|
||||
|
||||
178
Frontend/src/pages/AccessibilityPage.tsx
Normal file
178
Frontend/src/pages/AccessibilityPage.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Accessibility, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
|
||||
const AccessibilityPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getAccessibilityContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
if (content.content) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Accessibility className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Accessibility</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<Accessibility className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Accessibility'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For accessibility inquiries, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessibilityPage;
|
||||
|
||||
179
Frontend/src/pages/CancellationPolicyPage.tsx
Normal file
179
Frontend/src/pages/CancellationPolicyPage.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { XCircle, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
|
||||
const CancellationPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getCancellationContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<XCircle className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Cancellation Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<XCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Cancellation Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For questions about cancellations, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancellationPolicyPage;
|
||||
|
||||
178
Frontend/src/pages/FAQPage.tsx
Normal file
178
Frontend/src/pages/FAQPage.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HelpCircle, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
|
||||
const FAQPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getFAQContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
if (content.content) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<HelpCircle className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Frequently Asked Questions</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<HelpCircle className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Frequently Asked Questions'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
Still have questions? Contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FAQPage;
|
||||
|
||||
187
Frontend/src/pages/PrivacyPolicyPage.tsx
Normal file
187
Frontend/src/pages/PrivacyPolicyPage.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Shield, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
|
||||
const PrivacyPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getPrivacyContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update content with processed HTML
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Shield className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Privacy Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<Shield className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Privacy Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For questions about this policy, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicyPage;
|
||||
|
||||
187
Frontend/src/pages/RefundsPolicyPage.tsx
Normal file
187
Frontend/src/pages/RefundsPolicyPage.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RefreshCw, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
|
||||
const RefundsPolicyPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getRefundsContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update content with processed HTML
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<RefreshCw className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Refunds Policy</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<RefreshCw className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Refunds Policy'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For refund inquiries, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RefundsPolicyPage;
|
||||
|
||||
187
Frontend/src/pages/TermsPage.tsx
Normal file
187
Frontend/src/pages/TermsPage.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Scale, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pageContentService } from '../services/api';
|
||||
import type { PageContent } from '../services/api/pageContentService';
|
||||
import { useCompanySettings } from '../contexts/CompanySettingsContext';
|
||||
import Loading from '../components/common/Loading';
|
||||
|
||||
const TermsPage: React.FC = () => {
|
||||
const { settings } = useCompanySettings();
|
||||
const [pageContent, setPageContent] = useState<PageContent | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPageContent = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await pageContentService.getTermsContent();
|
||||
if (response.status === 'success' && response.data?.page_content) {
|
||||
const content = response.data.page_content;
|
||||
|
||||
// Process HTML content to ensure text is visible
|
||||
if (content.content) {
|
||||
// Create a temporary div to parse HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content.content;
|
||||
|
||||
// Add color styles to elements that don't have them
|
||||
const allElements = tempDiv.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const tagName = htmlEl.tagName.toLowerCase();
|
||||
const currentColor = htmlEl.style.color;
|
||||
|
||||
// Only add color if not already set
|
||||
if (!currentColor || currentColor === 'black' || currentColor === '#000' || currentColor === '#000000') {
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
||||
htmlEl.style.color = '#ffffff';
|
||||
} else if (['strong', 'b'].includes(tagName)) {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else if (tagName === 'a') {
|
||||
htmlEl.style.color = '#d4af37';
|
||||
} else {
|
||||
htmlEl.style.color = '#d1d5db';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update content with processed HTML
|
||||
content.content = tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
setPageContent(content);
|
||||
|
||||
if (content.meta_title) {
|
||||
document.title = content.meta_title;
|
||||
}
|
||||
if (content.meta_description) {
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
metaDescription = document.createElement('meta');
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', content.meta_description);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching page content:', err);
|
||||
// If page is disabled (404), set pageContent to null to show disabled message
|
||||
if (err.response?.status === 404) {
|
||||
setPageContent(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPageContent();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!pageContent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6 flex items-center justify-center"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Scale className="w-16 h-16 text-[#d4af37]/50 mx-auto mb-4" />
|
||||
<h1 className="text-2xl font-elegant font-bold text-white mb-2">Terms & Conditions</h1>
|
||||
<p className="text-gray-400">This page is currently unavailable.</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mt-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f] w-screen relative -mt-6 -mb-6"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
paddingTop: '2rem',
|
||||
paddingBottom: '2rem',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<div className="w-full px-3 sm:px-4 md:px-6 lg:px-8 py-8 sm:py-12 max-w-5xl mx-auto">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-2 text-[#d4af37]/80 hover:text-[#d4af37] mb-6 transition-all duration-300 group font-light tracking-wide text-xs sm:text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||
<span>Back to Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8 sm:mb-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 mb-4 sm:mb-6 bg-gradient-to-br from-[#d4af37]/20 to-[#c9a227]/10 rounded-full border border-[#d4af37]/30">
|
||||
<Scale className="w-8 h-8 sm:w-10 sm:h-10 text-[#d4af37]" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-elegant font-bold text-white mb-3 sm:mb-4 tracking-tight leading-tight bg-gradient-to-r from-white via-[#d4af37] to-white bg-clip-text text-transparent">
|
||||
{pageContent.title || 'Terms & Conditions'}
|
||||
</h1>
|
||||
{pageContent.subtitle && (
|
||||
<p className="text-base sm:text-lg text-gray-300 font-light tracking-wide">
|
||||
{pageContent.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-gradient-to-br from-[#1a1a1a] to-[#0a0a0a] rounded-xl border border-[#d4af37]/20 backdrop-blur-xl shadow-2xl shadow-[#d4af37]/5 p-6 sm:p-8 lg:p-12">
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-none text-gray-300
|
||||
prose-headings:text-white prose-headings:font-elegant prose-headings:font-semibold
|
||||
prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 prose-h2:border-b prose-h2:border-[#d4af37]/20 prose-h2:pb-2
|
||||
prose-p:text-gray-300 prose-p:font-light prose-p:leading-relaxed prose-p:mb-4
|
||||
prose-ul:text-gray-300 prose-ul:font-light prose-ul:my-4
|
||||
prose-li:text-gray-300 prose-li:mb-2 prose-li:ml-4
|
||||
prose-strong:text-[#d4af37] prose-strong:font-medium
|
||||
prose-a:text-[#d4af37] prose-a:no-underline hover:prose-a:underline
|
||||
[&_*]:text-gray-300 [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_h5]:text-white [&_h6]:text-white
|
||||
[&_strong]:text-[#d4af37] [&_b]:text-[#d4af37] [&_a]:text-[#d4af37]"
|
||||
style={{ color: '#d1d5db' }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: pageContent.content || pageContent.description || '<p style="color: #d1d5db;">No content available.</p>'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer Note */}
|
||||
{settings.company_email && (
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-gray-400 font-light">
|
||||
For questions about these terms, contact us at{' '}
|
||||
<a href={`mailto:${settings.company_email}`} className="text-[#d4af37] hover:underline">
|
||||
{settings.company_email}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsPage;
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
Check,
|
||||
XCircle,
|
||||
Award,
|
||||
Shield
|
||||
Shield,
|
||||
RefreshCw,
|
||||
Accessibility,
|
||||
HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { pageContentService, PageContent, PageType, UpdatePageContentData, bannerService, Banner } from '../../services/api';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -26,7 +29,7 @@ import Loading from '../../components/common/Loading';
|
||||
import { ConfirmationDialog } from '../../components/common';
|
||||
import IconPicker from '../../components/admin/IconPicker';
|
||||
|
||||
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo';
|
||||
type ContentTab = 'overview' | 'home' | 'contact' | 'about' | 'footer' | 'seo' | 'privacy' | 'terms' | 'refunds' | 'cancellation' | 'accessibility' | 'faq';
|
||||
|
||||
const PageContentDashboard: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<ContentTab>('overview');
|
||||
@@ -38,6 +41,12 @@ const PageContentDashboard: React.FC = () => {
|
||||
about: null,
|
||||
footer: null,
|
||||
seo: null,
|
||||
privacy: null,
|
||||
terms: null,
|
||||
refunds: null,
|
||||
cancellation: null,
|
||||
accessibility: null,
|
||||
faq: null,
|
||||
});
|
||||
|
||||
// Form states for each page
|
||||
@@ -46,6 +55,12 @@ const PageContentDashboard: React.FC = () => {
|
||||
const [aboutData, setAboutData] = useState<UpdatePageContentData>({});
|
||||
const [footerData, setFooterData] = useState<UpdatePageContentData>({});
|
||||
const [seoData, setSeoData] = useState<UpdatePageContentData>({});
|
||||
const [privacyData, setPrivacyData] = useState<UpdatePageContentData>({ is_active: true });
|
||||
const [termsData, setTermsData] = useState<UpdatePageContentData>({ is_active: true });
|
||||
const [refundsData, setRefundsData] = useState<UpdatePageContentData>({ is_active: true });
|
||||
const [cancellationData, setCancellationData] = useState<UpdatePageContentData>({ is_active: true });
|
||||
const [accessibilityData, setAccessibilityData] = useState<UpdatePageContentData>({ is_active: true });
|
||||
const [faqData, setFaqData] = useState<UpdatePageContentData>({ is_active: true });
|
||||
|
||||
// Banner management state
|
||||
const [banners, setBanners] = useState<Banner[]>([]);
|
||||
@@ -88,6 +103,12 @@ const PageContentDashboard: React.FC = () => {
|
||||
about: null,
|
||||
footer: null,
|
||||
seo: null,
|
||||
privacy: null,
|
||||
terms: null,
|
||||
refunds: null,
|
||||
cancellation: null,
|
||||
accessibility: null,
|
||||
faq: null,
|
||||
};
|
||||
|
||||
contents.forEach((content) => {
|
||||
@@ -241,6 +262,84 @@ const PageContentDashboard: React.FC = () => {
|
||||
canonical_url: contents.seo.canonical_url || '',
|
||||
});
|
||||
}
|
||||
|
||||
// Privacy
|
||||
if (contents.privacy) {
|
||||
setPrivacyData({
|
||||
title: contents.privacy.title || '',
|
||||
subtitle: contents.privacy.subtitle || '',
|
||||
description: contents.privacy.description || '',
|
||||
content: contents.privacy.content || '',
|
||||
meta_title: contents.privacy.meta_title || '',
|
||||
meta_description: contents.privacy.meta_description || '',
|
||||
is_active: contents.privacy.is_active ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// Terms
|
||||
if (contents.terms) {
|
||||
setTermsData({
|
||||
title: contents.terms.title || '',
|
||||
subtitle: contents.terms.subtitle || '',
|
||||
description: contents.terms.description || '',
|
||||
content: contents.terms.content || '',
|
||||
meta_title: contents.terms.meta_title || '',
|
||||
meta_description: contents.terms.meta_description || '',
|
||||
is_active: contents.terms.is_active ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// Refunds
|
||||
if (contents.refunds) {
|
||||
setRefundsData({
|
||||
title: contents.refunds.title || '',
|
||||
subtitle: contents.refunds.subtitle || '',
|
||||
description: contents.refunds.description || '',
|
||||
content: contents.refunds.content || '',
|
||||
meta_title: contents.refunds.meta_title || '',
|
||||
meta_description: contents.refunds.meta_description || '',
|
||||
is_active: contents.refunds.is_active ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// Cancellation
|
||||
if (contents.cancellation) {
|
||||
setCancellationData({
|
||||
title: contents.cancellation.title || '',
|
||||
subtitle: contents.cancellation.subtitle || '',
|
||||
description: contents.cancellation.description || '',
|
||||
content: contents.cancellation.content || '',
|
||||
meta_title: contents.cancellation.meta_title || '',
|
||||
meta_description: contents.cancellation.meta_description || '',
|
||||
is_active: contents.cancellation.is_active ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
if (contents.accessibility) {
|
||||
setAccessibilityData({
|
||||
title: contents.accessibility.title || '',
|
||||
subtitle: contents.accessibility.subtitle || '',
|
||||
description: contents.accessibility.description || '',
|
||||
content: contents.accessibility.content || '',
|
||||
meta_title: contents.accessibility.meta_title || '',
|
||||
meta_description: contents.accessibility.meta_description || '',
|
||||
is_active: contents.accessibility.is_active ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// FAQ
|
||||
if (contents.faq) {
|
||||
setFaqData({
|
||||
title: contents.faq.title || '',
|
||||
subtitle: contents.faq.subtitle || '',
|
||||
description: contents.faq.description || '',
|
||||
content: contents.faq.content || '',
|
||||
meta_title: contents.faq.meta_title || '',
|
||||
meta_description: contents.faq.meta_description || '',
|
||||
is_active: contents.faq.is_active ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (pageType: PageType, data: UpdatePageContentData) => {
|
||||
@@ -449,6 +548,12 @@ const PageContentDashboard: React.FC = () => {
|
||||
{ id: 'home' as ContentTab, label: 'Home', icon: Home },
|
||||
{ id: 'contact' as ContentTab, label: 'Contact', icon: Mail },
|
||||
{ id: 'about' as ContentTab, label: 'About', icon: Info },
|
||||
{ id: 'privacy' as ContentTab, label: 'Privacy', icon: Shield },
|
||||
{ id: 'terms' as ContentTab, label: 'Terms', icon: FileText },
|
||||
{ id: 'refunds' as ContentTab, label: 'Refunds', icon: RefreshCw },
|
||||
{ id: 'cancellation' as ContentTab, label: 'Cancellation', icon: XCircle },
|
||||
{ id: 'accessibility' as ContentTab, label: 'Accessibility', icon: Accessibility },
|
||||
{ id: 'faq' as ContentTab, label: 'FAQ', icon: HelpCircle },
|
||||
{ id: 'footer' as ContentTab, label: 'Footer', icon: FileText },
|
||||
{ id: 'seo' as ContentTab, label: 'SEO', icon: Search },
|
||||
];
|
||||
@@ -528,6 +633,12 @@ const PageContentDashboard: React.FC = () => {
|
||||
{ id: 'home' as PageType, label: 'Home Page', icon: Home, color: 'blue', description: 'Manage hero section, featured content' },
|
||||
{ id: 'contact' as PageType, label: 'Contact Page', icon: Mail, color: 'green', description: 'Manage contact information and form' },
|
||||
{ id: 'about' as PageType, label: 'About Page', icon: Info, color: 'amber', description: 'Manage story, values, and features' },
|
||||
{ id: 'privacy' as PageType, label: 'Privacy Policy', icon: Shield, color: 'red', description: 'Manage privacy policy content' },
|
||||
{ id: 'terms' as PageType, label: 'Terms & Conditions', icon: FileText, color: 'teal', description: 'Manage terms and conditions' },
|
||||
{ id: 'refunds' as PageType, label: 'Refunds Policy', icon: RefreshCw, color: 'orange', description: 'Manage refunds policy content' },
|
||||
{ id: 'cancellation' as PageType, label: 'Cancellation Policy', icon: XCircle, color: 'pink', description: 'Manage cancellation policy content' },
|
||||
{ id: 'accessibility' as PageType, label: 'Accessibility', icon: Accessibility, color: 'cyan', description: 'Manage accessibility information' },
|
||||
{ id: 'faq' as PageType, label: 'FAQ', icon: HelpCircle, color: 'violet', description: 'Manage frequently asked questions' },
|
||||
{ id: 'footer' as PageType, label: 'Footer', icon: FileText, color: 'purple', description: 'Manage footer links and social media' },
|
||||
{ id: 'seo' as PageType, label: 'SEO Settings', icon: Search, color: 'indigo', description: 'Manage meta tags and SEO optimization' },
|
||||
].map((page) => {
|
||||
@@ -542,6 +653,9 @@ const PageContentDashboard: React.FC = () => {
|
||||
page.color === 'green' ? 'border-green-100/50 hover:border-green-300/60' :
|
||||
page.color === 'amber' ? 'border-amber-100/50 hover:border-amber-300/60' :
|
||||
page.color === 'purple' ? 'border-purple-100/50 hover:border-purple-300/60' :
|
||||
page.color === 'red' ? 'border-red-100/50 hover:border-red-300/60' :
|
||||
page.color === 'teal' ? 'border-teal-100/50 hover:border-teal-300/60' :
|
||||
page.color === 'orange' ? 'border-orange-100/50 hover:border-orange-300/60' :
|
||||
'border-indigo-100/50 hover:border-indigo-300/60'
|
||||
}`}
|
||||
>
|
||||
@@ -550,6 +664,9 @@ const PageContentDashboard: React.FC = () => {
|
||||
page.color === 'green' ? 'from-green-400' :
|
||||
page.color === 'amber' ? 'from-amber-400' :
|
||||
page.color === 'purple' ? 'from-purple-400' :
|
||||
page.color === 'red' ? 'from-red-400' :
|
||||
page.color === 'teal' ? 'from-teal-400' :
|
||||
page.color === 'orange' ? 'from-orange-400' :
|
||||
'from-indigo-400'
|
||||
}`}></div>
|
||||
<div className="relative space-y-5">
|
||||
@@ -560,6 +677,9 @@ const PageContentDashboard: React.FC = () => {
|
||||
page.color === 'green' ? 'bg-gradient-to-br from-green-500 to-green-600 border-green-400/50' :
|
||||
page.color === 'amber' ? 'bg-gradient-to-br from-amber-500 to-amber-600 border-amber-400/50' :
|
||||
page.color === 'purple' ? 'bg-gradient-to-br from-purple-500 to-purple-600 border-purple-400/50' :
|
||||
page.color === 'red' ? 'bg-gradient-to-br from-red-500 to-red-600 border-red-400/50' :
|
||||
page.color === 'teal' ? 'bg-gradient-to-br from-teal-500 to-teal-600 border-teal-400/50' :
|
||||
page.color === 'orange' ? 'bg-gradient-to-br from-orange-500 to-orange-600 border-orange-400/50' :
|
||||
'bg-gradient-to-br from-indigo-500 to-indigo-600 border-indigo-400/50'
|
||||
}`}>
|
||||
<Icon className="w-6 h-6 text-white" />
|
||||
@@ -568,6 +688,9 @@ const PageContentDashboard: React.FC = () => {
|
||||
<h3 className="font-bold text-gray-900 text-xl mb-1">{page.label}</h3>
|
||||
<div className={`h-1 w-12 rounded-full ${
|
||||
page.color === 'blue' ? 'bg-gradient-to-r from-blue-500 to-blue-600' :
|
||||
page.color === 'red' ? 'bg-gradient-to-r from-red-500 to-red-600' :
|
||||
page.color === 'teal' ? 'bg-gradient-to-r from-teal-500 to-teal-600' :
|
||||
page.color === 'orange' ? 'bg-gradient-to-r from-orange-500 to-orange-600' :
|
||||
page.color === 'green' ? 'bg-gradient-to-r from-green-500 to-green-600' :
|
||||
page.color === 'amber' ? 'bg-gradient-to-r from-amber-500 to-amber-600' :
|
||||
page.color === 'purple' ? 'bg-gradient-to-r from-purple-500 to-purple-600' :
|
||||
@@ -3607,6 +3730,570 @@ const PageContentDashboard: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Privacy Tab */}
|
||||
{activeTab === 'privacy' && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Privacy Policy Content</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPrivacyData({ ...privacyData, is_active: !privacyData.is_active })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||
(privacyData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
(privacyData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={privacyData.title || ''}
|
||||
onChange={(e) => setPrivacyData({ ...privacyData, title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Privacy Policy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={privacyData.subtitle || ''}
|
||||
onChange={(e) => setPrivacyData({ ...privacyData, subtitle: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Your privacy is important to us"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
||||
<textarea
|
||||
value={privacyData.content || ''}
|
||||
onChange={(e) => setPrivacyData({ ...privacyData, content: e.target.value })}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 font-mono text-sm"
|
||||
placeholder="Enter HTML content for privacy policy..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">SEO Settings</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={privacyData.meta_title || ''}
|
||||
onChange={(e) => setPrivacyData({ ...privacyData, meta_title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
||||
<textarea
|
||||
value={privacyData.meta_description || ''}
|
||||
onChange={(e) => setPrivacyData({ ...privacyData, meta_description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={() => handleSave('privacy', privacyData)}
|
||||
disabled={saving}
|
||||
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
{saving ? 'Saving...' : 'Save Privacy Policy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terms Tab */}
|
||||
{activeTab === 'terms' && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">Terms & Conditions Content</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={termsData.title || ''}
|
||||
onChange={(e) => setTermsData({ ...termsData, title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Terms & Conditions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={termsData.subtitle || ''}
|
||||
onChange={(e) => setTermsData({ ...termsData, subtitle: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Please read these terms carefully"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
||||
<textarea
|
||||
value={termsData.content || ''}
|
||||
onChange={(e) => setTermsData({ ...termsData, content: e.target.value })}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 font-mono text-sm"
|
||||
placeholder="Enter HTML content for terms & conditions..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">SEO Settings</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={termsData.meta_title || ''}
|
||||
onChange={(e) => setTermsData({ ...termsData, meta_title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
||||
<textarea
|
||||
value={termsData.meta_description || ''}
|
||||
onChange={(e) => setTermsData({ ...termsData, meta_description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={() => handleSave('terms', termsData)}
|
||||
disabled={saving}
|
||||
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
{saving ? 'Saving...' : 'Save Terms & Conditions'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Refunds Tab */}
|
||||
{activeTab === 'refunds' && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Refunds Policy Content</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRefundsData({ ...refundsData, is_active: !refundsData.is_active })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||
(refundsData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
(refundsData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={refundsData.title || ''}
|
||||
onChange={(e) => setRefundsData({ ...refundsData, title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Refunds Policy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={refundsData.subtitle || ''}
|
||||
onChange={(e) => setRefundsData({ ...refundsData, subtitle: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Our commitment to fair refunds"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
||||
<textarea
|
||||
value={refundsData.content || ''}
|
||||
onChange={(e) => setRefundsData({ ...refundsData, content: e.target.value })}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 font-mono text-sm"
|
||||
placeholder="Enter HTML content for refunds policy..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">SEO Settings</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={refundsData.meta_title || ''}
|
||||
onChange={(e) => setRefundsData({ ...refundsData, meta_title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
||||
<textarea
|
||||
value={refundsData.meta_description || ''}
|
||||
onChange={(e) => setRefundsData({ ...refundsData, meta_description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={() => handleSave('refunds', refundsData)}
|
||||
disabled={saving}
|
||||
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
{saving ? 'Saving...' : 'Save Refunds Policy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cancellation Tab */}
|
||||
{activeTab === 'cancellation' && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Cancellation Policy Content</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCancellationData({ ...cancellationData, is_active: !cancellationData.is_active })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||
(cancellationData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
(cancellationData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cancellationData.title || ''}
|
||||
onChange={(e) => setCancellationData({ ...cancellationData, title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Cancellation Policy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cancellationData.subtitle || ''}
|
||||
onChange={(e) => setCancellationData({ ...cancellationData, subtitle: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Flexible cancellation options for your peace of mind"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
||||
<textarea
|
||||
value={cancellationData.content || ''}
|
||||
onChange={(e) => setCancellationData({ ...cancellationData, content: e.target.value })}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 font-mono text-sm"
|
||||
placeholder="Enter HTML content for cancellation policy..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={cancellationData.meta_title || ''}
|
||||
onChange={(e) => setCancellationData({ ...cancellationData, meta_title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Cancellation Policy - Luxury Hotel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
||||
<textarea
|
||||
value={cancellationData.meta_description || ''}
|
||||
onChange={(e) => setCancellationData({ ...cancellationData, meta_description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Review our cancellation policy..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSave('cancellation', cancellationData)}
|
||||
disabled={saving}
|
||||
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
{saving ? 'Saving...' : 'Save Cancellation Policy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Accessibility Tab */}
|
||||
{activeTab === 'accessibility' && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">Accessibility Content</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAccessibilityData({ ...accessibilityData, is_active: !accessibilityData.is_active })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||
(accessibilityData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
(accessibilityData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={accessibilityData.title || ''}
|
||||
onChange={(e) => setAccessibilityData({ ...accessibilityData, title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Accessibility"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={accessibilityData.subtitle || ''}
|
||||
onChange={(e) => setAccessibilityData({ ...accessibilityData, subtitle: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Committed to providing an inclusive experience for all guests"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
||||
<textarea
|
||||
value={accessibilityData.content || ''}
|
||||
onChange={(e) => setAccessibilityData({ ...accessibilityData, content: e.target.value })}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 font-mono text-sm"
|
||||
placeholder="Enter HTML content for accessibility page..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={accessibilityData.meta_title || ''}
|
||||
onChange={(e) => setAccessibilityData({ ...accessibilityData, meta_title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Accessibility - Luxury Hotel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
||||
<textarea
|
||||
value={accessibilityData.meta_description || ''}
|
||||
onChange={(e) => setAccessibilityData({ ...accessibilityData, meta_description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Discover our commitment to accessibility..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSave('accessibility', accessibilityData)}
|
||||
disabled={saving}
|
||||
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
{saving ? 'Saving...' : 'Save Accessibility Content'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* FAQ Tab */}
|
||||
{activeTab === 'faq' && (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900">FAQ Content</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Enable Page</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFaqData({ ...faqData, is_active: !faqData.is_active })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||
(faqData.is_active ?? true) ? 'bg-purple-600' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
(faqData.is_active ?? true) ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={faqData.title || ''}
|
||||
onChange={(e) => setFaqData({ ...faqData, title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Frequently Asked Questions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={faqData.subtitle || ''}
|
||||
onChange={(e) => setFaqData({ ...faqData, subtitle: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Find answers to common questions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Content (HTML)</label>
|
||||
<textarea
|
||||
value={faqData.content || ''}
|
||||
onChange={(e) => setFaqData({ ...faqData, content: e.target.value })}
|
||||
rows={20}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200 font-mono text-sm"
|
||||
placeholder="Enter HTML content for FAQ page..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={faqData.meta_title || ''}
|
||||
onChange={(e) => setFaqData({ ...faqData, meta_title: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="FAQ - Luxury Hotel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">Meta Description</label>
|
||||
<textarea
|
||||
value={faqData.meta_description || ''}
|
||||
onChange={(e) => setFaqData({ ...faqData, meta_description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-4 py-3 bg-white border-2 border-gray-200 rounded-xl focus:border-purple-400 focus:ring-4 focus:ring-purple-100 transition-all duration-200"
|
||||
placeholder="Find answers to common questions..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => handleSave('faq', faqData)}
|
||||
disabled={saving}
|
||||
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-purple-600 text-white rounded-xl font-semibold hover:from-purple-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
{saving ? 'Saving...' : 'Save FAQ Content'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SEO Tab */}
|
||||
{activeTab === 'seo' && (
|
||||
<div className="space-y-8">
|
||||
|
||||
@@ -1787,12 +1787,12 @@ const BookingPage: React.FC = () => {
|
||||
}`}
|
||||
>
|
||||
{paymentMethod === 'cash' ? (
|
||||
<div className="flex items-start gap-2">
|
||||
<Shield className="w-3.5 h-3.5 text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-[10px] sm:text-xs text-orange-300 font-light tracking-wide leading-relaxed">
|
||||
<div className="flex items-start gap-2">
|
||||
<Shield className="w-3.5 h-3.5 text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-[10px] sm:text-xs text-orange-300 font-light tracking-wide leading-relaxed">
|
||||
<strong className="text-orange-200">20% deposit required</strong> to secure your booking. Pay the remaining balance ({formatPrice(totalPrice * 0.8)}) on arrival at the hotel.
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles className="w-3.5 h-3.5 text-[#d4af37] mt-0.5 flex-shrink-0" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import apiClient from './apiClient';
|
||||
|
||||
export type PageType = 'home' | 'contact' | 'about' | 'footer' | 'seo';
|
||||
export type PageType = 'home' | 'contact' | 'about' | 'footer' | 'seo' | 'privacy' | 'terms' | 'refunds' | 'cancellation' | 'accessibility' | 'faq';
|
||||
|
||||
export interface PageContent {
|
||||
id?: number;
|
||||
@@ -228,6 +228,36 @@ const pageContentService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPrivacyContent: async (): Promise<PageContentResponse> => {
|
||||
const response = await apiClient.get<PageContentResponse>('/privacy');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTermsContent: async (): Promise<PageContentResponse> => {
|
||||
const response = await apiClient.get<PageContentResponse>('/terms');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRefundsContent: async (): Promise<PageContentResponse> => {
|
||||
const response = await apiClient.get<PageContentResponse>('/refunds');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getCancellationContent: async (): Promise<PageContentResponse> => {
|
||||
const response = await apiClient.get<PageContentResponse>('/cancellation');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getAccessibilityContent: async (): Promise<PageContentResponse> => {
|
||||
const response = await apiClient.get<PageContentResponse>('/accessibility');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getFAQContent: async (): Promise<PageContentResponse> => {
|
||||
const response = await apiClient.get<PageContentResponse>('/faq');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
updatePageContent: async (
|
||||
pageType: PageType,
|
||||
|
||||
@@ -543,3 +543,37 @@ img[loading="lazy"].loaded,
|
||||
img[loading="lazy"]:not([src]) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Policy pages content styling - ensure all text is visible */
|
||||
.prose.prose-invert {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.prose.prose-invert * {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.prose.prose-invert p,
|
||||
.prose.prose-invert li,
|
||||
.prose.prose-invert span,
|
||||
.prose.prose-invert div {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.prose.prose-invert h1,
|
||||
.prose.prose-invert h2,
|
||||
.prose.prose-invert h3,
|
||||
.prose.prose-invert h4,
|
||||
.prose.prose-invert h5,
|
||||
.prose.prose-invert h6 {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.prose.prose-invert strong,
|
||||
.prose.prose-invert b {
|
||||
color: #d4af37 !important;
|
||||
}
|
||||
|
||||
.prose.prose-invert a {
|
||||
color: #d4af37 !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user