From 7a90e6330160b15fc4f1053b14a0212c6312165e Mon Sep 17 00:00:00 2001
From: Krikorios <99836218+Krikorios@users.noreply.github.com>
Date: Sun, 22 Feb 2026 02:24:15 +0200
Subject: [PATCH] Initial commit: MSPE website - full site with admin panel,
API, and public pages
---
.env.example | 42 +
.gitignore | 10 +
.htaccess | 85 +
Public HTML/.env.example | 47 +
Public HTML/.htaccess | 85 +
Public HTML/404.html | 104 +
Public HTML/about.html | 492 +
Public HTML/admin/availability.html | 930 +
Public HTML/admin/bookings.html | 732 +
Public HTML/admin/css/admin.css | 3334 +
Public HTML/admin/dashboard.html | 429 +
Public HTML/admin/index.html | 141 +
Public HTML/admin/js/admin.js | 440 +
Public HTML/admin/media.html | 731 +
Public HTML/admin/messages.html | 741 +
Public HTML/admin/news.html | 708 +
Public HTML/admin/pages.html | 512 +
Public HTML/admin/portfolio.html | 708 +
Public HTML/admin/reset-password.html | 147 +
Public HTML/admin/services.html | 411 +
Public HTML/admin/settings.html | 910 +
Public HTML/admin/subscribers.html | 531 +
Public HTML/admin/team.html | 508 +
Public HTML/admin/testimonials.html | 546 +
Public HTML/admin/users.html | 603 +
Public HTML/api/.htaccess | 10 +
Public HTML/api/auth.php | 391 +
Public HTML/api/bookings.php | 605 +
Public HTML/api/config.php | 773 +
Public HTML/api/contact.php | 287 +
Public HTML/api/media.php | 168 +
Public HTML/api/news.php | 273 +
Public HTML/api/pages.php | 405 +
Public HTML/api/portfolio.php | 264 +
Public HTML/api/public-settings.php | 46 +
Public HTML/api/services.php | 125 +
Public HTML/api/settings.php | 102 +
Public HTML/api/stats.php | 224 +
Public HTML/api/subscribe.php | 123 +
Public HTML/api/team.php | 140 +
Public HTML/api/testimonials.php | 141 +
Public HTML/api/users.php | 183 +
Public HTML/calendar.html | 902 +
Public HTML/composer.json | 5 +
Public HTML/composer.lock | 82 +
Public HTML/contact.html | 553 +
Public HTML/css/header-fix.css | 50 +
Public HTML/css/pages.css | 2367 +
Public HTML/css/style.css | 2608 +
Public HTML/css/ultimate-ui.css | 1753 +
Public HTML/data/.htaccess | 8 +
Public HTML/data/availability_slots.json | 1562 +
Public HTML/data/blocked_times.json | 1 +
Public HTML/data/bookings.json | 1 +
...tats_88c98f5c6e831e4719680337e91401b6.json | 1 +
Public HTML/data/messages.json | 1 +
Public HTML/data/news.json | 50 +
Public HTML/data/portfolio.json | 1 +
Public HTML/data/services.json | 54 +
Public HTML/data/settings.json | 37 +
Public HTML/data/subscribers.json | 1 +
Public HTML/images/about/client-success.svg | 146354 +++++++++++++
Public HTML/images/about/data-center.svg | 160653 +++++++++++++++
Public HTML/images/about/team-collab.svg | 133873 ++++++++++++
Public HTML/images/logo/logo.png | Bin 0 -> 581396 bytes
Public HTML/images/team/member-1.svg | 16 +
Public HTML/images/team/member-2.svg | 16 +
Public HTML/images/team/member-3.svg | 16 +
Public HTML/images/team/member-4.svg | 16 +
Public HTML/index.html | 663 +
Public HTML/js/main.js | 1110 +
Public HTML/js/ultimate-ui.js | 472 +
Public HTML/news.html | 440 +
Public HTML/portfolio.html | 561 +
Public HTML/privacy.html | 158 +
Public HTML/robots.txt | 11 +
Public HTML/services.html | 378 +
Public HTML/sitemap.xml | 57 +
Public HTML/terms.html | 164 +
Public HTML/ui-showcase.html | 397 +
Public HTML/uploads/.htaccess | 13 +
Public HTML/vendor/autoload.php | 22 +
Public HTML/vendor/composer/ClassLoader.php | 579 +
.../vendor/composer/InstalledVersions.php | 396 +
Public HTML/vendor/composer/LICENSE | 21 +
.../vendor/composer/autoload_classmap.php | 10 +
.../vendor/composer/autoload_namespaces.php | 9 +
Public HTML/vendor/composer/autoload_psr4.php | 10 +
Public HTML/vendor/composer/autoload_real.php | 38 +
.../vendor/composer/autoload_static.php | 36 +
Public HTML/vendor/composer/installed.json | 72 +
Public HTML/vendor/composer/installed.php | 32 +
.../vendor/composer/platform_check.php | 25 +
.../vendor/firebase/php-jwt/CHANGELOG.md | 229 +
Public HTML/vendor/firebase/php-jwt/LICENSE | 30 +
Public HTML/vendor/firebase/php-jwt/README.md | 425 +
.../vendor/firebase/php-jwt/composer.json | 42 +
.../php-jwt/src/BeforeValidException.php | 18 +
.../firebase/php-jwt/src/CachedKeySet.php | 274 +
.../firebase/php-jwt/src/ExpiredException.php | 30 +
.../vendor/firebase/php-jwt/src/JWK.php | 355 +
.../vendor/firebase/php-jwt/src/JWT.php | 748 +
.../src/JWTExceptionWithPayloadInterface.php | 20 +
.../vendor/firebase/php-jwt/src/Key.php | 54 +
.../php-jwt/src/SignatureInvalidException.php | 7 +
uploads/.htaccess | 13 +
106 files changed, 477087 insertions(+)
create mode 100644 .env.example
create mode 100644 .gitignore
create mode 100644 .htaccess
create mode 100644 Public HTML/.env.example
create mode 100644 Public HTML/.htaccess
create mode 100755 Public HTML/404.html
create mode 100755 Public HTML/about.html
create mode 100755 Public HTML/admin/availability.html
create mode 100755 Public HTML/admin/bookings.html
create mode 100755 Public HTML/admin/css/admin.css
create mode 100755 Public HTML/admin/dashboard.html
create mode 100755 Public HTML/admin/index.html
create mode 100755 Public HTML/admin/js/admin.js
create mode 100755 Public HTML/admin/media.html
create mode 100755 Public HTML/admin/messages.html
create mode 100755 Public HTML/admin/news.html
create mode 100755 Public HTML/admin/pages.html
create mode 100755 Public HTML/admin/portfolio.html
create mode 100755 Public HTML/admin/reset-password.html
create mode 100755 Public HTML/admin/services.html
create mode 100755 Public HTML/admin/settings.html
create mode 100755 Public HTML/admin/subscribers.html
create mode 100755 Public HTML/admin/team.html
create mode 100755 Public HTML/admin/testimonials.html
create mode 100755 Public HTML/admin/users.html
create mode 100755 Public HTML/api/.htaccess
create mode 100755 Public HTML/api/auth.php
create mode 100755 Public HTML/api/bookings.php
create mode 100755 Public HTML/api/config.php
create mode 100755 Public HTML/api/contact.php
create mode 100755 Public HTML/api/media.php
create mode 100755 Public HTML/api/news.php
create mode 100755 Public HTML/api/pages.php
create mode 100755 Public HTML/api/portfolio.php
create mode 100755 Public HTML/api/public-settings.php
create mode 100755 Public HTML/api/services.php
create mode 100755 Public HTML/api/settings.php
create mode 100755 Public HTML/api/stats.php
create mode 100755 Public HTML/api/subscribe.php
create mode 100755 Public HTML/api/team.php
create mode 100755 Public HTML/api/testimonials.php
create mode 100755 Public HTML/api/users.php
create mode 100755 Public HTML/calendar.html
create mode 100644 Public HTML/composer.json
create mode 100644 Public HTML/composer.lock
create mode 100755 Public HTML/contact.html
create mode 100755 Public HTML/css/header-fix.css
create mode 100755 Public HTML/css/pages.css
create mode 100755 Public HTML/css/style.css
create mode 100755 Public HTML/css/ultimate-ui.css
create mode 100755 Public HTML/data/.htaccess
create mode 100755 Public HTML/data/availability_slots.json
create mode 100755 Public HTML/data/blocked_times.json
create mode 100755 Public HTML/data/bookings.json
create mode 100644 Public HTML/data/cache/stats_88c98f5c6e831e4719680337e91401b6.json
create mode 100755 Public HTML/data/messages.json
create mode 100755 Public HTML/data/news.json
create mode 100755 Public HTML/data/portfolio.json
create mode 100755 Public HTML/data/services.json
create mode 100755 Public HTML/data/settings.json
create mode 100755 Public HTML/data/subscribers.json
create mode 100644 Public HTML/images/about/client-success.svg
create mode 100644 Public HTML/images/about/data-center.svg
create mode 100644 Public HTML/images/about/team-collab.svg
create mode 100644 Public HTML/images/logo/logo.png
create mode 100755 Public HTML/images/team/member-1.svg
create mode 100755 Public HTML/images/team/member-2.svg
create mode 100755 Public HTML/images/team/member-3.svg
create mode 100755 Public HTML/images/team/member-4.svg
create mode 100755 Public HTML/index.html
create mode 100755 Public HTML/js/main.js
create mode 100755 Public HTML/js/ultimate-ui.js
create mode 100755 Public HTML/news.html
create mode 100755 Public HTML/portfolio.html
create mode 100755 Public HTML/privacy.html
create mode 100755 Public HTML/robots.txt
create mode 100755 Public HTML/services.html
create mode 100755 Public HTML/sitemap.xml
create mode 100755 Public HTML/terms.html
create mode 100755 Public HTML/ui-showcase.html
create mode 100644 Public HTML/uploads/.htaccess
create mode 100644 Public HTML/vendor/autoload.php
create mode 100644 Public HTML/vendor/composer/ClassLoader.php
create mode 100644 Public HTML/vendor/composer/InstalledVersions.php
create mode 100644 Public HTML/vendor/composer/LICENSE
create mode 100644 Public HTML/vendor/composer/autoload_classmap.php
create mode 100644 Public HTML/vendor/composer/autoload_namespaces.php
create mode 100644 Public HTML/vendor/composer/autoload_psr4.php
create mode 100644 Public HTML/vendor/composer/autoload_real.php
create mode 100644 Public HTML/vendor/composer/autoload_static.php
create mode 100644 Public HTML/vendor/composer/installed.json
create mode 100644 Public HTML/vendor/composer/installed.php
create mode 100644 Public HTML/vendor/composer/platform_check.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/CHANGELOG.md
create mode 100644 Public HTML/vendor/firebase/php-jwt/LICENSE
create mode 100644 Public HTML/vendor/firebase/php-jwt/README.md
create mode 100644 Public HTML/vendor/firebase/php-jwt/composer.json
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/BeforeValidException.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/CachedKeySet.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/ExpiredException.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/JWK.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/JWT.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/Key.php
create mode 100644 Public HTML/vendor/firebase/php-jwt/src/SignatureInvalidException.php
create mode 100644 uploads/.htaccess
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..49b547f
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,42 @@
+# ═══════════════════════════════════════════════════════════════
+# MSPE Website - Environment Configuration
+# ═══════════════════════════════════════════════════════════════
+# Copy this file to .env and fill in your production values
+# cp .env.example .env
+#
+# IMPORTANT: Never commit .env to version control!
+# ═══════════════════════════════════════════════════════════════
+
+# ── Application ───────────────────────────────────────────────
+APP_ENV=production
+SITE_NAME=MSPE
+SITE_URL=https://mspe.pro
+
+# ── Database ──────────────────────────────────────────────────
+# Use 'file' for JSON file storage, or 'mysql' for MySQL database
+DB_TYPE=file
+# DB_HOST=localhost
+# DB_NAME=your_database_name
+# DB_USER=your_database_user
+# DB_PASS=your_database_password
+
+# ── Admin Credentials ─────────────────────────────────────────
+# IMPORTANT: Set a strong admin password before deploying!
+ADMIN_USER=admin
+ADMIN_PASS=CHANGE_ME_TO_A_STRONG_PASSWORD
+
+# ── JWT Secret ────────────────────────────────────────────────
+# Generate a random secret: php -r "echo bin2hex(random_bytes(32));"
+JWT_SECRET=CHANGE_ME_GENERATE_RANDOM_SECRET
+
+# ── Admin Email ───────────────────────────────────────────────
+ADMIN_EMAIL=info@mspe.pro
+
+# ── SMTP Configuration (for sending emails) ──────────────────
+# Hostinger SMTP settings (check Hostinger hPanel for exact values)
+SMTP_HOST=smtp.hostinger.com
+SMTP_PORT=465
+SMTP_USER=info@mspe.pro
+SMTP_PASS=your_email_password
+SMTP_FROM_NAME=MSPE
+SMTP_FROM_EMAIL=info@mspe.pro
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e04c930
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+.env
+data/*.json
+!data/.htaccess
+uploads/*
+!uploads/.htaccess
+!uploads/news/.gitkeep
+!uploads/portfolio/.gitkeep
+!uploads/clients/.gitkeep
+!uploads/team/.gitkeep
+.DS_Store
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..cbf167e
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,85 @@
+# MSPE Public Folder .htaccess
+
+RewriteEngine On
+RewriteBase /
+
+# Disable directory listing
+Options -Indexes
+
+# Force HTTPS
+RewriteCond %{HTTPS} off
+RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
+# Handle SPA-style routing if needed - redirect 404s to index
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_URI} !^/api/
+RewriteCond %{REQUEST_URI} !^/admin/
+RewriteRule ^.*$ 404.html [L]
+
+# Block dev/test/seed scripts in production
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
+
+# Protect .env files
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
+
+# Prevent access to hidden files
+
+ Order allow,deny
+ Deny from all
+
+
+# Prevent access to JSON data files from web
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
+
+# Security headers
+
+ Header set X-Content-Type-Options "nosniff"
+ Header set X-Frame-Options "SAMEORIGIN"
+ Header set X-XSS-Protection "0"
+ Header set Referrer-Policy "strict-origin-when-cross-origin"
+ Header set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+ Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; img-src 'self' data: https://ui-avatars.com https://maps.gstatic.com; connect-src 'self' https://challenges.cloudflare.com; frame-src https://www.google.com https://challenges.cloudflare.com; frame-ancestors 'self'"
+
+
+# Enable compression
+
+ AddOutputFilterByType DEFLATE text/html text/plain text/css application/json application/javascript text/xml
+
+
+# Browser caching
+
+ ExpiresActive On
+ ExpiresByType image/jpg "access plus 1 month"
+ ExpiresByType image/jpeg "access plus 1 month"
+ ExpiresByType image/png "access plus 1 month"
+ ExpiresByType image/gif "access plus 1 month"
+ ExpiresByType image/webp "access plus 1 month"
+ ExpiresByType text/css "access plus 1 week"
+ ExpiresByType application/javascript "access plus 1 week"
+
diff --git a/Public HTML/.env.example b/Public HTML/.env.example
new file mode 100644
index 0000000..2260f94
--- /dev/null
+++ b/Public HTML/.env.example
@@ -0,0 +1,47 @@
+# ========================================
+# MSPE Website - Environment Configuration
+# ========================================
+# Copy this file to .env and fill in your actual values.
+# NEVER commit the .env file to version control.
+
+# ── Application ───────────────────────────────────────────────
+APP_ENV=production # 'production' or 'development'
+
+# ── Database ──────────────────────────────────────────────────
+DB_TYPE=mysql # Only 'mysql' is supported in production
+DB_HOST=localhost
+DB_NAME=mspe_website
+DB_USER=
+DB_PASS=
+
+# ── Admin Credentials ─────────────────────────────────────────
+# ADMIN_PASS must be a bcrypt hash from password_hash().
+# Generate one with: php -r "echo password_hash('your-password', PASSWORD_DEFAULT);"
+ADMIN_USER=admin
+ADMIN_PASS=
+
+# ── JWT Secret ────────────────────────────────────────────────
+# REQUIRED in production. Generate with: php -r "echo bin2hex(random_bytes(32));"
+# The server will refuse to start if this is left as the default.
+JWT_SECRET=change-me-in-env-file
+
+# ── Site Settings ─────────────────────────────────────────────
+SITE_NAME=MSPE
+SITE_URL=https://mspe.pro
+ADMIN_EMAIL=info@mspe.pro
+
+# ── SMTP Email ────────────────────────────────────────────────
+# Used for contact form notifications and admin emails.
+# These can also be overridden in Admin > Settings.
+SMTP_HOST=
+SMTP_PORT=465
+SMTP_USER=
+SMTP_PASS=
+SMTP_FROM_NAME=MSPE
+SMTP_FROM_EMAIL=info@mspe.pro
+
+# ── Cloudflare Turnstile (Optional) ──────────────────────────
+# If set, enables human verification on the contact form.
+# Get keys at: https://dash.cloudflare.com/turnstile
+TURNSTILE_SITE_KEY=
+TURNSTILE_SECRET_KEY=
diff --git a/Public HTML/.htaccess b/Public HTML/.htaccess
new file mode 100644
index 0000000..b5844a2
--- /dev/null
+++ b/Public HTML/.htaccess
@@ -0,0 +1,85 @@
+# MSPE Public Folder .htaccess
+
+RewriteEngine On
+RewriteBase /
+
+# Disable directory listing
+Options -Indexes
+
+# Force HTTPS
+RewriteCond %{HTTPS} off
+RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
+# Handle SPA-style routing if needed - redirect 404s to index
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_URI} !^/api/
+RewriteCond %{REQUEST_URI} !^/admin/
+RewriteRule ^.*$ 404.html [L]
+
+# Block dev/test/seed scripts in production
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
+
+# Protect .env files
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
+
+# Prevent access to hidden files
+
+ Order allow,deny
+ Deny from all
+
+
+# Prevent access to JSON data files from web
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
+
+# Security headers
+
+ Header set X-Content-Type-Options "nosniff"
+ Header set X-Frame-Options "SAMEORIGIN"
+ Header set X-XSS-Protection "0"
+ Header set Referrer-Policy "strict-origin-when-cross-origin"
+ Header set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
+ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+ Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; img-src 'self' data: https://ui-avatars.com https://maps.gstatic.com https://www.google.com; connect-src 'self' https://challenges.cloudflare.com; frame-src https://www.google.com https://challenges.cloudflare.com; frame-ancestors 'self'"
+
+
+# Enable compression
+
+ AddOutputFilterByType DEFLATE text/html text/plain text/css application/json application/javascript text/xml
+
+
+# Browser caching
+
+ ExpiresActive On
+ ExpiresByType image/jpg "access plus 1 month"
+ ExpiresByType image/jpeg "access plus 1 month"
+ ExpiresByType image/png "access plus 1 month"
+ ExpiresByType image/gif "access plus 1 month"
+ ExpiresByType image/webp "access plus 1 month"
+ ExpiresByType text/css "access plus 1 week"
+ ExpiresByType application/javascript "access plus 1 week"
+
diff --git a/Public HTML/404.html b/Public HTML/404.html
new file mode 100755
index 0000000..0b0d978
--- /dev/null
+++ b/Public HTML/404.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+ Page Not Found | MSPE - Architects of Digital Resilience
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
404
+
Page Not Found
+
+ Oops! The page you're looking for doesn't exist or has been moved.
+ Let's get you back on track.
+
+
+ Back to Homepage
+
+
+
+
+
diff --git a/Public HTML/about.html b/Public HTML/about.html
new file mode 100755
index 0000000..56bf5cc
--- /dev/null
+++ b/Public HTML/about.html
@@ -0,0 +1,492 @@
+
+
+
+
+
+
+ About Us | MSPE - Architects of Digital Resilience
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to main content
+
+
+
+
+
+
+
+
+
+
Who We Are
+
About MSPE
+
We're architects of digital resilience - building the technological foundations that empower businesses to thrive in an ever-evolving digital landscape.
+
+
+
+
+
+
+
+
+
+
Our Origin
+
Born From a Bold Belief
+
MSPE emerged from a fundamental conviction: technology should be a catalyst for human potential, not a barrier to it. We saw too many businesses struggling with fragmented solutions, reactive security, and technology that served itself rather than its users.
+
We founded MSPE to be different - to be the company that sees the whole picture, that thinks three steps ahead, and that treats every client's challenge as if it were our own. We're not here to sell services; we're here to architect futures.
+
+
+
+
+
Our Mission
+
To transform complexity into competitive advantage. We engineer technology ecosystems that don't just solve today's problems - they anticipate tomorrow's opportunities. Every solution we craft is designed to amplify human capability, protect digital assets, and unlock growth that once seemed impossible.
+
+
+
+
+
+
Our Vision
+
A world where every organization, regardless of size, operates with the digital resilience of a Fortune 500 company. We envision a future where security is invisible yet impenetrable, where technology amplifies creativity rather than constraining it, and where businesses are limited only by their ambition - never by their infrastructure.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Anticipatory Intelligence
+
We don't wait for problems to find you. We study patterns, analyze trends, and build defenses against threats that don't yet exist. Prevention isn't just better than cure - it's the only acceptable approach.
+
+
+
+
Radical Transparency
+
No black boxes. No hidden agendas. No technical jargon designed to confuse. We believe you have the right to understand exactly what we're doing, why we're doing it, and what it costs.
+
+
+
+
Strategic Courage
+
We'll tell you what you need to hear, not what you want to hear. If your current path has risks, we'll say so. If a cheaper solution won't work, we'll explain why. Your trust is worth more than any contract.
+
+
+
+
Boundless Curiosity
+
Technology never stops evolving, and neither do we. Every challenge is a puzzle to solve, every project an opportunity to learn. We bring the same enthusiasm to our hundredth project as our first.
+
+
+
+
Bespoke Precision
+
Cookie-cutter solutions create cookie-cutter results. Every organization is unique, and every solution we create is tailored to your specific context, culture, and aspirations.
+
+
+
+
Uncompromising Security
+
Security isn't a feature - it's the foundation. It's woven into every recommendation we make, every system we design, every line of code we touch. There are no shortcuts when trust is on the line.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 01
+
+
Deep Discovery
+
We don't start with solutions - we start with questions. Understanding your business, challenges, and aspirations before touching any technology.
+
+
+
+
+ 02
+
+
Strategic Architecture
+
We design solutions that work today and scale tomorrow. Every recommendation is mapped to your goals, budget, and growth trajectory.
+
+
+
+
+ 03
+
+
Precision Execution
+
Implementation with minimal disruption and maximum impact. We move fast, but never at the expense of quality or security.
+
+
+
+
+ 04
+
+
Continuous Evolution
+
Our relationship doesn't end at deployment. We monitor, optimize, and evolve your systems as your business grows and threats change.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Kareem Mansour
+
Founder & CEO
+
Network & infrastructure specialist · 12+ yrs across enterprise and SMB environments.
+
+
+
+
+
+
+
+
+
+
Nour Haddad
+
Chief Technology Officer
+
Systems architect with deep expertise in cloud platforms and enterprise IT design.
+
+
+
+
+
+
+
+
+
+
Ranya Aziz
+
Head of Cybersecurity
+
Penetration tester and SIEM specialist · CEH certified and ISO 27001 lead auditor.
+
+
+
+
+
+
+
+
+
+
Elie Khoury
+
Cloud Solutions Architect
+
Azure & AWS certified · specializes in cloud migration, hybrid infrastructure, and DevOps.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
"MSPE transformed our IT infrastructure in weeks. Their proactive approach to security and hands-on support made us feel like their only client. Fast, responsive, and genuinely knowledgeable."
+
+
RH
+
+
Rami Haddad
+
Operations Manager, Levant Trading Co.
+
+
+
+
+
+
+
+
"We needed a partner who understood both cloud migration and cybersecurity. MSPE delivered on both fronts with zero downtime during our transition. Highly recommend their team."
+
+
NK
+
+
Nadia Khoury
+
IT Director, Cedar Financial Group
+
+
+
+
+
+
+
+
"What sets MSPE apart is their honesty and transparency. They don't oversell — they assess what you actually need and deliver exactly that. Our security posture improved dramatically."
+
+
MS
+
+
Marc Sfeir
+
CEO, BlueLine Digital Agency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Microsoft Azure
+
+
+
+
Amazon Web Services
+
+
+
+
Google Cloud
+
+
+
+
Linux & Open Source
+
+
+
+
Kubernetes
+
+
+
+
Docker
+
+
+
+
+
+
+
+
+
+
+
Ready to Work Together?
+
Let's discuss how MSPE can help transform your business with our expert services.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/availability.html b/Public HTML/admin/availability.html
new file mode 100755
index 0000000..8a7dc4a
--- /dev/null
+++ b/Public HTML/admin/availability.html
@@ -0,0 +1,930 @@
+
+
+
+
+
+
+ Availability - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Date
+ Time
+ Capacity
+ Status
+ Notes
+ Actions
+
+
+
+
+
+
+
+
Loading availability slots...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/bookings.html b/Public HTML/admin/bookings.html
new file mode 100755
index 0000000..da45d6e
--- /dev/null
+++ b/Public HTML/admin/bookings.html
@@ -0,0 +1,732 @@
+
+
+
+
+
+
+ Bookings - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status
+
+ All Statuses
+ Pending
+ Confirmed
+ Cancelled
+
+
+
+
+ Date From
+
+
+
+
+ Date To
+
+
+
+
+
+
+
+
+ Date
+ Time
+ Client
+ Email
+ Service
+ Status
+ Actions
+
+
+
+
+
+
+
+
Loading bookings...
+
+
+
+
+
+
+
+
+
+
+
+
Select a booking to view details
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/css/admin.css b/Public HTML/admin/css/admin.css
new file mode 100755
index 0000000..5fc22b7
--- /dev/null
+++ b/Public HTML/admin/css/admin.css
@@ -0,0 +1,3334 @@
+/* =========================================
+ MSPE Admin Panel Stylesheet
+ ========================================= */
+
+:root {
+ --primary: #0ea5e9;
+ --primary-dark: #0284c7;
+ --secondary: #0d9488;
+ --success: #10b981;
+ --warning: #f59e0b;
+ --danger: #ef4444;
+ --dark: #0a1628;
+ --dark-light: #1e293b;
+ --gray-100: #f1f5f9;
+ --gray-200: #e2e8f0;
+ --gray-300: #cbd5e1;
+ --gray-400: #94a3b8;
+ --gray-500: #64748b;
+ --gray-600: #475569;
+ --gray-700: #334155;
+ --white: #ffffff;
+ --sidebar-width: 260px;
+ --header-height: 70px;
+ --font-main: 'DM Sans', sans-serif;
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--font-main);
+ background: var(--gray-100);
+ color: var(--gray-700);
+ line-height: 1.6;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+/* ========================
+ Login Page
+ ======================== */
+.login-page {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--dark);
+ position: relative;
+ overflow: hidden;
+}
+
+.login-container {
+ width: 100%;
+ max-width: 450px;
+ padding: 2rem;
+ position: relative;
+ z-index: 1;
+}
+
+.login-card {
+ background: var(--white);
+ border-radius: var(--radius-lg);
+ padding: 3rem;
+ box-shadow: var(--shadow-lg);
+}
+
+.login-header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.login-logo {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.login-logo i {
+ font-size: 2.5rem;
+ color: var(--primary);
+}
+
+.login-logo span {
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--dark);
+}
+
+.login-header h1 {
+ font-size: 1.5rem;
+ color: var(--dark);
+ margin-bottom: 0.5rem;
+}
+
+.login-header p {
+ color: var(--gray-500);
+}
+
+.login-form .form-group {
+ margin-bottom: 1.25rem;
+}
+
+.login-form label {
+ display: block;
+ font-weight: 500;
+ color: var(--gray-700);
+ margin-bottom: 0.5rem;
+}
+
+.input-icon {
+ position: relative;
+}
+
+.input-icon i {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--gray-400);
+}
+
+.input-icon input {
+ width: 100%;
+ padding: 0.875rem 1rem 0.875rem 2.75rem;
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-md);
+ font-size: 1rem;
+ transition: all 0.3s ease;
+}
+
+.input-icon input:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.1);
+}
+
+.toggle-password {
+ position: absolute;
+ right: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: var(--gray-400);
+ cursor: pointer;
+}
+
+.form-options {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+ color: var(--gray-600);
+}
+
+.forgot-link {
+ color: var(--primary);
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.forgot-link:hover {
+ text-decoration: underline;
+}
+
+.login-message {
+ margin-top: 1rem;
+ padding: 0.75rem 1rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+ display: none;
+}
+
+.login-message.success {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+}
+
+.login-message.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+.login-footer {
+ margin-top: 2rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid var(--gray-200);
+ text-align: center;
+}
+
+.login-footer p {
+ font-size: 0.85rem;
+ color: var(--gray-500);
+ margin-bottom: 0.25rem;
+}
+
+.login-bg {
+ position: fixed;
+ inset: 0;
+ z-index: 0;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.bg-shape {
+ position: absolute;
+ border-radius: 50%;
+ opacity: 0.1;
+}
+
+.bg-shape-1 {
+ width: 600px;
+ height: 600px;
+ background: var(--primary);
+ top: -200px;
+ right: -200px;
+}
+
+.bg-shape-2 {
+ width: 400px;
+ height: 400px;
+ background: var(--secondary);
+ bottom: -100px;
+ left: -100px;
+}
+
+.bg-shape-3 {
+ width: 300px;
+ height: 300px;
+ background: var(--primary);
+ bottom: 20%;
+ right: 10%;
+}
+
+/* ========================
+ Buttons
+ ======================== */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: var(--font-main);
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
+ color: var(--white);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(14, 165, 233, 0.4);
+}
+
+.btn-secondary {
+ background: var(--gray-200);
+ color: var(--gray-700);
+}
+
+.btn-secondary:hover {
+ background: var(--gray-300);
+}
+
+.btn-danger {
+ background: var(--danger);
+ color: var(--white);
+}
+
+.btn-danger:hover {
+ background: #dc2626;
+}
+
+.btn-block {
+ width: 100%;
+}
+
+.btn-sm {
+ padding: 0.5rem 1rem;
+ font-size: 0.85rem;
+}
+
+/* ========================
+ Admin Layout
+ ======================== */
+.admin-wrapper {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Sidebar */
+.sidebar {
+ width: var(--sidebar-width);
+ background: var(--dark);
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ z-index: 100;
+ transition: transform 0.3s ease;
+}
+
+.sidebar-header {
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.sidebar-logo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.sidebar-logo i {
+ font-size: 1.75rem;
+ color: var(--primary);
+}
+
+.sidebar-logo span {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--white);
+}
+
+.sidebar-toggle {
+ display: none;
+ background: none;
+ border: none;
+ color: var(--gray-400);
+ font-size: 1.25rem;
+ cursor: pointer;
+}
+
+.sidebar-nav {
+ flex: 1;
+ padding: 1.5rem 0;
+ overflow-y: auto;
+}
+
+.nav-section {
+ margin-bottom: 1.5rem;
+}
+
+.nav-section-title {
+ display: block;
+ padding: 0 1.5rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: var(--gray-500);
+ margin-bottom: 0.75rem;
+}
+
+.nav-menu {
+ list-style: none;
+}
+
+.nav-item {
+ margin-bottom: 0.25rem;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1.5rem;
+ color: var(--gray-400);
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.nav-link:hover {
+ color: var(--white);
+ background: rgba(255,255,255,0.05);
+}
+
+.nav-item.active .nav-link {
+ color: var(--white);
+ background: linear-gradient(90deg, rgba(14, 165, 233, 0.2), transparent);
+ border-left: 3px solid var(--primary);
+}
+
+.nav-link i {
+ width: 20px;
+ text-align: center;
+}
+
+.nav-link .badge {
+ margin-left: auto;
+ background: var(--danger);
+ color: var(--white);
+ padding: 0.2rem 0.5rem;
+ border-radius: 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+}
+
+.sidebar-footer {
+ padding: 1.5rem;
+ border-top: 1px solid rgba(255,255,255,0.1);
+}
+
+.view-site-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background: rgba(255,255,255,0.05);
+ border-radius: var(--radius-md);
+ color: var(--gray-400);
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.view-site-btn:hover {
+ background: rgba(255,255,255,0.1);
+ color: var(--white);
+}
+
+/* Main Content */
+.main-content {
+ flex: 1;
+ margin-left: var(--sidebar-width);
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Header */
+.admin-header {
+ height: var(--header-height);
+ background: var(--white);
+ padding: 0 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: var(--shadow-sm);
+ position: sticky;
+ top: 0;
+ z-index: 50;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.mobile-toggle {
+ display: none;
+ background: none;
+ border: none;
+ font-size: 1.25rem;
+ color: var(--gray-600);
+ cursor: pointer;
+}
+
+.page-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--dark);
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+
+.header-search {
+ position: relative;
+}
+
+.header-search i {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--gray-400);
+}
+
+.header-search input {
+ padding: 0.625rem 1rem 0.625rem 2.5rem;
+ border: 2px solid var(--gray-200);
+ border-radius: 30px;
+ width: 250px;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+}
+
+.header-search input:focus {
+ outline: none;
+ border-color: var(--primary);
+ width: 300px;
+}
+
+.notification-btn {
+ position: relative;
+ background: none;
+ border: none;
+ font-size: 1.25rem;
+ color: var(--gray-500);
+ cursor: pointer;
+ padding: 0.5rem;
+}
+
+.notification-badge {
+ position: absolute;
+ top: 0;
+ right: 0;
+ background: var(--danger);
+ color: var(--white);
+ font-size: 0.65rem;
+ font-weight: 600;
+ padding: 0.15rem 0.4rem;
+ border-radius: 10px;
+}
+
+.header-user {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ position: relative;
+}
+
+.user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.user-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.user-name {
+ font-weight: 600;
+ color: var(--dark);
+ font-size: 0.9rem;
+}
+
+.user-role {
+ font-size: 0.75rem;
+ color: var(--gray-500);
+}
+
+.user-dropdown {
+ position: relative;
+}
+
+.dropdown-toggle {
+ background: none;
+ border: none;
+ color: var(--gray-400);
+ cursor: pointer;
+ padding: 0.25rem;
+}
+
+.user-dropdown .dropdown-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ background: var(--white);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ min-width: 180px;
+ padding: 0.5rem 0;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(10px);
+ transition: all 0.3s ease;
+ list-style: none;
+}
+
+.user-dropdown:hover .dropdown-menu {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.user-dropdown .dropdown-menu li a {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ color: var(--gray-600);
+ transition: all 0.3s ease;
+}
+
+.user-dropdown .dropdown-menu li a:hover {
+ background: var(--gray-100);
+ color: var(--primary);
+}
+
+.user-dropdown .dropdown-menu .divider {
+ border-top: 1px solid var(--gray-200);
+ margin: 0.5rem 0;
+}
+
+/* Content Wrapper */
+.content-wrapper {
+ padding: 2rem;
+ flex: 1;
+}
+
+/* ========================
+ Dashboard
+ ======================== */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.stat-card {
+ background: var(--white);
+ padding: 1.5rem;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.stat-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+ color: var(--white);
+}
+
+.stat-icon.bg-blue { background: linear-gradient(135deg, #3b82f6, #1d4ed8); }
+.stat-icon.bg-green { background: linear-gradient(135deg, #10b981, #059669); }
+.stat-icon.bg-purple { background: linear-gradient(135deg, #8b5cf6, #6d28d9); }
+.stat-icon.bg-orange { background: linear-gradient(135deg, #f59e0b, #d97706); }
+
+.stat-info h3 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: var(--dark);
+ line-height: 1;
+}
+
+.stat-info p {
+ color: var(--gray-500);
+ font-size: 0.9rem;
+ margin-top: 0.25rem;
+}
+
+.stat-trend {
+ margin-left: auto;
+ font-size: 0.85rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.stat-trend.up { color: var(--success); }
+.stat-trend.down { color: var(--danger); }
+
+/* Cards */
+.card {
+ background: var(--white);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ overflow: hidden;
+}
+
+.card-header {
+ padding: 1.25rem 1.5rem;
+ border-bottom: 1px solid var(--gray-200);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.card-header h2, .card-header h3 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--dark);
+}
+
+.card-body {
+ padding: 1.5rem;
+}
+
+.view-all {
+ color: var(--primary);
+ font-weight: 500;
+ font-size: 0.9rem;
+}
+
+.view-all:hover {
+ text-decoration: underline;
+}
+
+/* Dashboard Rows */
+.dashboard-row {
+ margin-bottom: 2rem;
+}
+
+.dashboard-row.two-col {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+}
+
+/* Quick Actions */
+.quick-actions {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 1rem;
+}
+
+.quick-action-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1.5rem;
+ background: var(--gray-100);
+ border-radius: var(--radius-md);
+ transition: all 0.3s ease;
+}
+
+.quick-action-btn:hover {
+ background: var(--primary);
+ color: var(--white);
+ transform: translateY(-3px);
+}
+
+.quick-action-btn i {
+ font-size: 1.5rem;
+}
+
+.quick-action-btn span {
+ font-weight: 500;
+ font-size: 0.9rem;
+}
+
+/* Content Lists */
+.content-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.content-item {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.content-item:last-child {
+ border-bottom: none;
+}
+
+.item-image {
+ width: 70px;
+ height: 50px;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.item-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.item-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.item-info h4 {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--dark);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.item-meta {
+ font-size: 0.8rem;
+ color: var(--gray-500);
+ margin-top: 0.25rem;
+}
+
+.item-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.action-btn {
+ width: 32px;
+ height: 32px;
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.action-btn.edit {
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+}
+
+.action-btn.edit:hover {
+ background: #3b82f6;
+ color: var(--white);
+}
+
+.action-btn.view {
+ background: rgba(16, 185, 129, 0.1);
+ color: #10b981;
+}
+
+.action-btn.view:hover {
+ background: #10b981;
+ color: var(--white);
+}
+
+.action-btn.delete {
+ background: rgba(239, 68, 68, 0.1);
+ color: #ef4444;
+}
+
+.action-btn.delete:hover {
+ background: #ef4444;
+ color: var(--white);
+}
+
+/* Messages List */
+.message-item {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.message-item:last-child {
+ border-bottom: none;
+}
+
+.message-avatar {
+ width: 45px;
+ height: 45px;
+ border-radius: 50%;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.message-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.message-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.message-info h4 {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--dark);
+}
+
+.message-info p {
+ font-size: 0.85rem;
+ color: var(--gray-500);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-top: 0.25rem;
+}
+
+.message-time {
+ font-size: 0.75rem;
+ color: var(--gray-400);
+ margin-top: 0.25rem;
+}
+
+.message-status {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.message-status.unread { background: var(--primary); }
+.message-status.read { background: var(--gray-300); }
+
+/* ========================
+ Content Management Pages
+ ======================== */
+.content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.content-filters {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.filter-group select {
+ padding: 0.625rem 2rem 0.625rem 1rem;
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+ color: var(--gray-700);
+ background: var(--white);
+ cursor: pointer;
+}
+
+.filter-group select:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+.search-box {
+ position: relative;
+}
+
+.search-box i {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--gray-400);
+}
+
+.search-box input {
+ padding: 0.625rem 1rem 0.625rem 2.5rem;
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-md);
+ width: 250px;
+ font-size: 0.9rem;
+}
+
+.search-box input:focus {
+ outline: none;
+ border-color: var(--primary);
+}
+
+/* Data Table */
+.table-responsive {
+ overflow-x: auto;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.data-table th,
+.data-table td {
+ padding: 1rem 1.25rem;
+ text-align: left;
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.data-table th {
+ background: var(--gray-100);
+ font-weight: 600;
+ color: var(--gray-600);
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.data-table tbody tr:hover {
+ background: var(--gray-100);
+}
+
+.table-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.table-item img {
+ width: 60px;
+ height: 40px;
+ border-radius: var(--radius-sm);
+ object-fit: cover;
+}
+
+.category-badge, .status-badge {
+ display: inline-block;
+ padding: 0.35rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.category-badge.news { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
+.category-badge.events { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
+.category-badge.insights { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
+.category-badge.updates { background: rgba(16, 185, 129, 0.1); color: #10b981; }
+
+.status-badge.published { background: rgba(16, 185, 129, 0.1); color: #10b981; }
+.status-badge.draft { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
+
+.table-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.table-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 1.25rem;
+ border-top: 1px solid var(--gray-200);
+}
+
+.bulk-actions {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.bulk-actions select {
+ padding: 0.5rem 1rem;
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+}
+
+.pagination {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.pagination-info {
+ font-size: 0.9rem;
+ color: var(--gray-500);
+}
+
+.pagination-controls {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.pagination-btn {
+ min-width: 36px;
+ height: 36px;
+ border: none;
+ background: var(--gray-100);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-weight: 500;
+ color: var(--gray-600);
+}
+
+.pagination-btn:hover:not(:disabled) {
+ background: var(--primary);
+ color: var(--white);
+}
+
+.pagination-btn.active {
+ background: var(--primary);
+ color: var(--white);
+}
+
+.pagination-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* ========================
+ Form Styles
+ ======================== */
+.form-grid {
+ display: grid;
+ grid-template-columns: 1fr 350px;
+ gap: 2rem;
+}
+
+.form-main, .form-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.edit-form .form-group {
+ margin-bottom: 1.25rem;
+}
+
+.edit-form .form-group:last-child {
+ margin-bottom: 0;
+}
+
+.edit-form label {
+ display: block;
+ font-weight: 500;
+ color: var(--gray-700);
+ margin-bottom: 0.5rem;
+}
+
+.edit-form input[type="text"],
+.edit-form input[type="email"],
+.edit-form input[type="datetime-local"],
+.edit-form select,
+.edit-form textarea {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-md);
+ font-size: 0.95rem;
+ font-family: var(--font-main);
+ transition: all 0.3s ease;
+}
+
+.edit-form input:focus,
+.edit-form select:focus,
+.edit-form textarea:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.1);
+}
+
+.edit-form textarea {
+ resize: vertical;
+ min-height: 150px;
+}
+
+.form-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-top: 1rem;
+}
+
+/* Image Upload */
+.image-upload {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius-md);
+ padding: 2rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.image-upload:hover {
+ border-color: var(--primary);
+ background: rgba(14, 165, 233, 0.02);
+}
+
+.upload-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.upload-placeholder i {
+ font-size: 2.5rem;
+ color: var(--gray-400);
+}
+
+.upload-placeholder p {
+ color: var(--gray-600);
+ font-weight: 500;
+}
+
+.upload-placeholder span {
+ font-size: 0.8rem;
+ color: var(--gray-400);
+}
+
+.image-upload img {
+ max-width: 100%;
+ max-height: 200px;
+ border-radius: var(--radius-sm);
+}
+
+.remove-image {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+}
+
+/* ========================
+ Modal
+ ======================== */
+.modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.modal.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modal-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.modal-content {
+ position: relative;
+ background: var(--white);
+ border-radius: var(--radius-lg);
+ width: 90%;
+ max-width: 500px;
+ box-shadow: var(--shadow-lg);
+ transform: scale(0.9);
+ transition: transform 0.3s ease;
+}
+
+.modal.active .modal-content {
+ transform: scale(1);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.25rem 1.5rem;
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.modal-header h3 {
+ font-size: 1.25rem;
+ color: var(--dark);
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ color: var(--gray-400);
+ cursor: pointer;
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.modal-footer {
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--gray-200);
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.75rem;
+}
+
+/* ========================
+ Responsive
+ ======================== */
+@media (max-width: 1200px) {
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .quick-actions {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .form-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .form-sidebar {
+ order: -1;
+ }
+}
+
+@media (max-width: 1024px) {
+ .sidebar {
+ transform: translateX(-100%);
+ }
+
+ .sidebar.active {
+ transform: translateX(0);
+ }
+
+ .main-content {
+ margin-left: 0;
+ }
+
+ .mobile-toggle {
+ display: block;
+ }
+
+ .sidebar-toggle {
+ display: block;
+ }
+
+ .dashboard-row.two-col {
+ grid-template-columns: 1fr;
+ }
+
+ .header-search {
+ display: none;
+ }
+}
+
+@media (max-width: 768px) {
+ .content-wrapper {
+ padding: 1rem;
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .content-header {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: stretch;
+ }
+
+ .content-filters {
+ flex-wrap: wrap;
+ }
+
+ .search-box input {
+ width: 100%;
+ }
+
+ .admin-header {
+ padding: 0 1rem;
+ }
+
+ .user-info {
+ display: none;
+ }
+
+ .table-footer {
+ flex-direction: column;
+ gap: 1rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .login-card {
+ padding: 2rem 1.5rem;
+ }
+
+ .quick-actions {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* =========================================
+ CRM Extended Styles
+ ========================================= */
+
+/* Pages Management Grid */
+.pages-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 1.5rem;
+}
+
+.page-admin-card {
+ background: var(--white);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+ transition: all 0.3s ease;
+}
+
+.page-admin-card:hover {
+ box-shadow: var(--shadow-md);
+ border-color: var(--primary);
+}
+
+.page-admin-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 1rem;
+}
+
+.page-icon {
+ width: 48px;
+ height: 48px;
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--white);
+ font-size: 1.25rem;
+}
+
+.page-status {
+ font-size: 0.75rem;
+ font-weight: 500;
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.page-status.published {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+}
+
+.page-status.published i {
+ font-size: 0.5rem;
+}
+
+.page-status.draft {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--warning);
+}
+
+.page-admin-card h3 {
+ font-size: 1.125rem;
+ color: var(--dark);
+ margin-bottom: 0.5rem;
+}
+
+.page-admin-card p {
+ font-size: 0.875rem;
+ color: var(--gray-500);
+ margin-bottom: 1rem;
+}
+
+.page-sections {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.section-tag {
+ background: var(--gray-100);
+ color: var(--gray-600);
+ padding: 0.25rem 0.625rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.page-admin-actions {
+ display: flex;
+ gap: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--gray-200);
+}
+
+/* Section collapse in page editor */
+.sect-body.collapsed {
+ display: none;
+}
+
+/* Global Sections */
+.global-sections-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1rem;
+}
+
+.global-section-item {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ background: var(--gray-100);
+ border-radius: var(--radius-sm);
+}
+
+.global-section-item i {
+ font-size: 1.5rem;
+ color: var(--primary);
+ width: 40px;
+ text-align: center;
+}
+
+.global-section-item div {
+ flex: 1;
+}
+
+.global-section-item h4 {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--dark);
+}
+
+.global-section-item p {
+ font-size: 0.75rem;
+ color: var(--gray-500);
+ margin: 0;
+}
+
+/* Team Management Grid */
+.team-admin-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1.5rem;
+}
+
+.team-admin-card {
+ background: var(--white);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ text-align: center;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+}
+
+.team-member-photo {
+ position: relative;
+ width: 120px;
+ height: 120px;
+ margin: 0 auto 1rem;
+ border-radius: 50%;
+ overflow: hidden;
+}
+
+.team-member-photo img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.photo-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.team-member-photo:hover .photo-overlay {
+ opacity: 1;
+}
+
+.team-member-info h3 {
+ font-size: 1.125rem;
+ color: var(--dark);
+ margin-bottom: 0.25rem;
+}
+
+.team-role {
+ display: block;
+ font-size: 0.875rem;
+ color: var(--primary);
+ font-weight: 500;
+}
+
+.team-dept {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--gray-500);
+ margin-top: 0.25rem;
+}
+
+.team-socials {
+ display: flex;
+ justify-content: center;
+ gap: 0.75rem;
+ margin: 1rem 0;
+}
+
+.social-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--gray-100);
+ color: var(--gray-500);
+ font-size: 0.875rem;
+ transition: all 0.3s ease;
+}
+
+.social-icon:hover {
+ background: var(--primary);
+ color: var(--white);
+}
+
+.team-admin-actions {
+ display: flex;
+ justify-content: center;
+ gap: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--gray-200);
+}
+
+.team-order {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--gray-200);
+ font-size: 0.75rem;
+ color: var(--gray-500);
+}
+
+.order-controls {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.order-controls button {
+ background: var(--gray-100);
+ border: none;
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+ cursor: pointer;
+ color: var(--gray-600);
+}
+
+.order-controls button:hover:not(:disabled) {
+ background: var(--primary);
+ color: var(--white);
+}
+
+.order-controls button:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+/* Testimonials Admin Grid */
+.testimonials-admin-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+ gap: 1.5rem;
+}
+
+.testimonial-admin-card {
+ background: var(--white);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+}
+
+.testimonial-admin-card.pending {
+ border-left: 4px solid var(--warning);
+}
+
+.testimonial-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 1rem;
+}
+
+.testimonial-author {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.testimonial-author img {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.testimonial-author h4 {
+ font-size: 0.9375rem;
+ color: var(--dark);
+ margin-bottom: 0.125rem;
+}
+
+.testimonial-author span {
+ font-size: 0.75rem;
+ color: var(--gray-500);
+}
+
+.testimonial-status {
+ font-size: 0.75rem;
+ font-weight: 500;
+ padding: 0.25rem 0.625rem;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.testimonial-status.published {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+}
+
+.testimonial-status.pending {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--warning);
+}
+
+.testimonial-rating {
+ color: #fbbf24;
+ font-size: 0.875rem;
+ margin-bottom: 0.75rem;
+}
+
+.testimonial-rating i.far {
+ color: var(--gray-300);
+}
+
+.testimonial-text {
+ font-size: 0.875rem;
+ color: var(--gray-600);
+ line-height: 1.6;
+ margin-bottom: 1rem;
+ font-style: italic;
+}
+
+.testimonial-meta {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.75rem;
+ color: var(--gray-400);
+ margin-bottom: 1rem;
+}
+
+.testimonial-meta i {
+ margin-right: 0.35rem;
+}
+
+.testimonial-admin-actions {
+ display: flex;
+ gap: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--gray-200);
+}
+
+/* Media Library Styles */
+.media-stats {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--white);
+ padding: 1.25rem 1.5rem;
+ border-radius: var(--radius-md);
+ margin-bottom: 1.5rem;
+ box-shadow: var(--shadow-sm);
+}
+
+.storage-overview {
+ flex: 1;
+ max-width: 300px;
+}
+
+.storage-used {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+}
+
+.storage-used i {
+ color: var(--primary);
+}
+
+.storage-value {
+ font-weight: 600;
+ color: var(--dark);
+}
+
+.storage-label {
+ font-size: 0.75rem;
+ color: var(--gray-500);
+ margin-left: 0.25rem;
+}
+
+.storage-bar {
+ height: 6px;
+ background: var(--gray-200);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.storage-progress {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary), var(--secondary));
+ border-radius: 3px;
+}
+
+.media-counts {
+ display: flex;
+ gap: 2rem;
+}
+
+.media-count-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ color: var(--gray-600);
+}
+
+.media-count-item i {
+ color: var(--primary);
+}
+
+.media-toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.toolbar-left,
+.toolbar-right {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.media-folders {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.folder-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ background: var(--white);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ border: 1px solid var(--gray-200);
+ transition: all 0.3s ease;
+}
+
+.folder-item:hover {
+ border-color: var(--primary);
+ background: rgba(14, 165, 233, 0.05);
+}
+
+.folder-item i {
+ color: #fbbf24;
+ font-size: 1.25rem;
+}
+
+.folder-item span {
+ font-weight: 500;
+ color: var(--dark);
+}
+
+.folder-item small {
+ font-size: 0.75rem;
+ color: var(--gray-400);
+}
+
+.media-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 1rem;
+}
+
+.media-item {
+ position: relative;
+ background: var(--white);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+}
+
+.media-preview {
+ position: relative;
+ height: 140px;
+ background: var(--gray-100);
+ overflow: hidden;
+}
+
+.media-preview img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.media-preview.document,
+.media-preview.video {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.media-preview.document i {
+ font-size: 3rem;
+ color: #ef4444;
+}
+
+.media-preview.video i {
+ font-size: 3rem;
+ color: var(--primary);
+}
+
+.media-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.media-item:hover .media-overlay {
+ opacity: 1;
+}
+
+.media-overlay .btn {
+ background: var(--white);
+ color: var(--dark);
+ border: none;
+ padding: 0.5rem;
+ border-radius: 4px;
+}
+
+.media-info {
+ padding: 0.75rem;
+}
+
+.media-name {
+ display: block;
+ font-size: 0.8125rem;
+ font-weight: 500;
+ color: var(--dark);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.media-size {
+ font-size: 0.75rem;
+ color: var(--gray-400);
+}
+
+.media-checkbox {
+ position: absolute;
+ top: 0.5rem;
+ left: 0.5rem;
+ z-index: 2;
+}
+
+.media-checkbox input {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+.bulk-actions {
+ position: fixed;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--dark);
+ padding: 1rem 1.5rem;
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: var(--shadow-lg);
+ z-index: 100;
+}
+
+.bulk-actions .selected-count {
+ color: var(--white);
+ font-weight: 500;
+}
+
+.upload-dropzone {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius-md);
+ padding: 3rem 2rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.upload-dropzone:hover,
+.upload-dropzone.dragover {
+ border-color: var(--primary);
+ background: rgba(14, 165, 233, 0.05);
+}
+
+.upload-dropzone i {
+ font-size: 3rem;
+ color: var(--gray-400);
+ margin-bottom: 1rem;
+}
+
+.upload-dropzone h3 {
+ color: var(--dark);
+ margin-bottom: 0.5rem;
+}
+
+.upload-dropzone p {
+ color: var(--gray-500);
+}
+
+.upload-hint {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--gray-400);
+ margin-top: 0.5rem;
+}
+
+/* Users Table Styles */
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.user-info img {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+}
+
+.user-info .name {
+ display: block;
+ font-weight: 500;
+ color: var(--dark);
+}
+
+.user-info .username {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--gray-400);
+}
+
+.role-badge {
+ display: inline-block;
+ padding: 0.25rem 0.625rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.role-badge.super-admin {
+ background: rgba(139, 92, 246, 0.1);
+ color: #8b5cf6;
+}
+
+.role-badge.editor {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+}
+
+.role-badge.contributor {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--warning);
+}
+
+.status-badge.active {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+}
+
+.status-badge.inactive {
+ background: rgba(107, 114, 128, 0.1);
+ color: var(--gray-500);
+}
+
+.status-badge.pending {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--warning);
+}
+
+.status-badge.confirmed {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+}
+
+.status-badge.completed {
+ background: rgba(14, 165, 233, 0.1);
+ color: var(--primary);
+}
+
+.status-badge.cancelled {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger);
+}
+
+/* Roles Grid */
+.roles-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1.5rem;
+}
+
+.role-card {
+ background: var(--gray-100);
+ border-radius: var(--radius-md);
+ padding: 1.25rem;
+}
+
+.role-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.role-count {
+ font-size: 0.75rem;
+ color: var(--gray-500);
+}
+
+.permissions-list {
+ list-style: none;
+}
+
+.permissions-list li {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0;
+ font-size: 0.875rem;
+ color: var(--gray-600);
+ border-bottom: 1px solid var(--gray-200);
+}
+
+.permissions-list li:last-child {
+ border-bottom: none;
+}
+
+.permissions-list li i.fa-check {
+ color: var(--success);
+}
+
+.permissions-list li i.fa-times {
+ color: var(--gray-400);
+}
+
+/* Activity Timeline */
+.activity-timeline {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.activity-item {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.activity-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.875rem;
+ flex-shrink: 0;
+}
+
+.activity-icon.login {
+ background: rgba(14, 165, 233, 0.1);
+ color: var(--primary);
+}
+
+.activity-icon.edit {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--warning);
+}
+
+.activity-icon.create {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--success);
+}
+
+.activity-icon.settings {
+ background: rgba(139, 92, 246, 0.1);
+ color: #8b5cf6;
+}
+
+.activity-content p {
+ font-size: 0.875rem;
+ color: var(--gray-700);
+}
+
+.activity-time {
+ font-size: 0.75rem;
+ color: var(--gray-400);
+}
+
+/* Filter Tabs */
+.filter-tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.filter-tab {
+ padding: 0.625rem 1rem;
+ background: var(--white);
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-sm);
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--gray-600);
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.filter-tab:hover {
+ border-color: var(--primary);
+ color: var(--primary);
+}
+
+.filter-tab.active {
+ background: var(--primary);
+ border-color: var(--primary);
+ color: var(--white);
+}
+
+.filter-tab .badge {
+ display: inline-block;
+ background: rgba(255, 255, 255, 0.2);
+ padding: 0.125rem 0.5rem;
+ border-radius: 10px;
+ font-size: 0.75rem;
+ margin-left: 0.5rem;
+}
+
+/* Client Info in Tables */
+.client-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.client-info .name {
+ font-weight: 500;
+ color: var(--dark);
+}
+
+.client-info .email {
+ font-size: 0.75rem;
+ color: var(--gray-400);
+}
+
+.service-badge {
+ display: inline-block;
+ background: var(--gray-100);
+ padding: 0.25rem 0.625rem;
+ border-radius: 4px;
+ font-size: 0.8125rem;
+ font-weight: 500;
+ color: var(--gray-700);
+}
+
+.datetime {
+ font-size: 0.8125rem;
+ color: var(--gray-600);
+ line-height: 1.8;
+}
+
+.datetime i {
+ color: var(--gray-400);
+ margin-right: 0.35rem;
+ width: 14px;
+}
+
+.completed-row {
+ opacity: 0.7;
+}
+
+/* Stats Row */
+.stats-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.stat-card {
+ background: var(--white);
+ padding: 1.25rem;
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ box-shadow: var(--shadow-sm);
+}
+
+.stat-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.25rem;
+}
+
+.stat-details {
+ display: flex;
+ flex-direction: column;
+}
+
+.stat-value {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--dark);
+ line-height: 1.2;
+}
+
+.stat-label {
+ font-size: 0.8125rem;
+ color: var(--gray-500);
+}
+
+/* View Toggle */
+.view-toggle {
+ display: flex;
+ border: 1px solid var(--gray-200);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+}
+
+.view-btn {
+ padding: 0.5rem 0.75rem;
+ background: var(--white);
+ border: none;
+ cursor: pointer;
+ color: var(--gray-500);
+ transition: all 0.3s ease;
+}
+
+.view-btn:hover {
+ color: var(--primary);
+}
+
+.view-btn.active {
+ background: var(--primary);
+ color: var(--white);
+}
+
+/* Filter Bar */
+.filter-bar {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+/* Social Inputs */
+.social-inputs {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.input-icon-group {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.input-icon-group i {
+ width: 24px;
+ color: var(--gray-400);
+ font-size: 1.125rem;
+}
+
+.input-icon-group input {
+ flex: 1;
+}
+
+/* Checkbox Group */
+.checkbox-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ color: var(--gray-600);
+ cursor: pointer;
+}
+
+.checkbox-label input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+}
+
+/* Action Buttons */
+.action-buttons {
+ display: flex;
+ gap: 0.35rem;
+}
+
+/* Header Actions */
+.header-actions {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+/* Media View Modal */
+.media-view-content {
+ display: grid;
+ grid-template-columns: 1fr 300px;
+ gap: 2rem;
+}
+
+.media-view-preview {
+ background: var(--gray-100);
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 300px;
+}
+
+.media-view-preview img {
+ max-width: 100%;
+ max-height: 400px;
+ object-fit: contain;
+}
+
+.media-view-details {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.detail-row {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.detail-row label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--gray-500);
+ text-transform: uppercase;
+}
+
+.detail-row span {
+ color: var(--gray-700);
+ font-size: 0.875rem;
+}
+
+.url-copy {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.url-copy input {
+ flex: 1;
+ font-size: 0.75rem;
+}
+
+/* Button Variations */
+.btn-success {
+ background: var(--success);
+ color: var(--white);
+ border: none;
+}
+
+.btn-success:hover {
+ background: #059669;
+}
+
+/* Responsive Adjustments for New Components */
+@media (max-width: 768px) {
+ .pages-grid,
+ .team-admin-grid,
+ .testimonials-admin-grid,
+ .roles-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .media-stats {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: stretch;
+ }
+
+ .storage-overview {
+ max-width: 100%;
+ }
+
+ .media-counts {
+ justify-content: space-between;
+ }
+
+ .media-view-content {
+ grid-template-columns: 1fr;
+ }
+
+ .bulk-actions {
+ left: 1rem;
+ right: 1rem;
+ transform: none;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+}
+
+/* ============================================================
+ Settings Tabs
+ ============================================================ */
+.settings-tabs {
+ display: flex;
+ gap: 0.25rem;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
+ background: var(--gray-100, #f1f5f9);
+ padding: 0.25rem;
+ border-radius: 0.75rem;
+}
+
+.tab-btn {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.625rem 1rem;
+ border: none;
+ background: transparent;
+ color: var(--gray-500, #64748b);
+ font-size: 0.875rem;
+ font-weight: 500;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+}
+
+.tab-btn:hover {
+ color: var(--gray-700, #334155);
+ background: var(--gray-200, #e2e8f0);
+}
+
+.tab-btn.active {
+ background: #fff;
+ color: var(--primary, #0ea5e9);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.tab-btn i {
+ font-size: 0.8rem;
+}
+
+.tab-content {
+ display: none;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+/* Settings Form Layout */
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ margin-bottom: 0;
+}
+
+.form-hint {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--gray-400, #94a3b8);
+ margin-top: 0.25rem;
+}
+
+.code-textarea {
+ font-family: 'SF Mono', 'Fira Code', monospace;
+ font-size: 0.8125rem;
+}
+
+/* Color Picker */
+.color-picker {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.color-picker input[type="color"] {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 40px;
+ height: 40px;
+ border: 2px solid var(--gray-200, #e2e8f0);
+ border-radius: 0.5rem;
+ cursor: pointer;
+ padding: 2px;
+ flex-shrink: 0;
+}
+
+.color-picker input[type="color"]::-webkit-color-swatch-wrapper {
+ padding: 0;
+}
+
+.color-picker input[type="color"]::-webkit-color-swatch {
+ border: none;
+ border-radius: 0.35rem;
+}
+
+.color-picker input[type="text"] {
+ width: 120px;
+ font-family: 'SF Mono', 'Fira Code', monospace;
+ font-size: 0.8125rem;
+}
+
+/* Logo Upload */
+.logo-upload {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.logo-upload .current-logo,
+.logo-upload .current-favicon {
+ width: 60px;
+ height: 60px;
+ object-fit: contain;
+ border: 1px solid var(--gray-200, #e2e8f0);
+ border-radius: 0.5rem;
+ padding: 4px;
+ background: #fff;
+}
+
+/* Social Input */
+.social-input label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.social-input label i {
+ width: 20px;
+ text-align: center;
+ font-size: 1.1rem;
+}
+
+/* Integration Items */
+.integration-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 0;
+ border-bottom: 1px solid var(--gray-100, #f1f5f9);
+}
+
+.integration-item:last-of-type {
+ border-bottom: none;
+}
+
+.integration-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.integration-info i {
+ font-size: 1.5rem;
+ width: 32px;
+ text-align: center;
+}
+
+.integration-info h4 {
+ margin: 0;
+ font-size: 0.9375rem;
+ font-weight: 600;
+}
+
+.integration-info p {
+ margin: 0;
+ font-size: 0.8125rem;
+ color: var(--gray-400, #94a3b8);
+}
+
+/* Toggle Switch */
+.toggle-switch {
+ position: relative;
+ display: inline-block;
+ width: 44px;
+ height: 24px;
+}
+
+.toggle-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.toggle-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: var(--gray-300, #cbd5e1);
+ border-radius: 24px;
+ transition: 0.3s;
+}
+
+.toggle-slider::before {
+ content: '';
+ position: absolute;
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ bottom: 3px;
+ background: #fff;
+ border-radius: 50%;
+ transition: 0.3s;
+}
+
+.toggle-switch input:checked + .toggle-slider {
+ background: var(--primary, #0ea5e9);
+}
+
+.toggle-switch input:checked + .toggle-slider::before {
+ transform: translateX(20px);
+}
+
+/* Settings responsive */
+@media (max-width: 768px) {
+ .settings-tabs {
+ gap: 0.125rem;
+ }
+
+ .tab-btn {
+ padding: 0.5rem 0.625rem;
+ font-size: 0.75rem;
+ }
+
+ .tab-btn span {
+ display: none;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* ============================================================
+ Missing Classes & Fixes — safe additions only
+ ============================================================ */
+
+/* Extended CSS Variables */
+:root {
+ --gray-50: #f8fafc;
+ --gray-900: #0f172a;
+}
+
+/* ── btn-outline ─────────────────────────────────────────── */
+.btn-outline {
+ background: transparent;
+ border: 2px solid var(--gray-300);
+ color: var(--gray-700);
+}
+.btn-outline:hover {
+ border-color: var(--primary);
+ color: var(--primary);
+ background: rgba(14, 165, 233, 0.05);
+ transform: translateY(-1px);
+}
+.btn-outline.btn-danger,
+.btn.btn-outline.btn-danger {
+ border-color: var(--danger);
+ color: var(--danger);
+ background: transparent;
+}
+.btn-outline.btn-danger:hover {
+ background: var(--danger);
+ color: var(--white);
+}
+
+/* ── form-control & form-select ──────────────────────────── */
+.form-control,
+.form-select {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--gray-200);
+ border-radius: var(--radius-md);
+ font-size: 0.95rem;
+ font-family: var(--font-main);
+ color: var(--gray-700);
+ background: var(--white);
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
+}
+.form-control:focus,
+.form-select:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.1);
+}
+.form-control textarea,
+textarea.form-control {
+ resize: vertical;
+ min-height: 100px;
+}
+
+/* Standalone .form-group (outside .edit-form) */
+.form-group {
+ margin-bottom: 1.25rem;
+}
+.form-group > label {
+ display: block;
+ font-weight: 500;
+ color: var(--gray-700);
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+}
+
+/* ── Utility classes ─────────────────────────────────────── */
+.opacity-50 { opacity: 0.5; }
+.text-danger { color: var(--danger) !important; }
+.text-center { text-align: center; }
+.text-success { color: var(--success) !important; }
+
+/* ── btn-icon ────────────────────────────────────────────── */
+.btn.btn-icon,
+.btn-icon {
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+ font-size: 0.875rem;
+}
+
+/* ── content-description ─────────────────────────────────── */
+.content-description {
+ color: var(--gray-500);
+ font-size: 0.9rem;
+ margin: 0;
+}
+
+/* ── User dropdown active state (JS-toggled) ─────────────── */
+.user-dropdown.active .dropdown-menu {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+/* ── Modal aliases (modal-backdrop / modal-container) ────── */
+.modal-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: inherit;
+}
+.modal-container {
+ position: relative;
+ background: var(--white);
+ border-radius: var(--radius-lg);
+ width: 90%;
+ max-width: 600px;
+ max-height: 90vh;
+ overflow-y: auto;
+ box-shadow: var(--shadow-lg);
+ transform: scale(0.9);
+ transition: transform 0.3s ease;
+}
+.modal.active .modal-container {
+ transform: scale(1);
+}
+.modal-lg,
+.modal-content.modal-lg,
+.modal-container.modal-lg {
+ max-width: 760px;
+}
+.modal-sm,
+.modal-content.modal-sm,
+.modal-container.modal-sm {
+ max-width: 440px;
+}
+
+/* ── Services admin grid ─────────────────────────────────── */
+.services-admin-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+ padding: 1.5rem 0;
+}
+.service-admin-card {
+ background: var(--white);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+ transition: all 0.3s ease;
+}
+.service-admin-card:hover {
+ box-shadow: var(--shadow-md);
+ border-color: var(--primary);
+}
+.service-admin-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 1rem;
+}
+.service-admin-card .service-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.25rem;
+}
+.service-admin-card .service-actions {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+.service-admin-card h3 {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--dark);
+ margin-bottom: 0.5rem;
+}
+.service-admin-card > p {
+ font-size: 0.875rem;
+ color: var(--gray-500);
+ margin-bottom: 1rem;
+}
+.service-meta {
+ font-size: 0.8rem;
+ color: var(--gray-400);
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+/* ── Portfolio admin grid & cards ────────────────────────── */
+.portfolio-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1.5rem;
+ padding: 1.5rem;
+}
+.portfolio-admin-card {
+ background: var(--white);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--gray-200);
+ transition: all 0.3s ease;
+}
+.portfolio-admin-card:hover {
+ box-shadow: var(--shadow-md);
+ transform: translateY(-3px);
+}
+.portfolio-admin-image {
+ position: relative;
+ height: 180px;
+ overflow: hidden;
+ background: var(--gray-100);
+}
+.portfolio-admin-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s ease;
+}
+.portfolio-admin-card:hover .portfolio-admin-image img {
+ transform: scale(1.05);
+}
+.portfolio-status {
+ position: absolute;
+ top: 0.75rem;
+ left: 0.75rem;
+ padding: 0.25rem 0.625rem;
+ border-radius: 20px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.portfolio-status.published { background: rgba(16,185,129,0.9); color: #fff; }
+.portfolio-status.draft { background: rgba(245,158,11,0.9); color: #fff; }
+.portfolio-featured {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ background: #fbbf24;
+ color: #fff;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.75rem;
+}
+.portfolio-admin-content {
+ padding: 1.25rem;
+}
+.portfolio-admin-content h3 {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--dark);
+ margin-bottom: 0.25rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.portfolio-category {
+ font-size: 0.8rem;
+ color: var(--primary);
+ font-weight: 500;
+ text-transform: capitalize;
+ margin-bottom: 0.25rem;
+}
+.portfolio-client {
+ font-size: 0.8rem;
+ color: var(--gray-500);
+ margin-bottom: 1rem;
+}
+.portfolio-admin-actions {
+ display: flex;
+ gap: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--gray-100);
+}
+
+/* ── Portfolio / Content editor layout ───────────────────── */
+.editor-grid {
+ display: grid;
+ grid-template-columns: 1fr 320px;
+ gap: 2rem;
+}
+.editor-main,
+.editor-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+.editor-actions {
+ display: flex;
+ gap: 0.75rem;
+}
+.editor-textarea {
+ min-height: 250px;
+ resize: vertical;
+ font-family: inherit;
+ line-height: 1.6;
+}
+@media (max-width: 1024px) {
+ .editor-grid {
+ grid-template-columns: 1fr;
+ }
+ .editor-sidebar {
+ order: -1;
+ }
+}
+
+/* ── Image / file upload areas ───────────────────────────── */
+.image-upload-area {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius-md);
+ padding: 2rem 1.5rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-size: cover;
+ background-position: center;
+ min-height: 140px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+}
+.image-upload-area:hover {
+ border-color: var(--primary);
+ background-color: rgba(14, 165, 233, 0.02);
+}
+.image-upload-area i { font-size: 2rem; color: var(--gray-400); }
+.image-upload-area p { color: var(--gray-600); font-weight: 500; margin: 0; }
+.image-upload-area span { font-size: 0.8rem; color: var(--gray-400); }
+
+.file-upload-area {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+}
+.file-upload-area:hover { border-color: var(--primary); }
+.file-upload-area i { font-size: 2rem; color: var(--gray-400); display: block; margin-bottom: 0.5rem; }
+.file-upload-area p { color: var(--gray-600); font-weight: 500; margin-bottom: 0.25rem; }
+.file-upload-area span { font-size: 0.75rem; color: var(--gray-400); }
+.file-upload-area input[type="file"] {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ cursor: pointer;
+ width: 100%;
+ height: 100%;
+}
+
+/* ── Gallery upload / preview ────────────────────────────── */
+.gallery-upload-area {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius-md);
+ padding: 1.5rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+.gallery-upload-area:hover { border-color: var(--primary); }
+.gallery-upload-area i { font-size: 1.75rem; color: var(--gray-400); display: block; margin-bottom: 0.5rem; }
+.gallery-upload-area p { color: var(--gray-600); margin: 0; }
+.gallery-preview {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+.gallery-preview img {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: var(--radius-sm);
+}
+.gallery-preview-item {
+ position: relative;
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+ aspect-ratio: 1;
+}
+.gallery-preview-item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+.gallery-remove-btn {
+ position: absolute;
+ top: 0.25rem;
+ right: 0.25rem;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: rgba(0,0,0,0.6);
+ color: #fff;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.625rem;
+ padding: 0;
+ line-height: 1;
+}
+.gallery-remove-btn:hover { background: var(--danger, #ef4444); }
+
+/* ── Project results grid ────────────────────────────────── */
+.results-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+.result-item {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: 0.75rem;
+}
+
+/* ── Checkbox wrapper ────────────────────────────────────── */
+.checkbox-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ font-weight: 500;
+ color: var(--gray-700);
+ font-size: 0.9rem;
+}
+.checkbox-wrapper input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ accent-color: var(--primary);
+}
+
+/* ── Subscribers stat cards small variant ────────────────── */
+.stats-row .stat-card h4 {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--gray-600);
+ margin-bottom: 0.25rem;
+}
+.stats-row .stat-card h2 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: var(--dark);
+ line-height: 1;
+}
+
+/* ── Service card features list ─────────────────────────── */
+.service-features-list {
+ list-style: none;
+ padding: 0;
+ margin: 0.5rem 0 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+.service-features-list li {
+ font-size: 0.8125rem;
+ color: var(--gray-600);
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+.service-features-list li i {
+ color: var(--success, #10b981);
+ font-size: 0.7rem;
+ flex-shrink: 0;
+}
+
+/* ── Responsive additions ────────────────────────────────── */
+@media (max-width: 768px) {
+ .services-admin-grid,
+ .portfolio-grid {
+ grid-template-columns: 1fr;
+ }
+ .editor-grid {
+ grid-template-columns: 1fr;
+ }
+ .result-item {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/Public HTML/admin/dashboard.html b/Public HTML/admin/dashboard.html
new file mode 100755
index 0000000..d1d6e0d
--- /dev/null
+++ b/Public HTML/admin/dashboard.html
@@ -0,0 +1,429 @@
+
+
+
+
+
+
+ Dashboard - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 this week
+
+
+
+
+
+
+
+
+
0
+
Published Articles
+
+
+ 0 drafts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/index.html b/Public HTML/admin/index.html
new file mode 100755
index 0000000..14e8a6c
--- /dev/null
+++ b/Public HTML/admin/index.html
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+ Admin Login - MSPE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sign In
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/js/admin.js b/Public HTML/admin/js/admin.js
new file mode 100755
index 0000000..123899e
--- /dev/null
+++ b/Public HTML/admin/js/admin.js
@@ -0,0 +1,440 @@
+/**
+ * MSPE Admin Panel JavaScript
+ */
+
+// Lock admin UI until auth verification completes (except login/reset pages)
+document.documentElement.classList.add('auth-pending');
+
+const authStyle = document.createElement('style');
+authStyle.textContent = `
+ html.auth-pending body {
+ visibility: hidden;
+ }
+ html.auth-pending::after {
+ content: '';
+ position: fixed;
+ inset: 0;
+ z-index: 99999;
+ background: #0f172a;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ html.auth-pending::before {
+ content: '';
+ position: fixed;
+ top: 50%; left: 50%;
+ z-index: 100000;
+ width: 36px; height: 36px;
+ margin: -18px 0 0 -18px;
+ border: 3px solid rgba(59,130,246,0.2);
+ border-top-color: #3b82f6;
+ border-radius: 50%;
+ animation: authSpin .7s linear infinite;
+ }
+ @keyframes authSpin { to { transform: rotate(360deg) } }
+`;
+document.head.appendChild(authStyle);
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (isAuthExemptPage()) {
+ unlockAdminUi();
+ return;
+ }
+
+ // Initialize components
+ initSidebar();
+ initDropdowns();
+ initLogout();
+
+ checkAuth();
+});
+
+/**
+ * Sidebar Toggle
+ */
+function initSidebar() {
+ const sidebar = document.getElementById('sidebar');
+ const mobileToggle = document.getElementById('mobile-toggle');
+ const sidebarToggle = document.getElementById('sidebar-toggle');
+
+ if (mobileToggle) {
+ mobileToggle.addEventListener('click', function() {
+ sidebar.classList.toggle('active');
+ });
+ }
+
+ if (sidebarToggle) {
+ sidebarToggle.addEventListener('click', function() {
+ sidebar.classList.toggle('active');
+ });
+ }
+
+ // Close sidebar when clicking outside on mobile
+ document.addEventListener('click', function(e) {
+ if (window.innerWidth <= 1024) {
+ if (!sidebar.contains(e.target) && !mobileToggle?.contains(e.target)) {
+ sidebar.classList.remove('active');
+ }
+ }
+ });
+}
+
+/**
+ * Dropdowns
+ */
+function initDropdowns() {
+ const dropdowns = document.querySelectorAll('.user-dropdown');
+
+ dropdowns.forEach(dropdown => {
+ const toggle = dropdown.querySelector('.dropdown-toggle');
+
+ if (toggle) {
+ toggle.addEventListener('click', function(e) {
+ e.stopPropagation();
+ dropdown.classList.toggle('active');
+ });
+ }
+ });
+
+ // Close dropdowns when clicking outside
+ document.addEventListener('click', function() {
+ dropdowns.forEach(dropdown => {
+ dropdown.classList.remove('active');
+ });
+ });
+}
+
+/**
+ * Logout Handler
+ */
+function initLogout() {
+ const logoutBtn = document.getElementById('logout-btn');
+
+ if (logoutBtn) {
+ logoutBtn.addEventListener('click', async function(e) {
+ e.preventDefault();
+
+ try {
+ await fetch('/api/auth.php', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ action: 'logout' })
+ });
+ } catch (error) {
+ console.log('Logout request failed, proceeding with local logout');
+ }
+
+ // Redirect to login
+ window.location.href = 'index.html';
+ });
+ }
+}
+
+/**
+ * Check Authentication
+ */
+function checkAuth() {
+ fetch('/api/auth.php', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ action: 'verify' })
+ })
+ .then(r => r.json())
+ .then(data => {
+ if (data.success) {
+ applyUserToHeader(data.user || {});
+ unlockAdminUi();
+ startSessionWatchdog();
+ } else {
+ window.location.href = 'index.html';
+ }
+ })
+ .catch(() => {
+ window.location.href = 'index.html';
+ });
+}
+
+function isAuthExemptPage() {
+ return window.location.pathname.includes('index.html') ||
+ window.location.pathname.endsWith('/admin/') ||
+ window.location.pathname.includes('reset-password.html');
+}
+
+function unlockAdminUi() {
+ document.documentElement.classList.remove('auth-pending');
+}
+
+/**
+ * Periodic Session Re-Verification (every 5 minutes)
+ * Catches expired / revoked sessions proactively.
+ */
+let _sessionInterval = null;
+function startSessionWatchdog() {
+ if (_sessionInterval) return;
+ _sessionInterval = setInterval(() => {
+ fetch('/api/auth.php', {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action: 'verify' })
+ })
+ .then(r => r.json())
+ .then(data => {
+ if (!data.success) {
+ clearInterval(_sessionInterval);
+ showNotification('Session expired — redirecting to login.', 'warning');
+ setTimeout(() => { window.location.href = 'index.html'; }, 2000);
+ }
+ })
+ .catch(() => { /* network hiccup, retry next cycle */ });
+ }, 5 * 60 * 1000);
+}
+
+function applyUserToHeader(user) {
+ const nameSpans = document.querySelectorAll('.user-name');
+ nameSpans.forEach(el => {
+ el.textContent = user.username || 'Admin';
+ });
+}
+
+/**
+ * API Helper Functions
+ */
+const API = {
+ baseUrl: '/api',
+
+ async request(endpoint, options = {}) {
+ const defaultOptions = {
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ };
+
+ const response = await fetch(`${this.baseUrl}/${endpoint}`, {
+ ...defaultOptions,
+ ...options,
+ headers: {
+ ...defaultOptions.headers,
+ ...options.headers
+ }
+ });
+
+ if (response.status === 401) {
+ window.location.href = 'index.html';
+ return;
+ }
+
+ if (response.status === 403) {
+ return response.json();
+ }
+
+ return response.json();
+ },
+
+ async get(endpoint) {
+ return this.request(endpoint);
+ },
+
+ async post(endpoint, data) {
+ return this.request(endpoint, {
+ method: 'POST',
+ body: JSON.stringify(data)
+ });
+ },
+
+ async put(endpoint, data) {
+ return this.request(endpoint, {
+ method: 'PUT',
+ body: JSON.stringify(data)
+ });
+ },
+
+ async delete(endpoint) {
+ return this.request(endpoint, {
+ method: 'DELETE'
+ });
+ },
+
+ async upload(endpoint, formData) {
+ const response = await fetch(`${this.baseUrl}/${endpoint}`, {
+ method: 'POST',
+ credentials: 'include',
+ body: formData
+ });
+
+ if (response.status === 401) {
+ window.location.href = 'index.html';
+ return;
+ }
+
+ return response.json();
+ }
+};
+
+/**
+ * Notification Helper
+ */
+function showNotification(message, type = 'success') {
+ const iconMap = {
+ success: 'check-circle',
+ error: 'exclamation-circle',
+ info: 'info-circle',
+ warning: 'exclamation-triangle'
+ };
+ const icon = iconMap[type] || 'info-circle';
+
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+
+ const iconEl = document.createElement('i');
+ iconEl.className = `fas fa-${icon}`;
+ const spanEl = document.createElement('span');
+ spanEl.textContent = message;
+ notification.appendChild(iconEl);
+ notification.appendChild(spanEl);
+
+ // Add styles if not present
+ if (!document.querySelector('.notification-styles')) {
+ const styles = document.createElement('style');
+ styles.className = 'notification-styles';
+ styles.textContent = `
+ .notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 1rem 1.5rem;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-weight: 500;
+ z-index: 9999;
+ animation: slideIn 0.3s ease;
+ box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
+ }
+ .notification.success {
+ background: #10b981;
+ color: white;
+ }
+ .notification.error {
+ background: #ef4444;
+ color: white;
+ }
+ .notification.info {
+ background: #3b82f6;
+ color: white;
+ }
+ .notification.warning {
+ background: #f59e0b;
+ color: white;
+ }
+ @keyframes slideIn {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+ `;
+ document.head.appendChild(styles);
+ }
+
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.style.animation = 'slideIn 0.3s ease reverse';
+ setTimeout(() => notification.remove(), 300);
+ }, 3000);
+}
+
+/**
+ * Format Date Helper
+ */
+function formatDate(dateString) {
+ const options = { year: 'numeric', month: 'short', day: 'numeric' };
+ return new Date(dateString).toLocaleDateString('en-US', options);
+}
+
+/**
+ * Confirm Dialog Helper
+ */
+function confirmDialog(message) {
+ return new Promise((resolve) => {
+ const modal = document.createElement('div');
+ modal.className = 'modal active';
+ modal.innerHTML = `
+
+
+ `;
+ modal.querySelector('.confirm-msg').textContent = message;
+
+ document.body.appendChild(modal);
+
+ modal.querySelector('#confirm-ok').addEventListener('click', () => {
+ modal.remove();
+ resolve(true);
+ });
+
+ modal.querySelector('#confirm-cancel').addEventListener('click', () => {
+ modal.remove();
+ resolve(false);
+ });
+
+ modal.querySelector('.modal-close').addEventListener('click', () => {
+ modal.remove();
+ resolve(false);
+ });
+
+ modal.querySelector('.modal-overlay').addEventListener('click', () => {
+ modal.remove();
+ resolve(false);
+ });
+ });
+}
+
+/**
+ * Debounce Helper
+ */
+function debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
+
+// Export for use in other scripts
+window.MSPE = {
+ API,
+ showNotification,
+ formatDate,
+ confirmDialog,
+ debounce
+};
diff --git a/Public HTML/admin/media.html b/Public HTML/admin/media.html
new file mode 100755
index 0000000..4826cfa
--- /dev/null
+++ b/Public HTML/admin/media.html
@@ -0,0 +1,731 @@
+
+
+
+
+
+
+ Media Library - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 selected
+
+ Move
+
+
+ Download
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Drag & Drop Files Here
+
or click to browse
+
Supports: JPG, PNG, GIF, PDF, DOC, MP4 (Max 50MB each)
+
+
+
+
+ Upload to Folder
+
+ Root (uploads/)
+ news/
+ portfolio/
+ team/
+ clients/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/messages.html b/Public HTML/admin/messages.html
new file mode 100755
index 0000000..2407732
--- /dev/null
+++ b/Public HTML/admin/messages.html
@@ -0,0 +1,741 @@
+
+
+
+
+
+
+ Messages - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All Messages
+ Unread
+ Read
+
+
+
+
+
+
+
Loading messages...
+
+
+
+
+
+
+
+
Select a message to view details
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/news.html b/Public HTML/admin/news.html
new file mode 100755
index 0000000..ad4eb72
--- /dev/null
+++ b/Public HTML/admin/news.html
@@ -0,0 +1,708 @@
+
+
+
+
+
+
+ News Management - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete this article? This action cannot be undone.
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/pages.html b/Public HTML/admin/pages.html
new file mode 100755
index 0000000..f94c4e4
--- /dev/null
+++ b/Public HTML/admin/pages.html
@@ -0,0 +1,512 @@
+
+
+
+
+
+
+ Pages Management - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SEO & Metadata
+
+
+ Meta Title
+
+ Recommended: 50–60 characters
+
+
+ Meta Description
+
+ Recommended: 150–160 characters
+
+
+ Status
+
+ Published
+ Draft
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/portfolio.html b/Public HTML/admin/portfolio.html
new file mode 100755
index 0000000..0f3125b
--- /dev/null
+++ b/Public HTML/admin/portfolio.html
@@ -0,0 +1,708 @@
+
+
+
+
+
+
+ Portfolio Management - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading projects...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Title
+
+
+
+
+ Short Description
+
+
+
+
+ Full Description
+
+
+
+
+
+
+ Technologies Used
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/reset-password.html b/Public HTML/admin/reset-password.html
new file mode 100755
index 0000000..a66916d
--- /dev/null
+++ b/Public HTML/admin/reset-password.html
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+ Reset Password - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Send Reset Link
+
+
+
+
+
+
+
+
+ Update Password
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/services.html b/Public HTML/admin/services.html
new file mode 100755
index 0000000..d70328d
--- /dev/null
+++ b/Public HTML/admin/services.html
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+ Services Management - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading services...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Short Description
+
+
+
+
+ Features (one per line)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/settings.html b/Public HTML/admin/settings.html
new file mode 100755
index 0000000..ca01fc3
--- /dev/null
+++ b/Public HTML/admin/settings.html
@@ -0,0 +1,910 @@
+
+
+
+
+
+
+ Site Settings - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ General
+
+
+ Contact Info
+
+
+ Social Media
+
+
+ SEO
+
+
+ Appearance
+
+
+ Email & Auth
+
+
+ Integrations
+
+
+
+
+
+
+
+
+
+ Site Name
+
+
+
+
+ Site Tagline
+
+
+
+
+ Site Description
+ MSPE transforms complexity into competitive advantage through innovative technology solutions and uncompromising security.
+
+
+
+
+
+ Timezone
+
+ UTC
+ Eastern Time (US)
+ Central Time (US)
+ Mountain Time (US)
+ Pacific Time (US)
+ London
+ Paris
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Address
+
+
+
+
+
+
+ Google Maps Embed URL
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Default Meta Title
+
+ Recommended: 50-60 characters
+
+
+
+ Default Meta Description
+ MSPE delivers enterprise-grade IT support, cybersecurity, and cloud solutions. Transform complexity into competitive advantage.
+ Recommended: 150-160 characters
+
+
+
+ Meta Keywords
+
+
+
+
+ Google Analytics ID
+
+
+
+
+ Google Tag Manager ID
+
+
+
+
+ Robots.txt Content
+ User-agent: *
+Allow: /
+Sitemap: https://mspe.pro/sitemap.xml
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Font Family
+
+ DM Sans
+ Inter
+ Poppins
+ Roboto
+ Open Sans
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SMTP Password
+
+ Stored in site settings. Use a dedicated mailbox/app password.
+
+
+
+ Password Reset Page URL
+
+ Reset tokens will be appended as ?token=...
+
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Google reCAPTCHA
+
Protect forms from spam
+
+
+
+
+
+
+
+
+
+
+
+
+
Mailchimp
+
Email marketing integration
+
+
+
+
+
+
+
+
+
+
+
+
+
Slack Notifications
+
Get notified of new messages
+
+
+
+
+
+
+
+
+
+
+
+
+
Calendly
+
Booking integration
+
+
+
+
+
+
+
+
+
+
Calendly Settings
+
+ Calendly URL
+
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/subscribers.html b/Public HTML/admin/subscribers.html
new file mode 100755
index 0000000..90e1dba
--- /dev/null
+++ b/Public HTML/admin/subscribers.html
@@ -0,0 +1,531 @@
+
+
+
+
+
+
+ Subscribers - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total Subscribers
+
0
+
+
+
Active Subscribers
+
0
+
+
+
+
+
+
+
+
+
+
+ Name
+ Email
+ Subscribed
+ Status
+ Actions
+
+
+
+
+
+
+
+
Loading subscribers...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/team.html b/Public HTML/admin/team.html
new file mode 100755
index 0000000..23aab8a
--- /dev/null
+++ b/Public HTML/admin/team.html
@@ -0,0 +1,508 @@
+
+
+
+
+
+
+ Team Management - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading team members...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bio
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/testimonials.html b/Public HTML/admin/testimonials.html
new file mode 100755
index 0000000..96a7980
--- /dev/null
+++ b/Public HTML/admin/testimonials.html
@@ -0,0 +1,546 @@
+
+
+
+
+
+
+ Testimonials - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ Total Testimonials
+
+
+
+
+
+
+
+ 0
+ Published
+
+
+
+
+
+
+
+ 0
+ Avg Rating
+
+
+
+
+
+
+
+ 0
+ Pending Review
+
+
+
+
+
+
+
+
+
Loading testimonials...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Testimonial Text *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/admin/users.html b/Public HTML/admin/users.html
new file mode 100755
index 0000000..8b756ca
--- /dev/null
+++ b/Public HTML/admin/users.html
@@ -0,0 +1,603 @@
+
+
+
+
+
+
+ Admin Users - MSPE Admin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User
+ Email
+ Role
+ Last Login
+ Status
+ Actions
+
+
+
+
+
+ Loading users...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Full system access
+ Manage all users
+ Site settings
+ Delete content
+ View analytics
+
+
+
+
+
+
+ Create/edit content
+ Publish content
+ Manage media
+ View messages
+ Site settings
+
+
+
+
+
+
+ Create content
+ Publish content
+ Upload media
+ Delete content
+ Site settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Super Admin logged in
+
10 minutes ago
+
+
+
+
+
+
+
+
+
Sarah Johnson edited "New IT Security Features"
+
2 hours ago
+
+
+
+
+
+
+
+
+
Sarah Johnson created new news article
+
Yesterday
+
+
+
+
+
+
+
+
+
Super Admin updated site settings
+
2 days ago
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Public HTML/api/.htaccess b/Public HTML/api/.htaccess
new file mode 100755
index 0000000..152cde5
--- /dev/null
+++ b/Public HTML/api/.htaccess
@@ -0,0 +1,10 @@
+# Block dev/test/seed scripts in production
+
+
+ Require all denied
+
+
+ Order deny,allow
+ Deny from all
+
+
diff --git a/Public HTML/api/auth.php b/Public HTML/api/auth.php
new file mode 100755
index 0000000..b9d66cc
--- /dev/null
+++ b/Public HTML/api/auth.php
@@ -0,0 +1,391 @@
+ false, 'message' => 'Invalid action'], 400);
+ }
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function handleLogin($body) {
+ global $db;
+
+ $username = $body['username'] ?? '';
+ $password = $body['password'] ?? '';
+ $rememberMe = !empty($body['remember']);
+
+ if (empty($username) || empty($password)) {
+ jsonResponse(['success' => false, 'message' => 'Username and password required'], 400);
+ }
+
+ // ── Rate limiting: max 10 attempts per IP per 15 minutes (DB-backed) ──
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+ $maxAttempts = 10;
+ $windowSeconds = 900; // 15 minutes
+ if (countRecentFailedLogins($ip, $windowSeconds) >= $maxAttempts) {
+ jsonResponse(['success' => false, 'message' => 'Too many login attempts. Try again later.'], 429);
+ }
+
+ // ── Per-username rate limiting: max 5 attempts per username per 15 minutes ──
+ $maxUsernameAttempts = 5;
+ if (countRecentFailedLoginsByUsername($username, $windowSeconds) >= $maxUsernameAttempts) {
+ jsonResponse(['success' => false, 'message' => 'This account is temporarily locked. Try again later.'], 429);
+ }
+
+ // Check override admin auth record first (if created via reset flow)
+ $adminAuthRows = $db->getAll('admin_auth');
+ $adminAuth = !empty($adminAuthRows) ? $adminAuthRows[0] : null;
+
+ if ($adminAuth && isset($adminAuth['username']) && isset($adminAuth['password_hash'])) {
+ if ($username === $adminAuth['username'] && password_verify($password, $adminAuth['password_hash'])) {
+ loginSuccessResponse('admin', $adminAuth['username'], 'admin', $rememberMe);
+ }
+ } else {
+ // Fallback to config credentials (ADMIN_PASS must be a password hash)
+ if ($username === ADMIN_USER && verifyConfigAdminPassword($password)) {
+ loginSuccessResponse('admin', $username, 'admin', $rememberMe);
+ }
+ }
+
+ // Check against database users
+ $users = $db->query('users', ['username' => $username]);
+
+ if (!empty($users)) {
+ $user = array_values($users)[0];
+
+ if (password_verify($password, $user['password'])) {
+ loginSuccessResponse($user['id'], $user['username'], $user['role'] ?? 'admin', $rememberMe);
+ }
+ }
+
+ recordFailedLogin();
+ auditLog('login_failure', ['username' => $username]);
+ jsonResponse(['success' => false, 'message' => 'Invalid credentials'], 401);
+}
+
+function recordFailedLogin() {
+ global $db;
+
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+ $body = getRequestBody();
+ $db->insert('login_attempts', [
+ 'ip_address' => $ip,
+ 'username' => sanitize($body['username'] ?? ''),
+ 'attempted_at' => date('Y-m-d H:i:s')
+ ]);
+
+ cleanupOldFailedLogins();
+}
+
+function countRecentFailedLogins($ip, $windowSeconds) {
+ global $db;
+
+ $cutoffDate = date('Y-m-d H:i:s', time() - (int)$windowSeconds);
+ $now = date('Y-m-d H:i:s');
+
+ return $db->countByDateRange('login_attempts', 'attempted_at', $cutoffDate, $now, [
+ 'ip_address' => $ip
+ ]);
+}
+
+function countRecentFailedLoginsByUsername($username, $windowSeconds) {
+ global $db;
+
+ if (trim($username) === '') return 0;
+
+ $cutoffDate = date('Y-m-d H:i:s', time() - (int)$windowSeconds);
+ $now = date('Y-m-d H:i:s');
+
+ return $db->countByDateRange('login_attempts', 'attempted_at', $cutoffDate, $now, [
+ 'username' => $username
+ ]);
+}
+
+function cleanupOldFailedLogins() {
+ global $db;
+
+ $attempts = $db->getAll('login_attempts');
+ $cutoff = time() - (24 * 60 * 60);
+
+ foreach ($attempts as $attempt) {
+ $attemptedAt = strtotime((string)($attempt['attempted_at'] ?? '1970-01-01 00:00:00'));
+ if ($attemptedAt < $cutoff && !empty($attempt['id'])) {
+ $db->delete('login_attempts', $attempt['id']);
+ }
+ }
+}
+
+function handleLogout() {
+ clearAdminAuthCookie();
+ jsonResponse(['success' => true, 'message' => 'Logged out successfully']);
+}
+
+function handleVerify() {
+ $user = checkAuth();
+
+ if ($user) {
+ jsonResponse([
+ 'success' => true,
+ 'user' => [
+ 'id' => $user['user_id'],
+ 'username' => $user['username'],
+ 'role' => $user['role']
+ ]
+ ]);
+ }
+
+ jsonResponse(['success' => false, 'message' => 'Invalid token'], 401);
+}
+
+function loginSuccessResponse($id, $username, $role, $rememberMe = false) {
+ $ttlSeconds = $rememberMe ? (30 * 24 * 60 * 60) : (12 * 60 * 60);
+ $token = JWT::encode([
+ 'user_id' => $id,
+ 'username' => $username,
+ 'role' => $role
+ ], $ttlSeconds);
+
+ setAdminAuthCookie($token, $ttlSeconds);
+
+ auditLog('login_success', ['username' => $username, 'role' => $role, 'remember_me' => $rememberMe], $id);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Login successful',
+ 'user' => [
+ 'id' => $id,
+ 'username' => $username,
+ 'role' => $role
+ ]
+ ]);
+}
+
+function verifyConfigAdminPassword($submittedPassword) {
+ if (!is_string(ADMIN_PASS) || ADMIN_PASS === '') {
+ return false;
+ }
+
+ $info = password_get_info(ADMIN_PASS);
+ if (!empty($info['algo'])) {
+ return password_verify($submittedPassword, ADMIN_PASS);
+ }
+
+ error_log('MSPE security warning: ADMIN_PASS is not a password hash. Configure a password_hash() value in .env.');
+ return false;
+}
+
+function setAdminAuthCookie($token, $ttlSeconds) {
+ $isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+ || (($_SERVER['SERVER_PORT'] ?? '') == 443)
+ || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
+
+ setcookie('mspe_admin_token', $token, [
+ 'expires' => time() + max(300, (int)$ttlSeconds),
+ 'path' => '/',
+ 'secure' => $isHttps,
+ 'httponly' => true,
+ 'samesite' => 'Strict'
+ ]);
+}
+
+function clearAdminAuthCookie() {
+ $isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+ || (($_SERVER['SERVER_PORT'] ?? '') == 443)
+ || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
+
+ setcookie('mspe_admin_token', '', [
+ 'expires' => time() - 3600,
+ 'path' => '/',
+ 'secure' => $isHttps,
+ 'httponly' => true,
+ 'samesite' => 'Strict'
+ ]);
+}
+
+function requestPasswordReset($body) {
+ global $db;
+
+ cleanupExpiredPasswordResets();
+
+ $identity = trim((string)($body['identity'] ?? $body['email'] ?? ''));
+ if ($identity === '') {
+ jsonResponse(['success' => false, 'message' => 'Email or username is required'], 400);
+ }
+
+ $matched = findUserForPasswordReset($identity);
+
+ // Always return generic success to avoid username/email enumeration
+ if (!$matched) {
+ jsonResponse(['success' => true, 'message' => 'If an account exists, a reset email has been sent.']);
+ }
+
+ $token = bin2hex(random_bytes(32));
+ $expiresAt = date('Y-m-d H:i:s', time() + (30 * 60));
+
+ $db->insert('password_resets', [
+ 'token' => $token,
+ 'user_type' => $matched['user_type'],
+ 'user_id' => $matched['user_id'],
+ 'email' => $matched['email'],
+ 'used' => false,
+ 'expires_at' => $expiresAt
+ ]);
+
+ $resetUrl = trim((string)getSetting('password_reset_url', SITE_URL . '/admin/reset-password.html'));
+ if ($resetUrl === '') {
+ $resetUrl = SITE_URL . '/admin/reset-password.html';
+ }
+ $separator = strpos($resetUrl, '?') === false ? '?' : '&';
+ $resetLink = $resetUrl . $separator . 'token=' . urlencode($token);
+
+ $subject = 'Reset your admin password';
+ $plain = "A password reset was requested for your admin account.\n\n";
+ $plain .= "Reset link (valid for 30 minutes):\n{$resetLink}\n\n";
+ $plain .= "If you did not request this, you can ignore this email.";
+
+ $html = 'Password Reset '
+ . 'A password reset was requested for your admin account.
'
+ . 'Reset Password
'
+ . 'This link is valid for 30 minutes.
';
+
+ $send = sendEmail($matched['email'], $subject, $html, $plain);
+
+ if (!$send['success']) {
+ error_log('MSPE password reset email failed: ' . ($send['message'] ?? 'unknown'));
+ }
+
+ jsonResponse(['success' => true, 'message' => 'If an account exists, a reset email has been sent.']);
+}
+
+function resetPassword($body) {
+ global $db;
+
+ cleanupExpiredPasswordResets();
+
+ $token = trim((string)($body['token'] ?? ''));
+ $newPassword = (string)($body['new_password'] ?? '');
+
+ if ($token === '' || $newPassword === '') {
+ jsonResponse(['success' => false, 'message' => 'Token and new password are required'], 400);
+ }
+
+ if (strlen($newPassword) < 12) {
+ jsonResponse(['success' => false, 'message' => 'Password must be at least 12 characters'], 400);
+ }
+
+ $resets = $db->query('password_resets', ['token' => $token]);
+ if (empty($resets)) {
+ jsonResponse(['success' => false, 'message' => 'Invalid or expired reset token'], 400);
+ }
+
+ $reset = array_values($resets)[0];
+ if (!empty($reset['used'])) {
+ jsonResponse(['success' => false, 'message' => 'This reset token has already been used'], 400);
+ }
+
+ if (strtotime($reset['expires_at'] ?? '1970-01-01 00:00:00') < time()) {
+ jsonResponse(['success' => false, 'message' => 'Reset token has expired'], 400);
+ }
+
+ $hash = password_hash($newPassword, PASSWORD_DEFAULT);
+
+ if (($reset['user_type'] ?? '') === 'default_admin') {
+ $existingRows = $db->getAll('admin_auth');
+ if (!empty($existingRows)) {
+ $first = $existingRows[0];
+ $db->update('admin_auth', $first['id'], [
+ 'username' => ADMIN_USER,
+ 'email' => $reset['email'] ?? getSetting('admin_email', ADMIN_EMAIL),
+ 'password_hash' => $hash
+ ]);
+ } else {
+ $db->insert('admin_auth', [
+ 'username' => ADMIN_USER,
+ 'email' => $reset['email'] ?? getSetting('admin_email', ADMIN_EMAIL),
+ 'password_hash' => $hash
+ ]);
+ }
+ } else {
+ $userId = $reset['user_id'] ?? '';
+ $user = $db->get('users', $userId);
+ if (!$user) {
+ jsonResponse(['success' => false, 'message' => 'User no longer exists'], 400);
+ }
+ $db->update('users', $userId, ['password' => $hash]);
+ }
+
+ $db->update('password_resets', $reset['id'], ['used' => true]);
+
+ jsonResponse(['success' => true, 'message' => 'Password updated successfully']);
+}
+
+function findUserForPasswordReset($identity) {
+ global $db;
+
+ // Check DB users by email or username
+ $users = $db->getAll('users');
+ foreach ($users as $user) {
+ if (($user['email'] ?? '') === $identity || ($user['username'] ?? '') === $identity) {
+ return [
+ 'user_type' => 'db_user',
+ 'user_id' => $user['id'],
+ 'email' => $user['email']
+ ];
+ }
+ }
+
+ // Fallback default admin account
+ if ($identity === ADMIN_USER || $identity === ADMIN_EMAIL || $identity === getSetting('admin_email', ADMIN_EMAIL)) {
+ return [
+ 'user_type' => 'default_admin',
+ 'user_id' => 'admin',
+ 'email' => getSetting('admin_email', ADMIN_EMAIL)
+ ];
+ }
+
+ return null;
+}
+
+function cleanupExpiredPasswordResets() {
+ global $db;
+
+ $rows = $db->getAll('password_resets');
+ $now = time();
+ $retentionCutoff = $now - (30 * 24 * 60 * 60);
+
+ foreach ($rows as $row) {
+ $expiresAt = strtotime((string)($row['expires_at'] ?? '1970-01-01 00:00:00'));
+ $createdAt = strtotime((string)($row['created_at'] ?? '1970-01-01 00:00:00'));
+ $isUsed = !empty($row['used']);
+
+ if (($expiresAt > 0 && $expiresAt < $now) || ($isUsed && $createdAt < $retentionCutoff)) {
+ if (!empty($row['id'])) {
+ $db->delete('password_resets', $row['id']);
+ }
+ }
+ }
+}
diff --git a/Public HTML/api/bookings.php b/Public HTML/api/bookings.php
new file mode 100755
index 0000000..e9dab51
--- /dev/null
+++ b/Public HTML/api/bookings.php
@@ -0,0 +1,605 @@
+ false, 'message' => 'Booking ID required'], 400);
+ }
+ updateBooking($id);
+ break;
+
+ case 'DELETE':
+ requireAuth();
+ if (!$id) {
+ jsonResponse(['success' => false, 'message' => 'ID required'], 400);
+ }
+ $type = $_GET['type'] ?? 'booking';
+ if ($type === 'blocked') {
+ deleteBlockedTime($id);
+ } else {
+ deleteBooking($id);
+ }
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function getBookings() {
+ global $db;
+
+ requireAuth();
+
+ $bookings = $db->getAll('bookings');
+
+ $status = $_GET['status'] ?? null;
+ $limit = (int)($_GET['limit'] ?? 50);
+ $offset = (int)($_GET['offset'] ?? 0);
+ $dateFrom = $_GET['date_from'] ?? null;
+ $dateTo = $_GET['date_to'] ?? null;
+
+ if ($status) {
+ $bookings = array_filter($bookings, function($b) use ($status) {
+ return $b['status'] === $status;
+ });
+ }
+
+ if ($dateFrom) {
+ $bookings = array_filter($bookings, function($b) use ($dateFrom) {
+ return $b['booking_date'] >= $dateFrom;
+ });
+ }
+
+ if ($dateTo) {
+ $bookings = array_filter($bookings, function($b) use ($dateTo) {
+ return $b['booking_date'] <= $dateTo;
+ });
+ }
+
+ usort($bookings, function($a, $b) {
+ return strtotime($a['booking_date'] . ' ' . $a['booking_time']) - strtotime($b['booking_date'] . ' ' . $b['booking_time']);
+ });
+
+ $total = count($bookings);
+ $bookings = array_slice(array_values($bookings), $offset, $limit);
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $bookings,
+ 'total' => $total,
+ 'limit' => $limit,
+ 'offset' => $offset
+ ]);
+}
+
+function getBooking($id) {
+ global $db;
+
+ requireAuth();
+
+ $booking = $db->get('bookings', $id);
+
+ if (!$booking) {
+ jsonResponse(['success' => false, 'message' => 'Booking not found'], 404);
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $booking
+ ]);
+}
+
+function getAvailability() {
+ global $db;
+
+ $slots = $db->getAll('availability_slots');
+ $bookings = $db->getAll('bookings');
+
+ $dateFrom = $_GET['date_from'] ?? date('Y-m-d');
+ $dateTo = $_GET['date_to'] ?? date('Y-m-d', strtotime('+30 days'));
+
+ $slots = array_filter($slots, function($slot) use ($dateFrom, $dateTo) {
+ return $slot['date'] >= $dateFrom && $slot['date'] <= $dateTo && $slot['is_available'];
+ });
+
+ $result = [];
+
+ foreach ($slots as $slot) {
+ $slotBookings = array_filter($bookings, function($booking) use ($slot) {
+ return $booking['booking_date'] === $slot['date'] &&
+ $booking['booking_time'] === $slot['time'] &&
+ in_array($booking['status'], ['confirmed', 'pending']);
+ });
+
+ $slot['booked'] = count($slotBookings) > 0;
+ $slot['remaining'] = $slot['capacity'] - count($slotBookings);
+
+ $result[] = $slot;
+ }
+
+ usort($result, function($a, $b) {
+ return strtotime($a['date'] . ' ' . $a['time']) - strtotime($b['date'] . ' ' . $b['time']);
+ });
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $result
+ ]);
+}
+
+/**
+ * Get public availability - shows only available/not available status
+ * Does NOT expose any booking details, client info, or internal notes
+ * This is specifically for the public calendar
+ */
+function getPublicAvailability() {
+ global $db;
+
+ $slots = $db->getAll('availability_slots');
+ $bookings = $db->getAll('bookings');
+ $blockedTimes = $db->getAll('blocked_times');
+
+ $dateFrom = $_GET['date_from'] ?? date('Y-m-d');
+ $dateTo = $_GET['date_to'] ?? date('Y-m-d', strtotime('+60 days'));
+
+ // Filter slots within date range that are marked as available
+ $slots = array_filter($slots, function($slot) use ($dateFrom, $dateTo) {
+ return $slot['date'] >= $dateFrom && $slot['date'] <= $dateTo && $slot['is_available'];
+ });
+
+ $result = [];
+
+ foreach ($slots as $slot) {
+ // Count active bookings for this slot
+ $slotBookings = array_filter($bookings, function($booking) use ($slot) {
+ return $booking['booking_date'] === $slot['date'] &&
+ $booking['booking_time'] === $slot['time'] &&
+ in_array($booking['status'], ['confirmed', 'pending']);
+ });
+
+ // Check if time is blocked by admin
+ $isBlocked = false;
+ foreach ($blockedTimes as $blocked) {
+ if ($blocked['date'] === $slot['date'] && $blocked['time'] === $slot['time']) {
+ $isBlocked = true;
+ break;
+ }
+ }
+
+ $bookedCount = count($slotBookings);
+ $isFullyBooked = $bookedCount >= ($slot['capacity'] ?? 1);
+
+ // Only return minimal info - just what the public needs
+ $result[] = [
+ 'date' => $slot['date'],
+ 'time' => $slot['time'],
+ 'available' => !$isFullyBooked && !$isBlocked,
+ // Don't expose: capacity, remaining, notes, booking details, etc.
+ ];
+ }
+
+ usort($result, function($a, $b) {
+ return strtotime($a['date'] . ' ' . $a['time']) - strtotime($b['date'] . ' ' . $b['time']);
+ });
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $result
+ ]);
+}
+
+function getUpcomingBookings() {
+ global $db;
+
+ requireAuth();
+
+ $bookings = $db->getAll('bookings');
+ $today = date('Y-m-d');
+
+ $upcoming = array_filter($bookings, function($b) use ($today) {
+ return $b['booking_date'] >= $today && in_array($b['status'], ['confirmed', 'pending']);
+ });
+
+ usort($upcoming, function($a, $b) {
+ return strtotime($a['booking_date'] . ' ' . $a['booking_time']) - strtotime($b['booking_date'] . ' ' . $b['booking_time']);
+ });
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => array_slice(array_values($upcoming), 0, 10)
+ ]);
+}
+
+/**
+ * Block a time slot (admin only)
+ * This makes a specific date/time unavailable without creating a booking
+ */
+function blockTime() {
+ global $db;
+
+ $data = getRequestBody();
+
+ if (empty($data['date']) || empty($data['time'])) {
+ jsonResponse(['success' => false, 'message' => 'Date and time are required'], 400);
+ }
+
+ $blockData = [
+ 'date' => sanitize($data['date']),
+ 'time' => sanitize($data['time']),
+ 'reason' => sanitize($data['reason'] ?? ''),
+ 'blocked_by' => 'admin',
+ 'created_at' => date('Y-m-d H:i:s')
+ ];
+
+ $blocked = $db->insert('blocked_times', $blockData);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Time blocked successfully',
+ 'data' => $blocked
+ ], 201);
+}
+
+/**
+ * Get all blocked times (admin only)
+ */
+function getBlockedTimes() {
+ global $db;
+
+ $blockedTimes = $db->getAll('blocked_times');
+
+ // Sort by date and time
+ usort($blockedTimes, function($a, $b) {
+ return strtotime($a['date'] . ' ' . $a['time']) - strtotime($b['date'] . ' ' . $b['time']);
+ });
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $blockedTimes
+ ]);
+}
+
+/**
+ * Delete a blocked time (admin only)
+ */
+function deleteBlockedTime($id) {
+ global $db;
+
+ $existing = $db->get('blocked_times', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Blocked time not found'], 404);
+ }
+
+ $db->delete('blocked_times', $id);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Blocked time removed successfully'
+ ]);
+}
+
+function createAvailabilitySlot() {
+ global $db;
+
+ $data = getRequestBody();
+
+ if (empty($data['date']) || empty($data['time'])) {
+ jsonResponse(['success' => false, 'message' => 'Date and time are required'], 400);
+ }
+
+ $slotData = [
+ 'date' => sanitize($data['date']),
+ 'time' => sanitize($data['time']),
+ 'capacity' => (int)($data['capacity'] ?? 1),
+ 'is_available' => true,
+ 'notes' => sanitize($data['notes'] ?? '')
+ ];
+
+ $slot = $db->insert('availability_slots', $slotData);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Availability slot created',
+ 'data' => $slot
+ ], 201);
+}
+
+function createBooking($isPublicRequest = false) {
+ global $db;
+
+ if ($isPublicRequest) {
+ requireSameOriginRequest();
+
+ // Rate limiting: max 5 booking requests per IP per hour
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+ $oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour'));
+ $now = date('Y-m-d H:i:s');
+ $recentCount = $db->countByDateRange('bookings', 'created_at', $oneHourAgo, $now, [
+ 'ip_address' => $ip
+ ]);
+ if ($recentCount >= 5) {
+ jsonResponse(['success' => false, 'message' => 'Too many booking requests. Please try again later.'], 429);
+ }
+ }
+
+ $data = getRequestBody();
+
+ $required = ['first_name', 'last_name', 'email', 'booking_date', 'booking_time'];
+ foreach ($required as $field) {
+ if (empty($data[$field])) {
+ jsonResponse(['success' => false, 'message' => ucfirst(str_replace('_', ' ', $field)) . ' is required'], 400);
+ }
+ }
+
+ if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
+ jsonResponse(['success' => false, 'message' => 'Invalid email address'], 400);
+ }
+
+ $bookingDate = date('Y-m-d', strtotime($data['booking_date']));
+ $today = date('Y-m-d');
+
+ if ($bookingDate < $today) {
+ jsonResponse(['success' => false, 'message' => 'Cannot book appointments in the past'], 400);
+ }
+
+ $slots = $db->query('availability_slots', [
+ 'date' => $bookingDate,
+ 'time' => $data['booking_time']
+ ]);
+
+ if (empty($slots) || !$slots[array_key_first($slots)]['is_available']) {
+ jsonResponse(['success' => false, 'message' => 'This time slot is not available'], 400);
+ }
+
+ $slot = array_values($slots)[0];
+ $bookings = $db->query('bookings', [
+ 'booking_date' => $bookingDate,
+ 'booking_time' => $data['booking_time']
+ ]);
+
+ $activeBookings = array_filter($bookings, function($b) {
+ return in_array($b['status'], ['confirmed', 'pending']);
+ });
+
+ if (count($activeBookings) >= $slot['capacity']) {
+ jsonResponse(['success' => false, 'message' => 'This time slot is fully booked'], 400);
+ }
+
+ $bookingData = [
+ 'first_name' => sanitize($data['first_name']),
+ 'last_name' => sanitize($data['last_name']),
+ 'email' => sanitize($data['email']),
+ 'phone' => sanitize($data['phone'] ?? ''),
+ 'company' => sanitize($data['company'] ?? ''),
+ 'service_interest' => sanitize($data['service_interest'] ?? ''),
+ 'booking_date' => $bookingDate,
+ 'booking_time' => sanitize($data['booking_time']),
+ 'duration' => (int)($data['duration'] ?? 60),
+ 'message' => sanitize($data['message'] ?? ''),
+ 'status' => 'pending',
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? ''
+ ];
+
+ $booking = $db->insert('bookings', $bookingData);
+
+ sendBookingConfirmationEmail($bookingData);
+
+ sendAdminBookingNotification($bookingData);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Booking request submitted successfully. We will confirm your appointment shortly.',
+ 'data' => [
+ 'id' => $booking['id'],
+ 'date' => $booking['booking_date'],
+ 'time' => $booking['booking_time'],
+ 'status' => 'pending'
+ ]
+ ], 201);
+}
+
+function updateBooking($id) {
+ global $db;
+
+ $existing = $db->get('bookings', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Booking not found'], 404);
+ }
+
+ $data = getRequestBody();
+ $allowed = ['status', 'notes'];
+
+ $updateData = [];
+ foreach ($allowed as $field) {
+ if (isset($data[$field])) {
+ $updateData[$field] = $data[$field];
+ }
+ }
+
+ if (!empty($data['status']) && $data['status'] === 'confirmed' && $existing['status'] !== 'confirmed') {
+ sendBookingConfirmedEmail($existing);
+ }
+
+ if (!empty($data['status']) && $data['status'] === 'cancelled' && $existing['status'] !== 'cancelled') {
+ sendBookingCancelledEmail($existing);
+ }
+
+ $booking = $db->update('bookings', $id, $updateData);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Booking updated successfully',
+ 'data' => $booking
+ ]);
+}
+
+function deleteBooking($id) {
+ global $db;
+
+ $existing = $db->get('bookings', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Booking not found'], 404);
+ }
+
+ $db->delete('bookings', $id);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Booking deleted successfully'
+ ]);
+}
+
+function sendBookingConfirmationEmail($booking) {
+ $to = $booking['email'];
+ $subject = 'Booking Confirmation - ' . SITE_NAME;
+
+ $date = date('F j, Y', strtotime($booking['booking_date']));
+ $time = date('g:i A', strtotime($booking['booking_time']));
+
+ $plain = "Thank you for booking a consultation with MSPE!\n\n";
+ $plain .= "Booking Details:\n";
+ $plain .= "Date: {$date}\n";
+ $plain .= "Time: {$time}\n";
+ $plain .= "Duration: {$booking['duration']} minutes\n\n";
+ $plain .= "Your booking is currently pending confirmation. We will send you a confirmation email shortly.\n\n";
+ $plain .= "If you need to reschedule or cancel, please contact us at " . ADMIN_EMAIL . "\n\n";
+ $plain .= "Best regards,\nThe MSPE Team";
+
+ $html = 'Booking Received '
+ . 'Thank you for booking a consultation with MSPE!
'
+ . 'Date: ' . $date . '
'
+ . 'Time: ' . $time . '
'
+ . 'Duration: ' . $booking['duration'] . ' minutes
'
+ . 'Your booking is currently pending confirmation.
';
+
+ $result = sendEmail($to, $subject, $html, $plain);
+ if (!$result['success']) {
+ error_log('MSPE booking confirmation email failed: ' . ($result['message'] ?? 'unknown'));
+ }
+}
+
+function sendBookingConfirmedEmail($booking) {
+ $to = $booking['email'];
+ $subject = 'Booking Confirmed - ' . SITE_NAME;
+
+ $date = date('F j, Y', strtotime($booking['booking_date']));
+ $time = date('g:i A', strtotime($booking['booking_time']));
+
+ $plain = "Your consultation with MSPE has been confirmed!\n\n";
+ $plain .= "Booking Details:\n";
+ $plain .= "Date: {$date}\n";
+ $plain .= "Time: {$time}\n";
+ $plain .= "Duration: {$booking['duration']} minutes\n\n";
+ $plain .= "We look forward to speaking with you!\n\n";
+ $plain .= "If you need to reschedule, please contact us at " . ADMIN_EMAIL . "\n\n";
+ $plain .= "Best regards,\nThe MSPE Team";
+
+ $html = 'Booking Confirmed! '
+ . 'Your consultation with MSPE has been confirmed.
'
+ . 'Date: ' . $date . '
'
+ . 'Time: ' . $time . '
'
+ . 'Duration: ' . $booking['duration'] . ' minutes
'
+ . 'We look forward to speaking with you!
';
+
+ $result = sendEmail($to, $subject, $html, $plain);
+ if (!$result['success']) {
+ error_log('MSPE booking confirmed email failed: ' . ($result['message'] ?? 'unknown'));
+ }
+}
+
+function sendBookingCancelledEmail($booking) {
+ $to = $booking['email'];
+ $subject = 'Booking Cancelled - ' . SITE_NAME;
+
+ $plain = "Your consultation with MSPE has been cancelled.\n\n";
+ $plain .= "We apologize for any inconvenience. If you would like to reschedule, please contact us at " . ADMIN_EMAIL . "\n\n";
+ $plain .= "Best regards,\nThe MSPE Team";
+
+ $html = 'Booking Cancelled '
+ . 'Your consultation with MSPE has been cancelled.
'
+ . 'If you would like to reschedule, please contact us at ' . ADMIN_EMAIL . '
';
+
+ $result = sendEmail($to, $subject, $html, $plain);
+ if (!$result['success']) {
+ error_log('MSPE booking cancelled email failed: ' . ($result['message'] ?? 'unknown'));
+ }
+}
+
+function sendAdminBookingNotification($booking) {
+ $to = getSetting('admin_email', ADMIN_EMAIL);
+ $subject = 'New Booking Request - ' . SITE_NAME;
+
+ $plain = "A new booking request has been submitted.\n\n";
+ $plain .= "Client Details:\n";
+ $plain .= "Name: {$booking['first_name']} {$booking['last_name']}\n";
+ $plain .= "Email: {$booking['email']}\n";
+ $plain .= "Phone: {$booking['phone']}\n";
+ $plain .= "Company: {$booking['company']}\n\n";
+ $plain .= "Booking Details:\n";
+ $plain .= "Date: {$booking['booking_date']}\n";
+ $plain .= "Time: {$booking['booking_time']}\n";
+ $plain .= "Duration: {$booking['duration']} minutes\n";
+ $plain .= "Service Interest: {$booking['service_interest']}\n";
+ $plain .= "Message: {$booking['message']}\n";
+
+ $html = 'New Booking Request '
+ . 'Name: ' . htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']) . '
'
+ . 'Email: ' . htmlspecialchars($booking['email']) . '
'
+ . 'Phone: ' . htmlspecialchars($booking['phone']) . '
'
+ . 'Company: ' . htmlspecialchars($booking['company']) . '
'
+ . 'Date: ' . $booking['booking_date'] . '
'
+ . 'Time: ' . $booking['booking_time'] . '
'
+ . 'Duration: ' . $booking['duration'] . ' minutes
'
+ . 'Message: ' . nl2br(htmlspecialchars($booking['message'])) . '
';
+
+ $result = sendEmail($to, $subject, $html, $plain, $booking['email']);
+ if (!$result['success']) {
+ error_log('MSPE admin booking notification failed: ' . ($result['message'] ?? 'unknown'));
+ }
+}
diff --git a/Public HTML/api/config.php b/Public HTML/api/config.php
new file mode 100755
index 0000000..8a7c913
--- /dev/null
+++ b/Public HTML/api/config.php
@@ -0,0 +1,773 @@
+ 1) && (($value[0] === '"' && substr($value, -1) === '"') || ($value[0] === "'" && substr($value, -1) === "'"))) {
+ $value = substr($value, 1, -1);
+ }
+ $_ENV[$key] = $value;
+ putenv("$key=$value");
+ }
+}
+
+// Try loading .env from project root, then from one level up (outside public_html)
+loadEnv(__DIR__ . '/../.env');
+loadEnv(dirname(__DIR__, 2) . '/.env');
+
+// ── Helpers to read env values ───────────────────────────────
+function env($key, $default = '') {
+ return $_ENV[$key] ?? getenv($key) ?: $default;
+}
+
+// ── Error reporting ──────────────────────────────────────────
+$appEnv = env('APP_ENV', 'production');
+if ($appEnv === 'production') {
+ error_reporting(0);
+ ini_set('display_errors', 0);
+} else {
+ error_reporting(E_ALL);
+ ini_set('display_errors', 1);
+}
+
+// Timezone
+date_default_timezone_set('UTC');
+
+// ── Database Configuration ───────────────────────────────────
+define('DB_TYPE', env('DB_TYPE', 'file'));
+define('DB_HOST', env('DB_HOST', 'localhost'));
+define('DB_NAME', env('DB_NAME', 'mspe_website'));
+define('DB_USER', env('DB_USER', ''));
+define('DB_PASS', env('DB_PASS', ''));
+
+// ── Admin credentials ────────────────────────────────────────
+define('ADMIN_USER', env('ADMIN_USER', 'admin'));
+define('ADMIN_PASS', env('ADMIN_PASS', ''));
+
+// ── JWT Secret ───────────────────────────────────────────────
+define('JWT_SECRET', env('JWT_SECRET', 'change-me-in-env-file'));
+if (JWT_SECRET === 'change-me-in-env-file' && $appEnv === 'production') {
+ http_response_code(500);
+ die(json_encode(['success' => false, 'message' => 'Server misconfigured']));
+}
+
+// ── Data directory (for JSON files, cache, etc.) ────────────
+define('DATA_DIR', __DIR__ . '/../data/');
+
+// ── Upload settings ──────────────────────────────────────────
+define('UPLOAD_DIR', __DIR__ . '/../uploads/');
+define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); // 5MB
+define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf']);
+
+// ── Site settings ────────────────────────────────────────────
+define('SITE_NAME', env('SITE_NAME', 'MSPE'));
+define('SITE_URL', env('SITE_URL', 'https://mspe.pro'));
+define('ADMIN_EMAIL', env('ADMIN_EMAIL', 'info@mspe.pro'));
+
+// ── SMTP settings (read from .env, used as defaults before admin settings override) ──
+define('ENV_SMTP_HOST', env('SMTP_HOST', ''));
+define('ENV_SMTP_PORT', env('SMTP_PORT', '465'));
+define('ENV_SMTP_USER', env('SMTP_USER', ''));
+define('ENV_SMTP_PASS', env('SMTP_PASS', ''));
+define('ENV_SMTP_FROM_NAME', env('SMTP_FROM_NAME', 'MSPE'));
+define('ENV_SMTP_FROM_EMAIL', env('SMTP_FROM_EMAIL', 'info@mspe.pro'));
+
+// ── CORS Headers ─────────────────────────────────────────────
+$allowedOrigins = [SITE_URL, 'https://www.mspe.pro'];
+// Allow localhost for local development
+if ($appEnv !== 'production') {
+ $allowedOrigins[] = 'http://localhost:8080';
+ $allowedOrigins[] = 'http://localhost:8000';
+ $allowedOrigins[] = 'http://127.0.0.1:8080';
+}
+$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
+if (in_array($origin, $allowedOrigins)) {
+ header('Access-Control-Allow-Origin: ' . $origin);
+} else {
+ header('Access-Control-Allow-Origin: ' . SITE_URL);
+}
+header('Vary: Origin');
+header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
+header('Access-Control-Allow-Headers: Content-Type, Authorization');
+header('Access-Control-Allow-Credentials: true');
+header('Content-Type: application/json; charset=UTF-8');
+
+// Handle preflight requests
+if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+ http_response_code(200);
+ exit();
+}
+
+// ── Security Headers ─────────────────────────────────────────
+header('X-Content-Type-Options: nosniff');
+header('X-Frame-Options: SAMEORIGIN');
+header('X-XSS-Protection: 0');
+header('Referrer-Policy: strict-origin-when-cross-origin');
+header("Content-Security-Policy: default-src 'none'; frame-ancestors 'none'");
+header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()');
+if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') {
+ header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
+}
+
+/**
+ * MySQL Database Helper
+ */
+class MySQLDB {
+ private $pdo;
+
+ public function __construct() {
+ $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
+ $options = [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ];
+ $this->pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
+ }
+
+ /** Ensure the table exists (auto-create a generic key-value/json table) */
+ private function ensureTable($table) {
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $this->pdo->exec("CREATE TABLE IF NOT EXISTS `$safe` (
+ `id` VARCHAR(64) NOT NULL PRIMARY KEY,
+ `data` JSON NOT NULL,
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+ }
+
+ public function getAll($table) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $stmt = $this->pdo->query("SELECT `data` FROM `$safe` ORDER BY `created_at` ASC");
+ $rows = $stmt->fetchAll();
+ return array_map(function($r) { return json_decode($r['data'], true); }, $rows);
+ }
+
+ /**
+ * Get records with SQL-level pagination (LIMIT/OFFSET).
+ * Returns ['data' => [...], 'total' => int].
+ */
+ public function getAllPaginated($table, $limit = 50, $offset = 0, $orderDir = 'ASC') {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $dir = strtoupper($orderDir) === 'DESC' ? 'DESC' : 'ASC';
+ $total = (int)$this->pdo->query("SELECT COUNT(*) FROM `{$safe}`")->fetchColumn();
+ $stmt = $this->pdo->prepare("SELECT `data` FROM `{$safe}` ORDER BY `created_at` {$dir} LIMIT ? OFFSET ?");
+ $stmt->execute([(int)$limit, (int)$offset]);
+ $rows = $stmt->fetchAll();
+ return [
+ 'data' => array_map(function($r) { return json_decode($r['data'], true); }, $rows),
+ 'total' => $total
+ ];
+ }
+
+ public function get($table, $id) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $stmt = $this->pdo->prepare("SELECT `data` FROM `$safe` WHERE `id` = ? LIMIT 1");
+ $stmt->execute([$id]);
+ $row = $stmt->fetch();
+ return $row ? json_decode($row['data'], true) : null;
+ }
+
+ public function insert($table, $data) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $data['id'] = $data['id'] ?? bin2hex(random_bytes(16));
+ $data['created_at'] = $data['created_at'] ?? date('Y-m-d H:i:s');
+ $data['updated_at'] = date('Y-m-d H:i:s');
+ $stmt = $this->pdo->prepare("INSERT INTO `$safe` (`id`, `data`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$data['id'], json_encode($data, JSON_UNESCAPED_UNICODE), $data['created_at'], $data['updated_at']]);
+ return $data;
+ }
+
+ public function update($table, $id, $data) {
+ $existing = $this->get($table, $id);
+ if (!$existing) return null;
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $data['id'] = $id;
+ $data['created_at'] = $existing['created_at'];
+ $data['updated_at'] = date('Y-m-d H:i:s');
+ $merged = array_merge($existing, $data);
+ $stmt = $this->pdo->prepare("UPDATE `$safe` SET `data` = ?, `updated_at` = ? WHERE `id` = ?");
+ $stmt->execute([json_encode($merged, JSON_UNESCAPED_UNICODE), $merged['updated_at'], $id]);
+ return $merged;
+ }
+
+ public function delete($table, $id) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $stmt = $this->pdo->prepare("DELETE FROM `$safe` WHERE `id` = ?");
+ $stmt->execute([$id]);
+ return true;
+ }
+
+ public function query($table, $conditions = []) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+
+ if (empty($conditions)) {
+ return $this->getAll($table);
+ }
+
+ // Use MySQL JSON_EXTRACT for server-side filtering when possible
+ $where = [];
+ $params = [];
+ foreach ($conditions as $key => $value) {
+ $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
+ $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?";
+ $params[] = (string)$value;
+ }
+
+ $sql = "SELECT `data` FROM `{$safe}` WHERE " . implode(' AND ', $where);
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute($params);
+ $rows = $stmt->fetchAll();
+ return array_map(function($r) { return json_decode($r['data'], true); }, $rows);
+ }
+
+ /** Count rows matching conditions (avoids loading all data into memory) */
+ public function count($table, $conditions = []) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+
+ if (empty($conditions)) {
+ return (int)$this->pdo->query("SELECT COUNT(*) FROM `{$safe}`")->fetchColumn();
+ }
+
+ $where = [];
+ $params = [];
+ foreach ($conditions as $key => $value) {
+ $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
+ $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?";
+ $params[] = (string)$value;
+ }
+
+ $sql = "SELECT COUNT(*) FROM `{$safe}` WHERE " . implode(' AND ', $where);
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute($params);
+ return (int)$stmt->fetchColumn();
+ }
+
+ /** Count rows where a JSON date-like field is between two bounds (inclusive) */
+ public function countByDateRange($table, $dateField, $fromValue, $toValue, $conditions = []) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $safeDateField = preg_replace('/[^a-zA-Z0-9_]/', '', $dateField);
+
+ $where = [
+ "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeDateField}')) >= ?",
+ "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeDateField}')) <= ?"
+ ];
+ $params = [(string)$fromValue, (string)$toValue];
+
+ foreach ($conditions as $key => $value) {
+ $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
+ $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?";
+ $params[] = (string)$value;
+ }
+
+ $sql = "SELECT COUNT(*) FROM `{$safe}` WHERE " . implode(' AND ', $where);
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute($params);
+ return (int)$stmt->fetchColumn();
+ }
+
+ /** Count rows where a single JSON field matches a comparison operator */
+ public function countByFieldComparison($table, $field, $operator, $value, $conditions = []) {
+ $this->ensureTable($table);
+ $safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
+ $safeField = preg_replace('/[^a-zA-Z0-9_]/', '', $field);
+
+ $allowedOps = ['=', '!=', '>', '>=', '<', '<='];
+ $op = in_array($operator, $allowedOps, true) ? $operator : '=';
+
+ $where = ["JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeField}')) {$op} ?"];
+ $params = [(string)$value];
+
+ foreach ($conditions as $key => $condValue) {
+ $safeKey = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
+ $where[] = "JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.{$safeKey}')) = ?";
+ $params[] = (string)$condValue;
+ }
+
+ $sql = "SELECT COUNT(*) FROM `{$safe}` WHERE " . implode(' AND ', $where);
+ $stmt = $this->pdo->prepare($sql);
+ $stmt->execute($params);
+ return (int)$stmt->fetchColumn();
+ }
+}
+
+/**
+ * Simple JWT Helper
+ */
+class JWT {
+ public static function encode($payload, $ttlSeconds = 86400, $secret = JWT_SECRET) {
+ $payload['exp'] = time() + max(300, (int)$ttlSeconds);
+ return FirebaseJWT::encode($payload, $secret, 'HS256');
+ }
+
+ public static function decode($token, $secret = JWT_SECRET) {
+ try {
+ $decoded = FirebaseJWT::decode($token, new Key($secret, 'HS256'));
+ return (array) $decoded;
+ } catch (Exception $e) {
+ return null;
+ }
+ }
+}
+
+/**
+ * Authentication Check
+ */
+function checkAuth() {
+ // Prefer HttpOnly cookie token for admin authentication
+ $cookieToken = $_COOKIE['mspe_admin_token'] ?? '';
+ if (is_string($cookieToken) && $cookieToken !== '') {
+ $payload = JWT::decode($cookieToken);
+ if ($payload) {
+ return $payload;
+ }
+ }
+
+ // Fallback: Authorization header (for API clients)
+ $authHeader = '';
+ if (function_exists('getallheaders')) {
+ $headers = getallheaders();
+ $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
+ }
+
+ if ($authHeader === '' && isset($_SERVER['HTTP_AUTHORIZATION'])) {
+ $authHeader = $_SERVER['HTTP_AUTHORIZATION'];
+ }
+
+ if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
+ $payload = JWT::decode($matches[1]);
+ if ($payload) {
+ return $payload;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Require Authentication
+ */
+function requireAuth() {
+ $user = checkAuth();
+ if (!$user) {
+ http_response_code(401);
+ echo json_encode(['success' => false, 'message' => 'Unauthorized']);
+ exit();
+ }
+ return $user;
+}
+
+/**
+ * JSON Response Helper
+ */
+function jsonResponse($data, $code = 200) {
+ http_response_code($code);
+ echo json_encode($data);
+ exit();
+}
+
+/**
+ * Sanitize Input (HTML Context)
+ * Note: For JavaScript context, use json_encode().
+ * For URL context, use urlencode().
+ */
+function sanitize($input) {
+ if (is_array($input)) {
+ return array_map('sanitize', $input);
+ }
+ // ENT_QUOTES | ENT_SUBSTITUTE handles both single/double quotes and invalid characters
+ return htmlspecialchars(trim($input ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+}
+
+/**
+ * Sanitize Rich Text (HTML Context)
+ * Allows only a whitelist of safe formatting tags and strips everything else.
+ * Removes all event handler attributes (onclick, onerror, etc.) from allowed tags.
+ */
+function sanitizeRichText($input) {
+ if (!is_string($input) || trim($input) === '') return '';
+ // Allow only safe formatting tags
+ $allowed = '
';
+ $cleaned = strip_tags(trim($input), $allowed);
+ // Remove any event handler attributes (on*) and dangerous attributes
+ $cleaned = preg_replace('/\s+on\w+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $cleaned);
+ // Remove javascript: and data: from href/src attributes
+ $cleaned = preg_replace('/\b(href|src)\s*=\s*(?:"(?:javascript|data):.*?"|\'(?:javascript|data):.*?\')/i', '$1=""', $cleaned);
+ // Remove style attributes that could contain expressions
+ $cleaned = preg_replace('/\s+style\s*=\s*(?:"[^"]*"|\'[^\']*\')/i', '', $cleaned);
+ return $cleaned;
+}
+
+/**
+ * Get Request Body
+ */
+function getRequestBody() {
+ return json_decode(file_get_contents('php://input'), true) ?? [];
+}
+
+/**
+ * Basic CSRF mitigation for public POST endpoints.
+ * Allows only same-origin browser requests by validating Origin/Referer.
+ */
+function requireSameOriginRequest() {
+ $appEnv = env('APP_ENV', 'production');
+
+ $allowedOrigins = [
+ rtrim((string)SITE_URL, '/'),
+ 'https://www.mspe.pro'
+ ];
+
+ if ($appEnv !== 'production') {
+ $allowedOrigins[] = 'http://localhost:8080';
+ $allowedOrigins[] = 'http://localhost:8000';
+ $allowedOrigins[] = 'http://127.0.0.1:8080';
+ }
+
+ $origin = trim((string)($_SERVER['HTTP_ORIGIN'] ?? ''));
+ if ($origin !== '') {
+ if (in_array(rtrim($origin, '/'), $allowedOrigins, true)) {
+ return;
+ }
+ jsonResponse(['success' => false, 'message' => 'Forbidden origin'], 403);
+ }
+
+ $referer = trim((string)($_SERVER['HTTP_REFERER'] ?? ''));
+ if ($referer !== '') {
+ foreach ($allowedOrigins as $allowedOrigin) {
+ if (stripos($referer, $allowedOrigin . '/') === 0 || rtrim($referer, '/') === $allowedOrigin) {
+ return;
+ }
+ }
+ jsonResponse(['success' => false, 'message' => 'Forbidden referer'], 403);
+ }
+
+ // Browser request with neither header is suspicious for public form submissions.
+ jsonResponse(['success' => false, 'message' => 'CSRF validation failed'], 403);
+}
+
+/**
+ * Read settings as key => value map
+ */
+function getSettingsMap() {
+ global $db;
+
+ $result = [];
+ $rows = $db->getAll('settings');
+
+ foreach ($rows as $row) {
+ if (isset($row['key'])) {
+ $result[$row['key']] = $row['value'] ?? '';
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * Get a single setting with fallback
+ */
+function getSetting($key, $default = null) {
+ $settings = getSettingsMap();
+ return array_key_exists($key, $settings) ? $settings[$key] : $default;
+}
+
+/**
+ * Send email (mail() or SMTP based on admin settings)
+ */
+function sendEmail($to, $subject, $htmlBody, $plainBody = '', $replyTo = null) {
+ // Determine transport: admin setting → auto-detect from .env
+ $defaultTransport = (ENV_SMTP_HOST !== '') ? 'smtp' : 'mail';
+ $transport = strtolower((string)getSetting('email_transport', $defaultTransport));
+
+ $fromEmail = trim((string)getSetting('smtp_from_email', ENV_SMTP_FROM_EMAIL ?: ADMIN_EMAIL));
+ if ($fromEmail === '') {
+ $fromEmail = ADMIN_EMAIL;
+ }
+
+ $fromName = trim((string)getSetting('smtp_from_name', ENV_SMTP_FROM_NAME ?: SITE_NAME));
+ if ($fromName === '') {
+ $fromName = SITE_NAME;
+ }
+
+ if ($plainBody === '') {
+ $plainBody = trim(strip_tags(str_replace([' ', ' ', ' '], "\n", $htmlBody)));
+ }
+
+ if ($transport === 'smtp') {
+ // Admin settings override .env values
+ $smtpHost = trim((string)getSetting('smtp_host', ENV_SMTP_HOST));
+ $smtpPort = (int)getSetting('smtp_port', ENV_SMTP_PORT ?: 465);
+ $smtpEncryption = strtolower((string)getSetting('smtp_encryption', ((int)($smtpPort) === 465 ? 'ssl' : 'tls')));
+ $smtpUser = trim((string)getSetting('smtp_username', ENV_SMTP_USER));
+ $smtpPass = (string)getSetting('smtp_password', ENV_SMTP_PASS);
+
+ if ($smtpHost !== '' && $smtpPort > 0 && $smtpUser !== '' && $smtpPass !== '') {
+ return sendEmailViaSmtp([
+ 'host' => $smtpHost,
+ 'port' => $smtpPort,
+ 'encryption' => $smtpEncryption,
+ 'username' => $smtpUser,
+ 'password' => $smtpPass,
+ 'from_email' => $fromEmail,
+ 'from_name' => $fromName,
+ 'to' => $to,
+ 'subject' => $subject,
+ 'html' => $htmlBody,
+ 'plain' => $plainBody,
+ 'reply_to' => $replyTo
+ ]);
+ }
+ }
+
+ // Fallback to PHP mail()
+ $boundary = 'mspe_' . md5((string)microtime(true));
+ $headers = [];
+ $headers[] = 'MIME-Version: 1.0';
+ $headers[] = 'From: ' . formatEmailAddress($fromName, $fromEmail);
+ if ($replyTo) {
+ $headers[] = 'Reply-To: ' . $replyTo;
+ }
+ $headers[] = 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
+
+ $body = "--{$boundary}\r\n";
+ $body .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
+ $body .= $plainBody . "\r\n\r\n";
+ $body .= "--{$boundary}\r\n";
+ $body .= "Content-Type: text/html; charset=UTF-8\r\n\r\n";
+ $body .= $htmlBody . "\r\n\r\n";
+ $body .= "--{$boundary}--\r\n";
+
+ $ok = @mail($to, $subject, $body, implode("\r\n", $headers));
+
+ return [
+ 'success' => $ok,
+ 'message' => $ok ? 'Sent via mail()' : 'mail() failed'
+ ];
+}
+
+function sendEmailViaSmtp($payload) {
+ $host = $payload['host'];
+ $port = (int)$payload['port'];
+ $encryption = $payload['encryption'];
+
+ $remote = $encryption === 'ssl' ? 'ssl://' . $host : $host;
+ $socket = @stream_socket_client($remote . ':' . $port, $errno, $errstr, 20, STREAM_CLIENT_CONNECT);
+
+ if (!$socket) {
+ return ['success' => false, 'message' => 'SMTP connect failed: ' . $errstr];
+ }
+
+ stream_set_timeout($socket, 20);
+
+ $expect = function($codes) use ($socket) {
+ $response = '';
+ while (($line = fgets($socket, 515)) !== false) {
+ $response .= $line;
+ if (preg_match('/^\d{3}\s/', $line)) {
+ break;
+ }
+ }
+
+ $code = (int)substr($response, 0, 3);
+ if (!in_array($code, (array)$codes, true)) {
+ throw new Exception(trim($response));
+ }
+
+ return $response;
+ };
+
+ $send = function($command) use ($socket) {
+ fwrite($socket, $command . "\r\n");
+ };
+
+ try {
+ $expect([220]);
+
+ $send('EHLO ' . ($_SERVER['SERVER_NAME'] ?? parse_url(SITE_URL, PHP_URL_HOST) ?? 'mspe.pro'));
+ $expect([250]);
+
+ if ($encryption === 'tls') {
+ $send('STARTTLS');
+ $expect([220]);
+
+ if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ throw new Exception('Failed to start TLS encryption');
+ }
+
+ $send('EHLO ' . ($_SERVER['SERVER_NAME'] ?? parse_url(SITE_URL, PHP_URL_HOST) ?? 'mspe.pro'));
+ $expect([250]);
+ }
+
+ $send('AUTH LOGIN');
+ $expect([334]);
+ $send(base64_encode($payload['username']));
+ $expect([334]);
+ $send(base64_encode($payload['password']));
+ $expect([235]);
+
+ $send('MAIL FROM:<' . $payload['from_email'] . '>');
+ $expect([250]);
+
+ $send('RCPT TO:<' . $payload['to'] . '>');
+ $expect([250, 251]);
+
+ $send('DATA');
+ $expect([354]);
+
+ $boundary = 'mspe_' . md5((string)microtime(true));
+ $headers = [];
+ $headers[] = 'From: ' . formatEmailAddress($payload['from_name'], $payload['from_email']);
+ $headers[] = 'To: ' . $payload['to'];
+ $headers[] = 'Subject: ' . $payload['subject'];
+ $headers[] = 'MIME-Version: 1.0';
+ if (!empty($payload['reply_to'])) {
+ $headers[] = 'Reply-To: ' . $payload['reply_to'];
+ }
+ $headers[] = 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
+
+ $message = implode("\r\n", $headers) . "\r\n\r\n";
+ $message .= "--{$boundary}\r\n";
+ $message .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
+ $message .= $payload['plain'] . "\r\n\r\n";
+ $message .= "--{$boundary}\r\n";
+ $message .= "Content-Type: text/html; charset=UTF-8\r\n\r\n";
+ $message .= $payload['html'] . "\r\n\r\n";
+ $message .= "--{$boundary}--\r\n.";
+
+ fwrite($socket, $message . "\r\n");
+ $expect([250]);
+
+ $send('QUIT');
+ fclose($socket);
+
+ return ['success' => true, 'message' => 'Sent via SMTP'];
+ } catch (Exception $e) {
+ fclose($socket);
+ return ['success' => false, 'message' => 'SMTP send failed: ' . $e->getMessage()];
+ }
+}
+
+function formatEmailAddress($name, $email) {
+ $cleanName = str_replace(['"', "\r", "\n"], '', $name);
+ $cleanEmail = str_replace(["\r", "\n"], '', $email);
+ return sprintf('"%s" <%s>', $cleanName, $cleanEmail);
+}
+
+/**
+ * Handle File Upload
+ */
+function handleFileUpload($file, $subdir = '') {
+ if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
+ return ['success' => false, 'message' => 'No file uploaded'];
+ }
+
+ // Check file size
+ if ($file['size'] > MAX_UPLOAD_SIZE) {
+ return ['success' => false, 'message' => 'File too large'];
+ }
+
+ // Check extension
+ $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+ if (!in_array($ext, ALLOWED_EXTENSIONS)) {
+ return ['success' => false, 'message' => 'File type not allowed'];
+ }
+
+ // Validate MIME type from file contents (not just extension)
+ $allowedMimeByExt = [
+ 'jpg' => ['image/jpeg'],
+ 'jpeg' => ['image/jpeg'],
+ 'png' => ['image/png'],
+ 'gif' => ['image/gif'],
+ 'webp' => ['image/webp'],
+ 'pdf' => ['application/pdf']
+ ];
+
+ if (function_exists('finfo_open')) {
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $detectedMime = $finfo ? finfo_file($finfo, $file['tmp_name']) : false;
+ if ($finfo) {
+ finfo_close($finfo);
+ }
+
+ $allowedMimes = $allowedMimeByExt[$ext] ?? [];
+ if (!$detectedMime || !in_array($detectedMime, $allowedMimes, true)) {
+ return ['success' => false, 'message' => 'Invalid file content type'];
+ }
+ }
+
+ // Create upload directory
+ $uploadDir = UPLOAD_DIR . ($subdir ? $subdir . '/' : '');
+ if (!is_dir($uploadDir)) {
+ mkdir($uploadDir, 0755, true);
+ }
+
+ // Generate unique filename (cryptographically random)
+ $filename = bin2hex(random_bytes(16)) . '_' . preg_replace('/[^a-zA-Z0-9.]/', '', $file['name']);
+ $filepath = $uploadDir . $filename;
+
+ if (move_uploaded_file($file['tmp_name'], $filepath)) {
+ return [
+ 'success' => true,
+ 'filename' => $filename,
+ 'path' => 'uploads/' . ($subdir ? $subdir . '/' : '') . $filename
+ ];
+ }
+
+ return ['success' => false, 'message' => 'Failed to save file'];
+}
+
+// ── Security Audit Logging ─────────────────────────────────────
+function auditLog($event, $details = [], $userId = null) {
+ $entry = [
+ 'timestamp' => date('c'),
+ 'event' => $event,
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
+ 'user_id' => $userId,
+ 'uri' => $_SERVER['REQUEST_URI'] ?? '',
+ 'method' => $_SERVER['REQUEST_METHOD'] ?? '',
+ 'details' => $details
+ ];
+ error_log('[MSPE_AUDIT] ' . json_encode($entry, JSON_UNESCAPED_SLASHES));
+}
+
+// Initialize database based on DB_TYPE from .env
+if (DB_TYPE === 'mysql') {
+ try {
+ $db = new MySQLDB();
+ } catch (Exception $e) {
+ error_log('MSPE MySQL connection failed: ' . $e->getMessage());
+ http_response_code(500);
+ die(json_encode(['success' => false, 'message' => 'Database connection failed']));
+ }
+} else {
+ http_response_code(500);
+ die(json_encode(['success' => false, 'message' => 'Only MySQL is supported in production']));
+}
diff --git a/Public HTML/api/contact.php b/Public HTML/api/contact.php
new file mode 100755
index 0000000..fde99ae
--- /dev/null
+++ b/Public HTML/api/contact.php
@@ -0,0 +1,287 @@
+ false, 'message' => 'Message ID required'], 400);
+ }
+ updateMessage($id);
+ break;
+
+ case 'DELETE':
+ requireAuth();
+ if (!$id) {
+ jsonResponse(['success' => false, 'message' => 'Message ID required'], 400);
+ }
+ deleteMessage($id);
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function getMessages() {
+ global $db;
+
+ $messages = $db->getAll('messages');
+
+ // Apply filters
+ $status = $_GET['status'] ?? null;
+ $limit = (int)($_GET['limit'] ?? 20);
+ $offset = (int)($_GET['offset'] ?? 0);
+
+ if ($status) {
+ $messages = array_filter($messages, function($m) use ($status) {
+ return $m['status'] === $status;
+ });
+ }
+
+ // Sort by date (newest first)
+ usort($messages, function($a, $b) {
+ return strtotime($b['created_at']) - strtotime($a['created_at']);
+ });
+
+ $total = count($messages);
+ $unread = count(array_filter($messages, function($m) {
+ return ($m['status'] ?? 'unread') === 'unread';
+ }));
+
+ $messages = array_slice(array_values($messages), $offset, $limit);
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $messages,
+ 'total' => $total,
+ 'unread' => $unread,
+ 'limit' => $limit,
+ 'offset' => $offset
+ ]);
+}
+
+function getMessage($id) {
+ global $db;
+
+ $message = $db->get('messages', $id);
+
+ if (!$message) {
+ jsonResponse(['success' => false, 'message' => 'Message not found'], 404);
+ }
+
+ // Mark as read
+ if (($message['status'] ?? 'unread') === 'unread') {
+ $db->update('messages', $id, ['status' => 'read']);
+ $message['status'] = 'read';
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $message
+ ]);
+}
+
+function submitMessage() {
+ global $db;
+
+ requireSameOriginRequest();
+
+ // Rate limiting: max 5 submissions per IP per hour
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+ $oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour'));
+ $now = date('Y-m-d H:i:s');
+
+ $recentCount = $db->countByDateRange('messages', 'created_at', $oneHourAgo, $now, [
+ 'ip_address' => $ip
+ ]);
+
+ if ($recentCount >= 5) {
+ jsonResponse(['success' => false, 'message' => 'Too many submissions. Please try again later.'], 429);
+ }
+
+ $data = getRequestBody();
+
+ // ── Cloudflare Turnstile verification (active only when TURNSTILE_SECRET_KEY is set) ──
+ $turnstileSecret = env('TURNSTILE_SECRET_KEY', '');
+ if ($turnstileSecret !== '') {
+ $turnstileToken = $data['cf-turnstile-response'] ?? '';
+ if (empty($turnstileToken)) {
+ jsonResponse(['success' => false, 'message' => 'Human verification is required.'], 400);
+ }
+ if (function_exists('curl_init')) {
+ $ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
+ curl_setopt_array($ch, [
+ CURLOPT_POST => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POSTFIELDS => http_build_query([
+ 'secret' => $turnstileSecret,
+ 'response' => $turnstileToken,
+ 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? ''
+ ])
+ ]);
+ $tvResult = json_decode(curl_exec($ch), true);
+ curl_close($ch);
+ if (!($tvResult['success'] ?? false)) {
+ jsonResponse(['success' => false, 'message' => 'Human verification failed. Please try again.'], 400);
+ }
+ }
+ }
+
+ // Validate required fields
+ $required = ['first_name', 'last_name', 'email', 'message'];
+ foreach ($required as $field) {
+ if (empty($data[$field])) {
+ jsonResponse(['success' => false, 'message' => ucfirst(str_replace('_', ' ', $field)) . ' is required'], 400);
+ }
+ }
+
+ // Validate email
+ if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
+ jsonResponse(['success' => false, 'message' => 'Invalid email address'], 400);
+ }
+
+ // Honeypot check (if implemented in form)
+ if (!empty($data['website'])) {
+ // Likely a bot
+ jsonResponse(['success' => true, 'message' => 'Message sent successfully']);
+ }
+
+ // Prepare message data
+ $messageData = [
+ 'first_name' => sanitize($data['first_name']),
+ 'last_name' => sanitize($data['last_name']),
+ 'email' => sanitize($data['email']),
+ 'phone' => sanitize($data['phone'] ?? ''),
+ 'company' => sanitize($data['company'] ?? ''),
+ 'service' => sanitize($data['service'] ?? ''),
+ 'budget' => sanitize($data['budget'] ?? ''),
+ 'message' => sanitize($data['message']),
+ 'newsletter' => $data['newsletter'] ?? false,
+ 'status' => 'unread',
+ 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''
+ ];
+
+ $message = $db->insert('messages', $messageData);
+
+ // Send email notification (optional)
+ sendNotificationEmail($messageData);
+
+ // Handle newsletter subscription
+ if (!empty($data['newsletter'])) {
+ subscribeNewsletter($data['email'], $data['first_name'] . ' ' . $data['last_name']);
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Thank you for your message! We\'ll be in touch soon.'
+ ], 201);
+}
+
+function updateMessage($id) {
+ global $db;
+
+ $existing = $db->get('messages', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Message not found'], 404);
+ }
+
+ $data = getRequestBody();
+ $allowed = ['status', 'notes'];
+
+ $updateData = [];
+ foreach ($allowed as $field) {
+ if (isset($data[$field])) {
+ $updateData[$field] = $data[$field];
+ }
+ }
+
+ $message = $db->update('messages', $id, $updateData);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Message updated successfully',
+ 'data' => $message
+ ]);
+}
+
+function deleteMessage($id) {
+ global $db;
+
+ $existing = $db->get('messages', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Message not found'], 404);
+ }
+
+ $db->delete('messages', $id);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Message deleted successfully'
+ ]);
+}
+
+function sendNotificationEmail($data) {
+ $to = getSetting('admin_email', getSetting('contact_email', ADMIN_EMAIL));
+ $subject = 'New Contact Form Submission - ' . SITE_NAME;
+
+ $plain = "You have received a new message from your website contact form.\n\n";
+ $plain .= "Name: {$data['first_name']} {$data['last_name']}\n";
+ $plain .= "Email: {$data['email']}\n";
+ $plain .= "Phone: {$data['phone']}\n";
+ $plain .= "Company: {$data['company']}\n";
+ $plain .= "Service Interest: {$data['service']}\n";
+ $plain .= "Budget: {$data['budget']}\n\n";
+ $plain .= "Message:\n{$data['message']}\n";
+
+ $html = 'New Contact Form Submission '
+ . 'Name: ' . htmlspecialchars($data['first_name'] . ' ' . $data['last_name']) . '
'
+ . 'Email: ' . htmlspecialchars($data['email']) . '
'
+ . 'Phone: ' . htmlspecialchars($data['phone']) . '
'
+ . 'Company: ' . htmlspecialchars($data['company']) . '
'
+ . 'Service Interest: ' . htmlspecialchars($data['service']) . '
'
+ . 'Budget: ' . htmlspecialchars($data['budget']) . '
'
+ . 'Message: ' . nl2br(htmlspecialchars($data['message'])) . '
';
+
+ $result = sendEmail($to, $subject, $html, $plain, $data['email']);
+
+ if (!$result['success']) {
+ error_log('MSPE contact email failed: ' . ($result['message'] ?? 'unknown'));
+ }
+}
+
+function subscribeNewsletter($email, $name) {
+ global $db;
+
+ // Check if already subscribed
+ $existing = $db->query('subscribers', ['email' => $email]);
+ if (!empty($existing)) {
+ return;
+ }
+
+ $db->insert('subscribers', [
+ 'email' => $email,
+ 'name' => $name,
+ 'status' => 'active'
+ ]);
+}
diff --git a/Public HTML/api/media.php b/Public HTML/api/media.php
new file mode 100755
index 0000000..2db9d9d
--- /dev/null
+++ b/Public HTML/api/media.php
@@ -0,0 +1,168 @@
+ false, 'message' => 'ID required'], 400);
+ }
+ deleteMedia($id);
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function getMedia() {
+ global $db;
+ // For a file-based system without a dedicated media DB table,
+ // we can scan the uploads directory.
+ // However, keeping a record in JSON is better for metadata.
+ // Let's assume we have a 'media' table.
+
+ $media = $db->getAll('media');
+
+ // Sync with filesystem?
+ // For simplicity, let's just return DB records.
+ // If empty, one could scan dir, but we'll rely on upload recording.
+
+ $folder = $_GET['folder'] ?? null;
+ $type = $_GET['type'] ?? null;
+ $search = $_GET['search'] ?? null;
+
+ if ($folder) {
+ $media = array_filter($media, function($m) use ($folder) {
+ return ($m['folder'] ?? '') === $folder;
+ });
+ }
+
+ if ($type) {
+ $media = array_filter($media, function($m) use ($type) {
+ return strpos($m['type'], $type) !== false;
+ });
+ }
+
+ if ($search) {
+ $media = array_filter($media, function($m) use ($search) {
+ return stripos($m['name'], $search) !== false;
+ });
+ }
+
+ // Sort recent first
+ usort($media, function($a, $b) {
+ return strtotime($b['created_at']) - strtotime($a['created_at']);
+ });
+
+ jsonResponse(['success' => true, 'data' => array_values($media)]);
+}
+
+function uploadMedia() {
+ global $db;
+
+ if (empty($_FILES)) {
+ jsonResponse(['success' => false, 'message' => 'No files uploaded'], 400);
+ }
+
+ $folder = $_POST['folder'] ?? '';
+ $uploaded = [];
+
+ foreach ($_FILES as $key => $file) {
+ // Handle array uploads (multiple files)
+ if (is_array($file['name'])) {
+ foreach ($file['name'] as $idx => $name) {
+ $singleFile = [
+ 'name' => $name,
+ 'type' => $file['type'][$idx],
+ 'tmp_name' => $file['tmp_name'][$idx],
+ 'error' => $file['error'][$idx],
+ 'size' => $file['size'][$idx]
+ ];
+ $result = processUpload($singleFile, $folder);
+ if ($result) $uploaded[] = $result;
+ }
+ } else {
+ $result = processUpload($file, $folder);
+ if ($result) $uploaded[] = $result;
+ }
+ }
+
+ jsonResponse(['success' => true, 'data' => $uploaded]);
+}
+
+function processUpload($file, $folder) {
+ global $db;
+ $res = handleFileUpload($file, $folder);
+
+ if ($res['success']) {
+ $mediaItem = [
+ 'name' => $file['name'],
+ 'path' => $res['path'], // Relative path
+ 'full_url' => SITE_URL . '/' . $res['path'],
+ 'type' => $file['type'],
+ 'size' => $file['size'],
+ 'folder' => $folder,
+ 'created_at' => date('Y-m-d H:i:s')
+ ];
+ return $db->insert('media', $mediaItem);
+ }
+ return null;
+}
+
+function deleteMedia($id) {
+ deleteMediaInternal($id);
+ jsonResponse(['success' => true, 'message' => 'File deleted']);
+}
+
+function deleteMediaInternal($id) {
+ global $db;
+ $item = $db->get('media', $id);
+
+ if ($item) {
+ // Delete physical file
+ $filepath = __DIR__ . '/../' . $item['path'];
+ if (file_exists($filepath)) {
+ unlink($filepath);
+ }
+ $db->delete('media', $id);
+ return true;
+ }
+ return false;
+}
+
+function deleteMediaBatch() {
+ $ids = getRequestBody()['ids'] ?? [];
+ $deleted = 0;
+ foreach ($ids as $id) {
+ if (deleteMediaInternal($id)) {
+ $deleted++;
+ }
+ }
+ jsonResponse(['success' => true, 'message' => "{$deleted} file(s) deleted"]);
+}
diff --git a/Public HTML/api/news.php b/Public HTML/api/news.php
new file mode 100755
index 0000000..dbb06bd
--- /dev/null
+++ b/Public HTML/api/news.php
@@ -0,0 +1,273 @@
+ false, 'message' => 'Article ID required'], 400);
+ }
+ updateArticle($id);
+ break;
+
+ case 'DELETE':
+ requireAuth();
+ if (!$id) {
+ jsonResponse(['success' => false, 'message' => 'Article ID required'], 400);
+ }
+ deleteArticle($id);
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function getArticles() {
+ global $db;
+
+ $articles = $db->getAll('news');
+
+ // Apply filters
+ $category = $_GET['category'] ?? null;
+ $status = $_GET['status'] ?? null;
+ $limit = (int)($_GET['limit'] ?? 10);
+ $offset = (int)($_GET['offset'] ?? 0);
+
+ if ($category) {
+ $articles = array_filter($articles, function($a) use ($category) {
+ return $a['category'] === $category;
+ });
+ }
+
+ if ($status) {
+ $articles = array_filter($articles, function($a) use ($status) {
+ return $a['status'] === $status;
+ });
+ }
+
+ // Sort by date (newest first)
+ usort($articles, function($a, $b) {
+ return strtotime($b['created_at']) - strtotime($a['created_at']);
+ });
+
+ // For public API, only show published articles
+ if (!checkAuth()) {
+ $articles = array_filter($articles, function($a) {
+ return ($a['status'] ?? 'draft') === 'published';
+ });
+ }
+
+ $total = count($articles);
+ $articles = array_slice(array_values($articles), $offset, $limit);
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $articles,
+ 'total' => $total,
+ 'limit' => $limit,
+ 'offset' => $offset
+ ]);
+}
+
+function getArticle($id) {
+ global $db;
+
+ $article = $db->get('news', $id);
+
+ if (!$article) {
+ jsonResponse(['success' => false, 'message' => 'Article not found'], 404);
+ }
+
+ // Check if published or user is authenticated
+ if (($article['status'] ?? 'draft') !== 'published' && !checkAuth()) {
+ jsonResponse(['success' => false, 'message' => 'Article not found'], 404);
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $article
+ ]);
+}
+
+function createArticle() {
+ global $db;
+
+ // Handle multipart form data or JSON
+ if (!empty($_FILES)) {
+ $data = $_POST;
+ } else {
+ $data = getRequestBody();
+ }
+
+ // Validate required fields
+ if (empty($data['title'])) {
+ jsonResponse(['success' => false, 'message' => 'Title is required'], 400);
+ }
+
+ // Sanitize text fields (allow safe HTML in content)
+ $data['title'] = sanitize($data['title']);
+ $data['excerpt'] = sanitize($data['excerpt'] ?? '');
+ $data['category'] = sanitize($data['category'] ?? '');
+ $data['author'] = sanitize($data['author'] ?? 'Admin');
+ // Sanitize rich content: strip dangerous tags/attributes while allowing formatting
+ if (!empty($data['content'])) {
+ $data['content'] = sanitizeRichText($data['content']);
+ }
+
+ // Handle image upload
+ if (!empty($_FILES['featured_image'])) {
+ $upload = handleFileUpload($_FILES['featured_image'], 'news');
+ if ($upload['success']) {
+ $data['featured_image'] = $upload['path'];
+ }
+ }
+
+ // Generate slug if not provided
+ if (empty($data['slug'])) {
+ $data['slug'] = generateSlug($data['title']);
+ } else {
+ $data['slug'] = generateSlug($data['slug']);
+ }
+
+ $data['slug'] = ensureUniqueNewsSlug($data['slug']);
+
+ // Set defaults
+ $data['status'] = $data['status'] ?? 'draft';
+ $data['author'] = $data['author'] ?? 'Admin';
+ $data['views'] = 0;
+
+ $article = $db->insert('news', $data);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Article created successfully',
+ 'data' => $article
+ ], 201);
+}
+
+function updateArticle($id) {
+ global $db;
+
+ $existing = $db->get('news', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Article not found'], 404);
+ }
+
+ // Handle multipart form data or JSON
+ if (!empty($_FILES)) {
+ $data = $_POST;
+ } else {
+ $data = getRequestBody();
+ }
+
+ // Handle image upload
+ if (!empty($_FILES['featured_image'])) {
+ $upload = handleFileUpload($_FILES['featured_image'], 'news');
+ if ($upload['success']) {
+ $data['featured_image'] = $upload['path'];
+ }
+ }
+
+ // Update slug if title changed
+ if (!empty($data['title']) && empty($data['slug'])) {
+ $data['slug'] = generateSlug($data['title']);
+ } elseif (!empty($data['slug'])) {
+ $data['slug'] = generateSlug($data['slug']);
+ }
+
+ if (!empty($data['slug'])) {
+ $data['slug'] = ensureUniqueNewsSlug($data['slug'], $id);
+ }
+
+ // Sanitize text fields
+ if (!empty($data['title'])) $data['title'] = sanitize($data['title']);
+ if (!empty($data['excerpt'])) $data['excerpt'] = sanitize($data['excerpt']);
+ if (!empty($data['category'])) $data['category'] = sanitize($data['category']);
+ if (!empty($data['author'])) $data['author'] = sanitize($data['author']);
+ // Sanitize rich content: strip dangerous tags/attributes while allowing formatting
+ if (!empty($data['content'])) {
+ $data['content'] = sanitizeRichText($data['content']);
+ }
+
+ $article = $db->update('news', $id, $data);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Article updated successfully',
+ 'data' => $article
+ ]);
+}
+
+function deleteArticle($id) {
+ global $db;
+
+ $existing = $db->get('news', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Article not found'], 404);
+ }
+
+ $db->delete('news', $id);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Article deleted successfully'
+ ]);
+}
+
+function generateSlug($title) {
+ $slug = strtolower($title);
+ $slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
+ $slug = trim($slug, '-');
+ return $slug !== '' ? $slug : 'article';
+}
+
+function ensureUniqueNewsSlug($baseSlug, $excludeId = null) {
+ global $db;
+
+ $articles = $db->getAll('news');
+ $used = [];
+ foreach ($articles as $article) {
+ if (!empty($excludeId) && ($article['id'] ?? null) === $excludeId) {
+ continue;
+ }
+ $slug = (string)($article['slug'] ?? '');
+ if ($slug !== '') {
+ $used[$slug] = true;
+ }
+ }
+
+ if (!isset($used[$baseSlug])) {
+ return $baseSlug;
+ }
+
+ $i = 2;
+ while (isset($used[$baseSlug . '-' . $i])) {
+ $i++;
+ }
+
+ return $baseSlug . '-' . $i;
+}
diff --git a/Public HTML/api/pages.php b/Public HTML/api/pages.php
new file mode 100755
index 0000000..8bed5d0
--- /dev/null
+++ b/Public HTML/api/pages.php
@@ -0,0 +1,405 @@
+ false, 'message' => 'Method not allowed'], 405);
+}
+
+// ── Page defaults ────────────────────────────────────────────
+function getDefaultPages() {
+ return [
+ [
+ 'slug' => 'home',
+ 'title' => 'Home Page',
+ 'icon' => 'fa-home',
+ 'url' => '/index.html',
+ 'description' => 'Main landing page with hero, services, and CTA sections',
+ 'status' => 'published',
+ 'meta_title' => 'MSPE | Architects of Digital Resilience',
+ 'meta_description' => 'Next-generation cybersecurity, cloud innovation, and technology consulting.',
+ 'sections' => [
+ ['key' => 'hero', 'label' => 'Hero', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'subheading', 'type' => 'textarea', 'label' => 'Subheading', 'value' => ''],
+ ['name' => 'cta_text', 'type' => 'text', 'label' => 'CTA Button Text', 'value' => ''],
+ ['name' => 'cta_link', 'type' => 'text', 'label' => 'CTA Button Link', 'value' => ''],
+ ]],
+ ['key' => 'services', 'label' => 'Services', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Section Heading', 'value' => ''],
+ ['name' => 'subheading', 'type' => 'textarea', 'label' => 'Section Subheading', 'value' => ''],
+ ]],
+ ['key' => 'stats', 'label' => 'Stats', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Section Heading', 'value' => ''],
+ ]],
+ ['key' => 'cta', 'label' => 'CTA', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'CTA Heading', 'value' => ''],
+ ['name' => 'text', 'type' => 'textarea', 'label' => 'CTA Text', 'value' => ''],
+ ['name' => 'button_text', 'type' => 'text', 'label' => 'Button Text', 'value' => ''],
+ ['name' => 'button_link', 'type' => 'text', 'label' => 'Button Link', 'value' => ''],
+ ]],
+ ],
+ ],
+ [
+ 'slug' => 'about',
+ 'title' => 'About Us',
+ 'icon' => 'fa-building',
+ 'url' => '/about.html',
+ 'description' => 'Company story, mission, values, and team information',
+ 'status' => 'published',
+ 'meta_title' => 'About Us - MSPE',
+ 'meta_description' => 'Learn about MSPE — our story, mission, values, and the team behind your digital resilience.',
+ 'sections' => [
+ ['key' => 'story', 'label' => 'Story', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'content', 'type' => 'textarea', 'label' => 'Content', 'value' => ''],
+ ]],
+ ['key' => 'mission', 'label' => 'Mission', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'content', 'type' => 'textarea', 'label' => 'Content', 'value' => ''],
+ ]],
+ ['key' => 'values', 'label' => 'Values', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ]],
+ ['key' => 'team', 'label' => 'Team', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'subheading', 'type' => 'textarea', 'label' => 'Subheading', 'value' => ''],
+ ]],
+ ],
+ ],
+ [
+ 'slug' => 'services',
+ 'title' => 'Services',
+ 'icon' => 'fa-concierge-bell',
+ 'url' => '/services.html',
+ 'description' => 'Detailed service offerings and pricing plans',
+ 'status' => 'published',
+ 'meta_title' => 'Services - MSPE',
+ 'meta_description' => 'Explore our cybersecurity, cloud, and IT support services.',
+ 'sections' => [
+ ['key' => 'it_support', 'label' => 'IT Support', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'content', 'type' => 'textarea', 'label' => 'Content', 'value' => ''],
+ ]],
+ ['key' => 'security', 'label' => 'Security', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'content', 'type' => 'textarea', 'label' => 'Content', 'value' => ''],
+ ]],
+ ['key' => 'cloud', 'label' => 'Cloud', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'content', 'type' => 'textarea', 'label' => 'Content', 'value' => ''],
+ ]],
+ ['key' => 'pricing', 'label' => 'Pricing', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ]],
+ ],
+ ],
+ [
+ 'slug' => 'portfolio',
+ 'title' => 'Portfolio',
+ 'icon' => 'fa-briefcase',
+ 'url' => '/portfolio.html',
+ 'description' => 'Showcase of completed projects and case studies',
+ 'status' => 'published',
+ 'meta_title' => 'Portfolio - MSPE',
+ 'meta_description' => 'See our completed projects, case studies, and client success stories.',
+ 'sections' => [
+ ['key' => 'projects', 'label' => 'Projects', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'subheading', 'type' => 'textarea', 'label' => 'Subheading', 'value' => ''],
+ ]],
+ ['key' => 'case_studies', 'label' => 'Case Studies', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ]],
+ ['key' => 'clients', 'label' => 'Clients', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ]],
+ ],
+ ],
+ [
+ 'slug' => 'news',
+ 'title' => 'News & Events',
+ 'icon' => 'fa-newspaper',
+ 'url' => '/news.html',
+ 'description' => 'Company news, blog posts, and upcoming events',
+ 'status' => 'published',
+ 'meta_title' => 'News & Events - MSPE',
+ 'meta_description' => 'Stay updated with the latest MSPE news, tech insights, and events.',
+ 'sections' => [
+ ['key' => 'news_list', 'label' => 'News List', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ]],
+ ['key' => 'newsletter', 'label' => 'Newsletter', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'subheading', 'type' => 'textarea', 'label' => 'Subheading', 'value' => ''],
+ ]],
+ ],
+ ],
+ [
+ 'slug' => 'contact',
+ 'title' => 'Contact',
+ 'icon' => 'fa-envelope',
+ 'url' => '/contact.html',
+ 'description' => 'Contact form, location, and FAQ section',
+ 'status' => 'published',
+ 'meta_title' => 'Contact Us - MSPE',
+ 'meta_description' => 'Get in touch with MSPE for cybersecurity, cloud, and IT services.',
+ 'sections' => [
+ ['key' => 'form', 'label' => 'Form', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'subheading', 'type' => 'textarea', 'label' => 'Subheading', 'value' => ''],
+ ]],
+ ['key' => 'info_cards', 'label' => 'Info Cards', 'fields' => [
+ ['name' => 'address', 'type' => 'textarea', 'label' => 'Address', 'value' => ''],
+ ['name' => 'phone', 'type' => 'text', 'label' => 'Phone', 'value' => ''],
+ ['name' => 'email', 'type' => 'text', 'label' => 'Email', 'value' => ''],
+ ['name' => 'hours', 'type' => 'text', 'label' => 'Business Hours', 'value' => ''],
+ ]],
+ ['key' => 'faq', 'label' => 'FAQ', 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ]],
+ ],
+ ],
+ ];
+}
+
+function getDefaultGlobalSections() {
+ return [
+ [
+ 'slug' => 'header',
+ 'label' => 'Header / Navigation',
+ 'icon' => 'fa-bars',
+ 'description' => 'Logo, menu items, CTA button',
+ 'fields' => [
+ ['name' => 'cta_text', 'type' => 'text', 'label' => 'CTA Button Text', 'value' => ''],
+ ['name' => 'cta_link', 'type' => 'text', 'label' => 'CTA Button Link', 'value' => ''],
+ ],
+ ],
+ [
+ 'slug' => 'footer',
+ 'label' => 'Footer',
+ 'icon' => 'fa-shoe-prints',
+ 'description' => 'Links, contact info, social media',
+ 'fields' => [
+ ['name' => 'tagline', 'type' => 'textarea', 'label' => 'Footer Tagline', 'value' => ''],
+ ['name' => 'copyright', 'type' => 'text', 'label' => 'Copyright Text', 'value' => ''],
+ ],
+ ],
+ [
+ 'slug' => 'cta',
+ 'label' => 'CTA Section',
+ 'icon' => 'fa-bullhorn',
+ 'description' => 'Call-to-action banner',
+ 'fields' => [
+ ['name' => 'heading', 'type' => 'text', 'label' => 'Heading', 'value' => ''],
+ ['name' => 'text', 'type' => 'textarea', 'label' => 'Text', 'value' => ''],
+ ['name' => 'button_text', 'type' => 'text', 'label' => 'Button Text', 'value' => ''],
+ ['name' => 'button_link', 'type' => 'text', 'label' => 'Button Link', 'value' => ''],
+ ],
+ ],
+ [
+ 'slug' => 'cookie',
+ 'label' => 'Cookie Banner',
+ 'icon' => 'fa-cookie',
+ 'description' => 'GDPR compliance notice',
+ 'fields' => [
+ ['name' => 'message', 'type' => 'textarea', 'label' => 'Banner Message', 'value' => ''],
+ ['name' => 'button_text', 'type' => 'text', 'label' => 'Accept Button Text', 'value' => ''],
+ ['name' => 'link_text', 'type' => 'text', 'label' => 'Policy Link Text', 'value' => ''],
+ ['name' => 'link_url', 'type' => 'text', 'label' => 'Policy URL', 'value' => ''],
+ ],
+ ],
+ ];
+}
+
+// ── Handlers ─────────────────────────────────────────────────
+
+function getPages() {
+ global $db;
+ $storedPages = $db->getAll('pages');
+ $storedMap = [];
+ foreach ($storedPages as $p) {
+ $slug = $p['slug'] ?? '';
+ if ($slug) $storedMap[$slug] = $p;
+ }
+
+ $pages = getDefaultPages();
+ foreach ($pages as &$page) {
+ if (isset($storedMap[$page['slug']])) {
+ $stored = $storedMap[$page['slug']];
+ // Merge saved values into section fields
+ if (!empty($stored['meta_title'])) $page['meta_title'] = $stored['meta_title'];
+ if (!empty($stored['meta_description'])) $page['meta_description'] = $stored['meta_description'];
+ if (isset($stored['status'])) $page['status'] = $stored['status'];
+ if (!empty($stored['sections_data'])) {
+ $savedData = $stored['sections_data']; // key => { fieldName => value }
+ foreach ($page['sections'] as &$section) {
+ if (isset($savedData[$section['key']])) {
+ foreach ($section['fields'] as &$field) {
+ if (isset($savedData[$section['key']][$field['name']])) {
+ $field['value'] = $savedData[$section['key']][$field['name']];
+ }
+ }
+ }
+ }
+ }
+ if (isset($stored['updated_at'])) $page['updated_at'] = $stored['updated_at'];
+ }
+ }
+
+ jsonResponse(['success' => true, 'data' => $pages]);
+}
+
+function getPage($id) {
+ global $db;
+ $pages = getDefaultPages();
+ $default = null;
+ foreach ($pages as $p) {
+ if ($p['slug'] === $id) { $default = $p; break; }
+ }
+ if (!$default) {
+ jsonResponse(['success' => false, 'message' => 'Page not found'], 404);
+ }
+
+ $stored = $db->query('pages', ['slug' => $id]);
+ if (!empty($stored)) {
+ $stored = array_values($stored)[0];
+ if (!empty($stored['meta_title'])) $default['meta_title'] = $stored['meta_title'];
+ if (!empty($stored['meta_description'])) $default['meta_description'] = $stored['meta_description'];
+ if (isset($stored['status'])) $default['status'] = $stored['status'];
+ if (!empty($stored['sections_data'])) {
+ $savedData = $stored['sections_data'];
+ foreach ($default['sections'] as &$section) {
+ if (isset($savedData[$section['key']])) {
+ foreach ($section['fields'] as &$field) {
+ if (isset($savedData[$section['key']][$field['name']])) {
+ $field['value'] = $savedData[$section['key']][$field['name']];
+ }
+ }
+ }
+ }
+ }
+ if (isset($stored['updated_at'])) $default['updated_at'] = $stored['updated_at'];
+ }
+
+ jsonResponse(['success' => true, 'data' => $default]);
+}
+
+function savePage() {
+ global $db;
+ $data = getRequestBody();
+
+ if (empty($data['slug'])) {
+ jsonResponse(['success' => false, 'message' => 'Page slug required'], 400);
+ }
+
+ $slug = sanitize($data['slug']);
+ $record = [
+ 'slug' => $slug,
+ 'meta_title' => sanitize($data['meta_title'] ?? ''),
+ 'meta_description' => sanitize($data['meta_description'] ?? ''),
+ 'status' => in_array($data['status'] ?? '', ['published', 'draft']) ? $data['status'] : 'published',
+ 'sections_data' => $data['sections_data'] ?? [],
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ];
+
+ $existing = $db->query('pages', ['slug' => $slug]);
+ if (!empty($existing)) {
+ $id = array_values($existing)[0]['id'];
+ $db->update('pages', $id, $record);
+ } else {
+ $db->insert('pages', $record);
+ }
+
+ jsonResponse(['success' => true, 'message' => 'Page saved successfully']);
+}
+
+// ── Global sections ──────────────────────────────────────────
+
+function getGlobalSections() {
+ global $db;
+ $stored = $db->query('pages', ['slug' => '__global__']);
+ $savedData = [];
+ if (!empty($stored)) {
+ $savedData = array_values($stored)[0]['sections_data'] ?? [];
+ }
+
+ $globals = getDefaultGlobalSections();
+ foreach ($globals as &$section) {
+ if (isset($savedData[$section['slug']])) {
+ foreach ($section['fields'] as &$field) {
+ if (isset($savedData[$section['slug']][$field['name']])) {
+ $field['value'] = $savedData[$section['slug']][$field['name']];
+ }
+ }
+ }
+ }
+
+ jsonResponse(['success' => true, 'data' => $globals]);
+}
+
+function saveGlobalSection() {
+ global $db;
+ $data = getRequestBody();
+
+ if (empty($data['section_slug'])) {
+ jsonResponse(['success' => false, 'message' => 'Section slug required'], 400);
+ }
+
+ $sectionSlug = sanitize($data['section_slug']);
+ $fieldValues = $data['fields'] ?? [];
+
+ // Load existing global record
+ $existing = $db->query('pages', ['slug' => '__global__']);
+ $record = [];
+ if (!empty($existing)) {
+ $record = array_values($existing)[0];
+ }
+
+ $sectionsData = $record['sections_data'] ?? [];
+ $sectionsData[$sectionSlug] = $fieldValues;
+
+ $saveData = [
+ 'slug' => '__global__',
+ 'sections_data' => $sectionsData,
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ];
+
+ if (!empty($existing)) {
+ $id = array_values($existing)[0]['id'];
+ $db->update('pages', $id, $saveData);
+ } else {
+ $db->insert('pages', $saveData);
+ }
+
+ jsonResponse(['success' => true, 'message' => 'Global section saved']);
+}
diff --git a/Public HTML/api/portfolio.php b/Public HTML/api/portfolio.php
new file mode 100755
index 0000000..920af56
--- /dev/null
+++ b/Public HTML/api/portfolio.php
@@ -0,0 +1,264 @@
+ false, 'message' => 'Project ID required'], 400);
+ }
+ updateProject($id);
+ break;
+
+ case 'DELETE':
+ requireAuth();
+ if (!$id) {
+ jsonResponse(['success' => false, 'message' => 'Project ID required'], 400);
+ }
+ deleteProject($id);
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function getProjects() {
+ global $db;
+
+ $projects = $db->getAll('portfolio');
+
+ // Apply filters
+ $category = $_GET['category'] ?? null;
+ $featured = $_GET['featured'] ?? null;
+ $limit = (int)($_GET['limit'] ?? 20);
+ $offset = (int)($_GET['offset'] ?? 0);
+
+ if ($category) {
+ $projects = array_filter($projects, function($p) use ($category) {
+ return $p['category'] === $category;
+ });
+ }
+
+ if ($featured !== null) {
+ $projects = array_filter($projects, function($p) use ($featured) {
+ return ($p['featured'] ?? false) == ($featured === 'true' || $featured === '1');
+ });
+ }
+
+ // Public view: only published/active projects
+ if (!checkAuth()) {
+ $projects = array_filter($projects, function($p) {
+ return ($p['status'] ?? 'published') === 'published';
+ });
+ }
+
+ // Sort by order or date
+ usort($projects, function($a, $b) {
+ $orderA = $a['order'] ?? 999;
+ $orderB = $b['order'] ?? 999;
+ if ($orderA !== $orderB) {
+ return $orderA - $orderB;
+ }
+ return strtotime($b['created_at']) - strtotime($a['created_at']);
+ });
+
+ $total = count($projects);
+ $projects = array_slice(array_values($projects), $offset, $limit);
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $projects,
+ 'total' => $total,
+ 'limit' => $limit,
+ 'offset' => $offset
+ ]);
+}
+
+function getProject($id) {
+ global $db;
+
+ $project = $db->get('portfolio', $id);
+
+ if (!$project) {
+ jsonResponse(['success' => false, 'message' => 'Project not found'], 404);
+ }
+
+ jsonResponse([
+ 'success' => true,
+ 'data' => $project
+ ]);
+}
+
+function createProject() {
+ global $db;
+
+ // Handle multipart form data or JSON
+ if (!empty($_FILES)) {
+ $data = $_POST;
+ } else {
+ $data = getRequestBody();
+ }
+
+ // Validate required fields
+ if (empty($data['title'])) {
+ jsonResponse(['success' => false, 'message' => 'Title is required'], 400);
+ }
+
+ // Sanitize text fields (allow HTML in description)
+ $data['title'] = sanitize($data['title']);
+ $data['category'] = sanitize($data['category'] ?? '');
+ $data['client'] = sanitize($data['client'] ?? '');
+ $data['technologies'] = sanitize($data['technologies'] ?? '');
+ // Sanitize description: allow only safe formatting tags
+ $data['description'] = sanitizeRichText($data['description'] ?? '');
+
+ // Handle image upload
+ if (!empty($_FILES['image'])) {
+ $upload = handleFileUpload($_FILES['image'], 'portfolio');
+ if ($upload['success']) {
+ $data['image'] = $upload['path'];
+ }
+ }
+
+ // Parse results JSON (JS sends as a serialised string)
+ $data['results'] = json_decode($data['results'] ?? '[]', true) ?? [];
+
+ // Gallery: merge existing-keep list + newly uploaded files
+ $gallery = json_decode($_POST['gallery_keep'] ?? '[]', true) ?? [];
+ if (!empty($_FILES['gallery_images']['tmp_name'])) {
+ foreach ($_FILES['gallery_images']['tmp_name'] as $key => $tmpName) {
+ if ($_FILES['gallery_images']['error'][$key] !== UPLOAD_ERR_OK) continue;
+ $file = [
+ 'name' => $_FILES['gallery_images']['name'][$key],
+ 'type' => $_FILES['gallery_images']['type'][$key],
+ 'tmp_name' => $tmpName,
+ 'error' => $_FILES['gallery_images']['error'][$key],
+ 'size' => $_FILES['gallery_images']['size'][$key],
+ ];
+ $upload = handleFileUpload($file, 'portfolio');
+ if ($upload['success']) {
+ $gallery[] = $upload['path'];
+ }
+ }
+ }
+ $data['gallery'] = $gallery;
+
+ // Set defaults
+ $data['featured'] = $data['featured'] ?? false;
+ $data['order'] = (int)($data['order'] ?? 0);
+ $data['status'] = $data['status'] ?? 'published';
+
+ $project = $db->insert('portfolio', $data);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Project created successfully',
+ 'data' => $project
+ ], 201);
+}
+
+function updateProject($id) {
+ global $db;
+
+ $existing = $db->get('portfolio', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Project not found'], 404);
+ }
+
+ // Handle multipart form data or JSON
+ if (!empty($_FILES)) {
+ $data = $_POST;
+ } else {
+ $data = getRequestBody();
+ }
+
+ // Handle image upload
+ if (!empty($_FILES['image'])) {
+ $upload = handleFileUpload($_FILES['image'], 'portfolio');
+ if ($upload['success']) {
+ $data['image'] = $upload['path'];
+ }
+ }
+
+ // Sanitize editable text fields
+ if (isset($data['title'])) $data['title'] = sanitize($data['title']);
+ if (isset($data['category'])) $data['category'] = sanitize($data['category']);
+ if (isset($data['client'])) $data['client'] = sanitize($data['client']);
+ if (isset($data['technologies'])) $data['technologies'] = sanitize($data['technologies']);
+ if (isset($data['description'])) $data['description'] = sanitizeRichText($data['description']);
+
+ // Parse results JSON
+ if (isset($data['results'])) {
+ $data['results'] = json_decode($data['results'], true) ?? $existing['results'] ?? [];
+ }
+
+ // Gallery: merge existing-keep list + newly uploaded files
+ if (array_key_exists('gallery_keep', $_POST)) {
+ $gallery = json_decode($_POST['gallery_keep'], true) ?? [];
+ if (!empty($_FILES['gallery_images']['tmp_name'])) {
+ foreach ($_FILES['gallery_images']['tmp_name'] as $key => $tmpName) {
+ if ($_FILES['gallery_images']['error'][$key] !== UPLOAD_ERR_OK) continue;
+ $file = [
+ 'name' => $_FILES['gallery_images']['name'][$key],
+ 'type' => $_FILES['gallery_images']['type'][$key],
+ 'tmp_name' => $tmpName,
+ 'error' => $_FILES['gallery_images']['error'][$key],
+ 'size' => $_FILES['gallery_images']['size'][$key],
+ ];
+ $upload = handleFileUpload($file, 'portfolio');
+ if ($upload['success']) {
+ $gallery[] = $upload['path'];
+ }
+ }
+ }
+ $data['gallery'] = $gallery;
+ }
+
+ $project = $db->update('portfolio', $id, $data);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Project updated successfully',
+ 'data' => $project
+ ]);
+}
+
+function deleteProject($id) {
+ global $db;
+
+ $existing = $db->get('portfolio', $id);
+ if (!$existing) {
+ jsonResponse(['success' => false, 'message' => 'Project not found'], 404);
+ }
+
+ $db->delete('portfolio', $id);
+
+ jsonResponse([
+ 'success' => true,
+ 'message' => 'Project deleted successfully'
+ ]);
+}
diff --git a/Public HTML/api/public-settings.php b/Public HTML/api/public-settings.php
new file mode 100755
index 0000000..f0ba17a
--- /dev/null
+++ b/Public HTML/api/public-settings.php
@@ -0,0 +1,46 @@
+ false, 'message' => 'Method not allowed'], 405);
+}
+
+$all = getSettingsMap();
+
+$allowedKeys = [
+ 'site_name',
+ 'site_tagline',
+ 'site_description',
+ 'contact_email',
+ 'contact_phone',
+ 'contact_address',
+ 'business_hours',
+ 'support_hours',
+ 'maps_url',
+ 'social_facebook',
+ 'social_linkedin',
+ 'social_twitter',
+ 'social_instagram',
+ 'social_youtube',
+ 'social_github',
+ 'primary_color',
+ 'secondary_color',
+ 'accent_color'
+];
+
+$data = [];
+foreach ($allowedKeys as $key) {
+ if (isset($all[$key]) && $all[$key] !== '') {
+ $data[$key] = $all[$key];
+ }
+}
+
+if (!isset($data['contact_email']) || $data['contact_email'] === '') {
+ $data['contact_email'] = ADMIN_EMAIL;
+}
+
+jsonResponse(['success' => true, 'data' => $data]);
diff --git a/Public HTML/api/services.php b/Public HTML/api/services.php
new file mode 100755
index 0000000..9f39fcf
--- /dev/null
+++ b/Public HTML/api/services.php
@@ -0,0 +1,125 @@
+ false, 'message' => 'Service ID required'], 400);
+ }
+ updateService($id);
+ break;
+
+ case 'DELETE':
+ requireAuth();
+ if (!$id) {
+ jsonResponse(['success' => false, 'message' => 'ID required'], 400);
+ }
+ deleteService($id);
+ break;
+
+ default:
+ jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405);
+}
+
+function getServices() {
+ global $db;
+ $services = $db->getAll('services');
+
+ // Sort by order
+ usort($services, function($a, $b) {
+ return ($a['order'] ?? 99) - ($b['order'] ?? 99);
+ });
+
+ // Public view: only active
+ if (!checkAuth()) {
+ $services = array_filter($services, function($s) {
+ return ($s['active'] ?? true) === true;
+ });
+ }
+
+ jsonResponse(['success' => true, 'data' => array_values($services)]);
+}
+
+function getService($id) {
+ global $db;
+ $service = $db->get('services', $id);
+ if (!$service) jsonResponse(['success' => false, 'message' => 'Not found'], 404);
+ jsonResponse(['success' => true, 'data' => $service]);
+}
+
+function createService() {
+ global $db;
+ $data = !empty($_POST) ? $_POST : getRequestBody();
+
+ if (empty($data['name'])) {
+ jsonResponse(['success' => false, 'message' => 'Service name is required'], 400);
+ }
+
+ // Sanitize text fields
+ $data['name'] = sanitize($data['name']);
+ $data['icon'] = sanitize($data['icon'] ?? '');
+ $data['features'] = sanitize($data['features'] ?? '');
+ $data['price'] = sanitize($data['price'] ?? '');
+ // Sanitize description: allow only safe formatting tags (no
+
+
Social Media Links
+