Dashboard
+Welcome to your client portal. Here you can manage your projects, view creative assets, access your courses, and handle billing.
+ +diff --git a/api/pexels.php b/api/pexels.php new file mode 100644 index 0000000..fccbde6 --- /dev/null +++ b/api/pexels.php @@ -0,0 +1,29 @@ + 'assets/images/pexels/'.$p['id'].'.jpg', + 'photographer' => $p['photographer'] ?? 'Unknown', + 'photographer_url' => $p['photographer_url'] ?? '', + ]; + } else { + // Fallback: Picsum + $out[] = [ + 'src' => 'https://picsum.photos/1200/800?random='.rand(), + 'photographer' => 'Random Picsum', + 'photographer_url' => 'https://picsum.photos/' + ]; + } +} +echo json_encode($out); +?> \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index ec38839..0bd0c98 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,4 +1,3 @@ - /* General Body Styles */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; @@ -59,8 +58,7 @@ h2 { /* Hero Section */ #hero { - background: linear-gradient(180deg, #FFFFFF 0%, #F2F2F7 100%); - padding: 120px 0; + padding: 0; } /* Card Styles */ @@ -92,3 +90,72 @@ footer { background-color: #FFFFFF; padding: 40px 0; } + +/* Pricing Table */ +#pricing .card { + border: 1px solid #e5e5e5; + transition: all .2s; +} + +#pricing .card:hover { + transform: scale(1.02); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.1); +} + +#pricing .card-popular { + border-color: #007AFF; + border-width: 2px; +} + +#pricing .card-price { + font-size: 3rem; + font-weight: 700; +} + +#pricing .period { + font-size: 0.8rem; + font-weight: normal; + color: #6c757d; +} + +#pricing .fa-ul { + list-style: none; + padding-left: 0; + margin-bottom: 2rem; +} + +#pricing .fa-li { + position: relative; + left: 0; + margin-bottom: 0.75rem; + padding-left: 2em; /* Add padding to align text */ +} + +#pricing .fa-li .fas { + position: absolute; + left: 0; + width: 2em; + text-align: center; + color: #007AFF; +} + +#pricing .text-muted .fa-li .fas { + color: #6c757d; +} + + +/* Hero Overlay */ +#hero { + position: relative; + background-size: cover; + background-position: center; + color: white; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +.hero-overlay { + position: relative; + z-index: 1; + background-color: rgba(0, 0, 0, 0.5); + padding: 120px 0; +} \ No newline at end of file diff --git a/assets/css/portal.css b/assets/css/portal.css new file mode 100644 index 0000000..60f4970 --- /dev/null +++ b/assets/css/portal.css @@ -0,0 +1,37 @@ +body { + background-color: #f8f9fa; +} + +.portal-nav { + background-color: #fff; + border-bottom: 1px solid #dee2e6; +} + +.portal-main { + min-height: calc(100vh - 56px); + display: flex; +} + +.sidebar { + width: 250px; + background-color: #fff; + border-right: 1px solid #dee2e6; +} + +.sidebar .nav-link { + color: #333; + padding: 1rem; + border-left: 3px solid transparent; +} + +.sidebar .nav-link.active, +.sidebar .nav-link:hover { + color: #007bff; + background-color: #e9ecef; + border-left-color: #007bff; +} + +.content { + flex-grow: 1; + padding: 2rem; +} diff --git a/assets/images/pexels/185576.jpg b/assets/images/pexels/185576.jpg new file mode 100644 index 0000000..3edc76f Binary files /dev/null and b/assets/images/pexels/185576.jpg differ diff --git a/assets/images/pexels/256514.jpg b/assets/images/pexels/256514.jpg new file mode 100644 index 0000000..7dc8fcf Binary files /dev/null and b/assets/images/pexels/256514.jpg differ diff --git a/assets/images/pexels/607812.jpg b/assets/images/pexels/607812.jpg new file mode 100644 index 0000000..279b354 Binary files /dev/null and b/assets/images/pexels/607812.jpg differ diff --git a/assets/images/pexels/669612.jpg b/assets/images/pexels/669612.jpg new file mode 100644 index 0000000..311e13d Binary files /dev/null and b/assets/images/pexels/669612.jpg differ diff --git a/assets/images/pexels/887843.jpg b/assets/images/pexels/887843.jpg new file mode 100644 index 0000000..6f89553 Binary files /dev/null and b/assets/images/pexels/887843.jpg differ diff --git a/auth_handler.php b/auth_handler.php new file mode 100644 index 0000000..d51a958 --- /dev/null +++ b/auth_handler.php @@ -0,0 +1,72 @@ +prepare('SELECT id FROM users WHERE email = ?'); + $stmt->execute([$email]); + if ($stmt->fetch()) { + header('Location: register.php?error=email_taken'); + exit(); + } + + $hashed_password = password_hash($password, PASSWORD_DEFAULT); + + // Default new registrations to the 'Client' role + $stmt = $db->prepare("SELECT id FROM roles WHERE name = 'Client'"); + $stmt->execute(); + $client_role = $stmt->fetch(PDO::FETCH_ASSOC); + + $stmt = $db->prepare('INSERT INTO users (name, email, password, role_id) VALUES (?, ?, ?, ?)'); + if ($stmt->execute([$name, $email, $hashed_password, $client_role['id'] ?? null])) { + $user_id = $db->lastInsertId(); + $_SESSION['user_id'] = $user_id; + header('Location: portal/'); + exit(); + } else { + header('Location: register.php?error=registration_failed'); + exit(); + } + +} elseif ($action === 'login') { + $email = $_POST['email'] ?? ''; + $password = $_POST['password'] ?? ''; + + if (empty($email) || empty($password)) { + header('Location: login.php?error=missing_fields'); + exit(); + } + + $db = db(); + $stmt = $db->prepare('SELECT * FROM users WHERE email = ?'); + $stmt->execute([$email]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($user && password_verify($password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + header('Location: portal/'); + exit(); + } else { + header('Location: login.php?error=invalid_credentials'); + exit(); + } +} else { + header('Location: index.php'); + exit(); +} diff --git a/db/migrations/002_create_users_and_roles.sql b/db/migrations/002_create_users_and_roles.sql new file mode 100644 index 0000000..49f479c --- /dev/null +++ b/db/migrations/002_create_users_and_roles.sql @@ -0,0 +1,26 @@ + +CREATE TABLE IF NOT EXISTS `roles` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(50) NOT NULL UNIQUE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Insert default roles +INSERT IGNORE INTO `roles` (`name`) VALUES +('Super Admin'), +('Agency Admin'), +('Account Manager'), +('Client'), +('Content Creator'), +('Analyst'), +('Billing'), +('Support'); + +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(100) NOT NULL, + `email` VARCHAR(100) NOT NULL UNIQUE, + `password` VARCHAR(255) NOT NULL, + `role_id` INT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/003_create_portal_tables.sql b/db/migrations/003_create_portal_tables.sql new file mode 100644 index 0000000..48638e3 --- /dev/null +++ b/db/migrations/003_create_portal_tables.sql @@ -0,0 +1,61 @@ + +-- Projects table to store project information +CREATE TABLE IF NOT EXISTS `projects` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `client_id` INT NOT NULL, + `status` VARCHAR(50) DEFAULT 'Pending', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`client_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Assets table for creative assets linked to projects +CREATE TABLE IF NOT EXISTS `assets` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `project_id` INT NOT NULL, + `name` VARCHAR(255) NOT NULL, + `file_path` VARCHAR(255) NOT NULL, + `status` VARCHAR(50) DEFAULT 'Pending Approval', + `uploaded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Comments on assets +CREATE TABLE IF NOT EXISTS `asset_comments` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `asset_id` INT NOT NULL, + `user_id` INT NOT NULL, + `comment` TEXT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`asset_id`) REFERENCES `assets`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Courses available for purchase +CREATE TABLE IF NOT EXISTS `courses` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `description` TEXT, + `price` DECIMAL(10, 2) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Link table for users who have access to courses +CREATE TABLE IF NOT EXISTS `user_courses` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, + `course_id` INT NOT NULL, + `purchased_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Invoices for billing +CREATE TABLE IF NOT EXISTS `invoices` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `client_id` INT NOT NULL, + `amount` DECIMAL(10, 2) NOT NULL, + `status` VARCHAR(50) DEFAULT 'Unpaid', + `due_date` DATE, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`client_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/004_create_extended_portal_tables.sql b/db/migrations/004_create_extended_portal_tables.sql new file mode 100644 index 0000000..62310a6 --- /dev/null +++ b/db/migrations/004_create_extended_portal_tables.sql @@ -0,0 +1,63 @@ +-- Messaging system tied to projects +CREATE TABLE IF NOT EXISTS `messages` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `project_id` INT NOT NULL, + `user_id` INT NOT NULL, + `message` TEXT NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Calendar events +CREATE TABLE IF NOT EXISTS `calendar_events` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(255) NOT NULL, + `start_event` DATETIME NOT NULL, + `end_event` DATETIME NOT NULL, + `project_id` INT, + `user_id` INT, + `description` TEXT, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Approvals for assets +CREATE TABLE IF NOT EXISTS `approvals` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `asset_id` INT NOT NULL, + `user_id` INT NOT NULL, + `status` ENUM('Pending', 'Approved', 'Rejected') NOT NULL DEFAULT 'Pending', + `comments` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (`asset_id`) REFERENCES `assets`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Metrics from ad platforms +CREATE TABLE IF NOT EXISTS `metrics` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `project_id` INT NOT NULL, + `platform` VARCHAR(50) NOT NULL, -- e.g., 'Facebook', 'Google Ads' + `date` DATE NOT NULL, + `impressions` INT, + `clicks` INT, + `conversions` INT, + `spend` DECIMAL(10, 2), + `engagement` INT, + `reach` INT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `project_platform_date` (`project_id`, `platform`, `date`), + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Secure storage for API tokens +CREATE TABLE IF NOT EXISTS `api_tokens` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `service` VARCHAR(50) NOT NULL UNIQUE, -- e.g., 'meta_ads', 'google_ads' + `access_token` TEXT NOT NULL, + `refresh_token` TEXT, + `expires_at` TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..4b4431d --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,102 @@ +prepare('SELECT roles.name FROM users JOIN roles ON users.role_id = roles.id WHERE users.id = ?'); + $stmt->execute([$user_id]); + $role = $stmt->fetchColumn(); + + return in_array($role, ['Super Admin', 'Agency Admin']); +} + +// Function to require admin privileges +function require_admin() { + if (!is_admin()) { + // Redirect to portal index if not an admin + header('Location: /portal'); + exit(); + } +} + +// Function to start impersonating a user +function impersonate_user($user_id_to_impersonate) { + if (!is_admin()) { + return false; // Only admins can impersonate + } + // Prevent impersonating another admin for security + $db = db(); + $stmt = $db->prepare('SELECT roles.name FROM users JOIN roles ON users.role_id = roles.id WHERE users.id = ?'); + $stmt->execute([$user_id_to_impersonate]); + $role_to_impersonate = $stmt->fetchColumn(); + + if (in_array($role_to_impersonate, ['Super Admin', 'Agency Admin']) && $_SESSION['user_id'] != $user_id_to_impersonate) { + // Allow admins to view their own profile without triggering this rule + return false; + } + + if (!isset($_SESSION['original_user_id'])) { + $_SESSION['original_user_id'] = $_SESSION['user_id']; + } + $_SESSION['user_id'] = $user_id_to_impersonate; + return true; +} + +// Function to stop impersonating +function stop_impersonating() { + if (isset($_SESSION['original_user_id'])) { + $_SESSION['user_id'] = $_SESSION['original_user_id']; + unset($_SESSION['original_user_id']); + return true; + } + return false; +} + +// Function to check if currently impersonating +function is_impersonating() { + return isset($_SESSION['original_user_id']); +} + +// Function to get the current user's data (handles impersonation) +function current_user() { + if (!is_logged_in()) { + return null; + } + $db = db(); + $stmt = $db->prepare('SELECT users.*, roles.name AS role_name FROM users LEFT JOIN roles ON users.role_id = roles.id WHERE users.id = ?'); + $stmt->execute([$_SESSION['user_id']]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} + +// Function to get the original admin user if impersonating +function original_user() { + if (!is_impersonating()) { + return null; + } + $db = db(); + $stmt = $db->prepare('SELECT users.*, roles.name AS role_name FROM users LEFT JOIN roles ON users.role_id = roles.id WHERE users.id = ?'); + $stmt->execute([$_SESSION['original_user_id']]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} diff --git a/includes/pexels.php b/includes/pexels.php new file mode 100644 index 0000000..823ea99 --- /dev/null +++ b/includes/pexels.php @@ -0,0 +1,26 @@ + 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18'; +} +function pexels_get($url) { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ], + CURLOPT_TIMEOUT => 15, + ]); + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true); + return null; +} +function download_to($srcUrl, $destPath) { + $data = file_get_contents($srcUrl); + if ($data === false) return false; + if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true); + return file_put_contents($destPath, $data) !== false; +} +?> \ No newline at end of file diff --git a/index.php b/index.php index 299cb8a..2f63c66 100644 --- a/index.php +++ b/index.php @@ -1,3 +1,22 @@ +
@@ -23,10 +42,26 @@ + + @@ -41,9 +76,14 @@ + @@ -52,10 +92,12 @@We build intelligent systems to scale your social media, content creation, and client management. Focus on strategy, not busywork.
- Get a Demo + @@ -69,8 +111,7 @@Our platform provides a unified portal for your team and clients. Manage projects, approve content, and track performance—all in one place. We integrate with the tools you already love, like Stripe, Calendly, and Meta Ads, to create seamless, automated workflows.
Social Media Growth
Content Automation
Ad Campaign Management
Social Media Growth
Content Automation
Ad Campaign Management
Choose the plan that's right for your business. All prices are placeholders.
+Don't have an account? Register here
+| Name | +Role | +Actions | +|
|---|---|---|---|
| + | + | + | + + Impersonate + + + + | +
This page will display a list of creative assets and other items pending approval. Clients and managers will be able to review and approve or reject work from this screen.
+ +Review and approve creative assets for your projects.
+ +There are no assets available for review at this time.
+View your invoices and manage your subscription.
+ +You have no invoices.
+This page will contain a calendar view of project milestones, deadlines, and other important events. The data will be dynamically loaded based on project and user roles.
+ +Access your purchased courses here.
+ +You have not purchased any courses yet.
+Welcome to your client portal. Here you can manage your projects, view creative assets, access your courses, and handle billing.
+ +Your account information.
+ +Name:
+Email:
+Role:
+Here is a list of your projects and their current status.
+ +No projects have been assigned to you yet.
+Already have an account? Login here
+