2
This commit is contained in:
parent
062a594521
commit
bfa71e661d
74
admin.php
Normal file
74
admin.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
require_once 'includes/header.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
// Security check: only admins can access this page
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
|
||||
// You can redirect them to the home page or show an error
|
||||
header('HTTP/1.0 403 Forbidden');
|
||||
echo "<h1>403 Forbidden</h1><p>You do not have permission to access this page.</p>";
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch all users for display
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT id, email, role, subscription_plan, subscription_expires_at, created_at FROM users ORDER BY created_at DESC");
|
||||
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$error = "Database error: " . $e->getMessage();
|
||||
$users = [];
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<main class="container">
|
||||
<div class="page-header">
|
||||
<h1><?= t('admin_panel_title') ?></h1>
|
||||
<p><?= t('admin_panel_subtitle') ?></p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><?= t('registered_users') ?></h3>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th><?= t('email') ?></th>
|
||||
<th><?= t('role') ?></th>
|
||||
<th><?= t('subscription_plan') ?></th>
|
||||
<th><?= t('subscription_expires_at') ?></th>
|
||||
<th><?= t('registered_on') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($users)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center"><?= t('no_users_found') ?></td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($user['id']) ?></td>
|
||||
<td><?= htmlspecialchars($user['email']) ?></td>
|
||||
<td><?= htmlspecialchars($user['role']) ?></td>
|
||||
<td><?= htmlspecialchars($user['subscription_plan'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($user['subscription_expires_at'] ?? 'N/A') ?></td>
|
||||
<td><?= date("Y-m-d", strtotime($user['created_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
@ -46,7 +46,7 @@ body {
|
||||
padding: 0 calc(var(--base-spacing) * 1.5);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-family-headings);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
@ -63,6 +63,25 @@ a {
|
||||
}
|
||||
a:hover { color: var(--color-secondary); }
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
padding: calc(var(--base-spacing) * 3) 0;
|
||||
background-color: var(--surface-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: calc(var(--base-spacing) * 3);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.75rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--subtle-text-color);
|
||||
max-width: 600px;
|
||||
margin: 0.5rem auto 0;
|
||||
}
|
||||
|
||||
/* --- Header --- */
|
||||
.header {
|
||||
background-color: var(--surface-color);
|
||||
@ -182,183 +201,6 @@ input:checked + .slider:before { transform: translateX(24px); }
|
||||
.slider.round { border-radius: 34px; }
|
||||
.slider.round:before { border-radius: 50%; }
|
||||
|
||||
/* --- Hero Section --- */
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: calc(var(--base-spacing) * 5) 0;
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
}
|
||||
.hero h1 { color: white; }
|
||||
.hero p {
|
||||
font-size: 1.25rem;
|
||||
margin: var(--base-spacing) 0 calc(var(--base-spacing) * 2);
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.cta-button {
|
||||
background: white;
|
||||
color: var(--color-primary);
|
||||
padding: 0.8rem 2.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-headings);
|
||||
font-size: 1.1rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* --- How It Works Section --- */
|
||||
.how-it-works {
|
||||
padding: calc(var(--base-spacing) * 4) 0;
|
||||
text-align: center;
|
||||
}
|
||||
.steps {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: calc(var(--base-spacing) * 2);
|
||||
margin-top: calc(var(--base-spacing) * 2);
|
||||
}
|
||||
.step {
|
||||
max-width: 300px;
|
||||
}
|
||||
.step-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 50%;
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--base-spacing);
|
||||
}
|
||||
.step-icon i {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.step h3 { margin-bottom: 0.5rem; }
|
||||
.step p { color: var(--subtle-text-color); }
|
||||
|
||||
/* --- Generator Section --- */
|
||||
.generator-section {
|
||||
padding: calc(var(--base-spacing) * 4) 0;
|
||||
background-color: var(--surface-color);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.generator-section h2 {
|
||||
text-align: center;
|
||||
margin-bottom: calc(var(--base-spacing) * 2);
|
||||
}
|
||||
.generator-form {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--base-spacing) * 2);
|
||||
}
|
||||
.form-column-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: var(--base-spacing);
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="number"],
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family-body);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s, background-color 0.3s;
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.photo-upload-container .upload-area {
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: calc(var(--base-spacing) * 2);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.photo-upload-container .upload-area:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.upload-area i {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--subtle-text-color);
|
||||
margin-bottom: var(--base-spacing);
|
||||
}
|
||||
.photo-preview {
|
||||
display: flex;
|
||||
gap: var(--base-spacing);
|
||||
margin-top: var(--base-spacing);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview-item {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.preview-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.remove-img-btn {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
.form-submit-group {
|
||||
text-align: center;
|
||||
margin-top: calc(var(--base-spacing) * 2);
|
||||
}
|
||||
.form-submit-group .cta-button {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-submit-group .cta-button:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
/* --- Footer --- */
|
||||
.footer {
|
||||
background-color: var(--surface-color);
|
||||
@ -370,48 +212,9 @@ input:checked + .slider:before { transform: translateX(24px); }
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
/* --- Result & Error Styles --- */
|
||||
.result-container {
|
||||
margin-top: calc(var(--base-spacing) * 3);
|
||||
padding: calc(var(--base-spacing) * 2);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.result-container h3 {
|
||||
text-align: center;
|
||||
margin-bottom: calc(var(--base-spacing) * 1.5);
|
||||
}
|
||||
|
||||
.result-item {
|
||||
margin-bottom: var(--base-spacing);
|
||||
}
|
||||
|
||||
.result-item label {
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-headings);
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.result-item p {
|
||||
background-color: var(--surface-color);
|
||||
padding: var(--base-spacing);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
margin-top: calc(var(--base-spacing) * 1.5);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--base-spacing);
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-headings);
|
||||
@ -419,15 +222,18 @@ input:checked + .slider:before { transform: translateX(24px); }
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: opacity 0.2s;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
.btn:hover { opacity: 0.85; }
|
||||
.btn:hover { opacity: 0.9; transform: translateY(-2px); }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient);
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { color: white; }
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--surface-color);
|
||||
@ -435,129 +241,295 @@ input:checked + .slider:before { transform: translateX(24px); }
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: calc(var(--base-spacing) * 2);
|
||||
padding: var(--base-spacing);
|
||||
background-color: #ffcccc;
|
||||
color: #990000;
|
||||
border: 1px solid #ff9999;
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
.btn-outline-primary {
|
||||
background-color: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 2px solid var(--color-primary);
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .error-message {
|
||||
background-color: #4d0000;
|
||||
color: #ffc2c2;
|
||||
border-color: #800000;
|
||||
/* --- Cards --- */
|
||||
.card {
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.card-body {
|
||||
padding: calc(var(--base-spacing) * 1.5);
|
||||
}
|
||||
.card-header {
|
||||
padding: var(--base-spacing) calc(var(--base-spacing) * 1.5);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
/* --- Forms --- */
|
||||
.form-group {
|
||||
margin-bottom: var(--base-spacing);
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-family-body);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s, background-color 0.3s;
|
||||
}
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.15);
|
||||
}
|
||||
|
||||
/* --- Auth Pages (Login/Register) --- */
|
||||
.auth-container {
|
||||
max-width: 450px;
|
||||
margin: calc(var(--base-spacing) * 3) auto;
|
||||
}
|
||||
.auth-container .card-header h2 {
|
||||
font-size: 1.75rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.auth-container .btn {
|
||||
width: 100%;
|
||||
}
|
||||
.auth-container .text-center {
|
||||
margin-top: var(--base-spacing);
|
||||
color: var(--subtle-text-color);
|
||||
}
|
||||
|
||||
/* --- History Page --- */
|
||||
.page-title {
|
||||
text-align: center;
|
||||
margin: calc(var(--base-spacing) * 2) 0;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--base-spacing) * 1.5);
|
||||
}
|
||||
|
||||
.history-item .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg-color);
|
||||
font-size: 0.9rem;
|
||||
color: var(--subtle-text-color);
|
||||
}
|
||||
|
||||
.history-item .card-body {
|
||||
background-color: var(--surface-color);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden; /* Ensures children conform to border radius */
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: calc(var(--base-spacing) * 1.5);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--base-spacing) calc(var(--base-spacing) * 1.5);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
.history-item .card-body p {
|
||||
white-space: pre-wrap; /* Keep formatting */
|
||||
}
|
||||
|
||||
/* --- Pricing Page --- */
|
||||
.pricing-card-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
.pricing-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: calc(var(--base-spacing) * 2.5);
|
||||
gap: var(--base-spacing);
|
||||
}
|
||||
.pricing-toggle span {
|
||||
font-weight: 500;
|
||||
color: var(--subtle-text-color);
|
||||
}
|
||||
.pricing-toggle .toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
}
|
||||
.pricing-toggle .toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.pricing-toggle .toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-primary);
|
||||
transition: .4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
.pricing-toggle .toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.pricing-toggle input:checked + .toggle-slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.pricing-card-title small {
|
||||
.pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: calc(var(--base-spacing) * 2);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pricing-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: calc(var(--base-spacing) * 2);
|
||||
border: 2px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pricing-card.popular {
|
||||
border-color: var(--color-primary);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 10px 30px rgba(108, 99, 255, 0.15);
|
||||
}
|
||||
|
||||
.pricing-card h3 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pricing-card .price {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-family-headings);
|
||||
margin: var(--base-spacing) 0;
|
||||
}
|
||||
.pricing-card .price .period {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--subtle-text-color);
|
||||
}
|
||||
|
||||
.card-header h4 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
padding-left: 0;
|
||||
.pricing-card .features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: var(--base-spacing) 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.pricing-card .features li {
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.pricing-card .features li svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
border-radius: var(--border-radius);
|
||||
.pricing-card .btn {
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn-group .btn:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.btn-group .btn:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
.annual-price {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- FAQ Page --- */
|
||||
.accordion-button {
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-headings);
|
||||
background-color: var(--surface-color);
|
||||
color: var(--text-color);
|
||||
.faq-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.accordion-button:not(.collapsed) {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.accordion-button:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(108, 99, 255, 0.25);
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: var(--base-spacing);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.accordion-header {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.accordion-button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: var(--base-spacing) calc(var(--base-spacing) * 1.5);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family-headings);
|
||||
font-size: 1.1rem;
|
||||
background-color: var(--surface-color);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.accordion-button:not(.collapsed) {
|
||||
color: var(--color-primary);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.accordion-button::after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
right: calc(var(--base-spacing) * 1.5);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.accordion-button:not(.collapsed)::after {
|
||||
content: '−';
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
.accordion-collapse {
|
||||
display: none;
|
||||
}
|
||||
.accordion-collapse.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
padding: calc(var(--base-spacing) * 1.5);
|
||||
background-color: var(--bg-color);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* --- Terms Page --- */
|
||||
.terms-content h4 {
|
||||
.terms-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background-color: var(--surface-color);
|
||||
padding: calc(var(--base-spacing) * 2);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
.terms-content h2 {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
.terms-content h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.terms-content p, .terms-content ul, .terms-content ol {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.terms-content ul, .terms-content ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
29
db/promote_user.php
Normal file
29
db/promote_user.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// This script is meant to be run from the command line.
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("This script can only be run from the command line.");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
if ($argc < 2) {
|
||||
echo "Usage: php promote_user.php <email>\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$email = $argv[1];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE users SET role = 'admin' WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo "User '{$email}' has been promoted to admin.\n";
|
||||
} else {
|
||||
echo "Could not find a user with email '{$email}'.\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
die("Database error: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
42
db/setup.php
42
db/setup.php
@ -1,18 +1,50 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
function columnExists($pdo, $table, $column) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?");
|
||||
$stmt->execute([$table, $column]);
|
||||
return $stmt->fetchColumn() !== false;
|
||||
} catch (PDOException $e) {
|
||||
// If the query fails, we can assume the column doesn't exist or there's a bigger issue.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "
|
||||
|
||||
// Create users table
|
||||
$sql_users = "
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);";
|
||||
$pdo->exec($sql);
|
||||
echo "Table 'users' created successfully (if it didn't exist).\n";
|
||||
$pdo->exec($sql_users);
|
||||
echo "Table 'users' is ready.\n";
|
||||
|
||||
// Add role column to users table
|
||||
if (!columnExists($pdo, 'users', 'role')) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'user'");
|
||||
echo "Column 'role' added to 'users' table.\n";
|
||||
}
|
||||
|
||||
// Add subscription_plan column to users table
|
||||
if (!columnExists($pdo, 'users', 'subscription_plan')) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN subscription_plan VARCHAR(50) DEFAULT NULL");
|
||||
echo "Column 'subscription_plan' added to 'users' table.\n";
|
||||
}
|
||||
|
||||
// Add subscription_expires_at column to users table
|
||||
if (!columnExists($pdo, 'users', 'subscription_expires_at')) {
|
||||
$pdo->exec("ALTER TABLE users ADD COLUMN subscription_expires_at DATE DEFAULT NULL");
|
||||
echo "Column 'subscription_expires_at' added to 'users' table.\n";
|
||||
}
|
||||
|
||||
// Create generations table
|
||||
$sql_generations = "
|
||||
CREATE TABLE IF NOT EXISTS generations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@ -23,7 +55,9 @@ try {
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);";
|
||||
$pdo->exec($sql_generations);
|
||||
echo "Table 'generations' created successfully (if it didn't exist).\n";
|
||||
echo "Table 'generations' is ready.\n";
|
||||
|
||||
echo "\nDatabase setup completed successfully!\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("DB ERROR: " . $e->getMessage());
|
||||
|
||||
89
generate.php
89
generate.php
@ -8,52 +8,10 @@ require_once __DIR__ . '/ai/LocalAIApi.php';
|
||||
// Set language from session or default to English
|
||||
$lang = $_SESSION['lang'] ?? 'en';
|
||||
|
||||
// --- Helper Functions ---
|
||||
// --- Helper Functions (omitted for brevity, same as before) ---
|
||||
function translate_to_english($text, $source_lang) { if ($source_lang === 'en') return $text; $prompt = "Translate the following text from '{$source_lang}' to English. Only return the translated text, with no extra commentary:\n\n{$text}"; $response = LocalAIApi::createResponse(['input' => [['role' => 'system', 'content' => 'You are a translation assistant.'],['role' => 'user', 'content' => $prompt]]]); if (!empty($response['success'])) { $decoded = LocalAIApi::decodeJsonFromResponse($response); return $decoded['choices'][0]['message']['content'] ?? $text; } return $text; }
|
||||
function translate_from_english($text, $target_lang) { if ($target_lang === 'en') return $text; $prompt = "Translate the following text from English to '{$target_lang}'. Only return the translated text, with no extra commentary:\n\n{$text}"; $response = LocalAIApi::createResponse(['input' => [['role' => 'system', 'content' => 'You are a translation assistant.'],['role' => 'user', 'content' => $prompt]]]); if (!empty($response['success'])) { $decoded = LocalAIApi::decodeJsonFromResponse($response); return $decoded['choices'][0]['message']['content'] ?? $text; } return $text; }
|
||||
|
||||
function translate_to_english($text, $source_lang) {
|
||||
if ($source_lang === 'en') {
|
||||
return $text;
|
||||
}
|
||||
// In a real app, you would use a translation API.
|
||||
// For this demo, we'll simulate by calling our AI with a translation prompt.
|
||||
$prompt = "Translate the following text from '{$source_lang}' to English. Only return the translated text, with no extra commentary:
|
||||
|
||||
{$text}";
|
||||
$response = LocalAIApi::createResponse([
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => 'You are a translation assistant.'],
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
],
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$decoded = LocalAIApi::decodeJsonFromResponse($response);
|
||||
return $decoded['choices'][0]['message']['content'] ?? $text;
|
||||
}
|
||||
return $text; // Fallback to original text on error
|
||||
}
|
||||
|
||||
function translate_from_english($text, $target_lang) {
|
||||
if ($target_lang === 'en') {
|
||||
return $text;
|
||||
}
|
||||
// Simulate translation
|
||||
$prompt = "Translate the following text from English to '{$target_lang}'. Only return the translated text, with no extra commentary:
|
||||
|
||||
{$text}";
|
||||
$response = LocalAIApi::createResponse([
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => 'You are a translation assistant.'],
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
],
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$decoded = LocalAIApi::decodeJsonFromResponse($response);
|
||||
return $decoded['choices'][0]['message']['content'] ?? $text;
|
||||
}
|
||||
return $text; // Fallback to original text on error
|
||||
}
|
||||
|
||||
// --- Main Logic ---
|
||||
|
||||
@ -61,6 +19,44 @@ $result_html = '';
|
||||
$error_message = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// --- 0. Subscription & Limit Check ---
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$pdo = db();
|
||||
|
||||
// Fetch user subscription details
|
||||
$stmt = $pdo->prepare("SELECT subscription_plan, subscription_expires_at FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
$is_active = $user && $user['subscription_expires_at'] && new DateTime() < new DateTime($user['subscription_expires_at']);
|
||||
|
||||
if (!$is_active) {
|
||||
$_SESSION['generation_error'] = t('subscription_expired_error');
|
||||
header('Location: pricing.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($user['subscription_plan'] === 'basic') {
|
||||
// Count generations in the last 30 days
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM generations WHERE user_id = ? AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)");
|
||||
$stmt->execute([$user_id]);
|
||||
$generation_count = $stmt->fetchColumn();
|
||||
|
||||
if ($generation_count >= 150) {
|
||||
$_SESSION['generation_error'] = t('generation_limit_exceeded_error');
|
||||
header('Location: pricing.php');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If user is not logged in, redirect to login page
|
||||
$_SESSION['generation_error'] = t('login_to_generate_error');
|
||||
header('Location: login.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
// --- 1. Collect and Sanitize Inputs ---
|
||||
$state = htmlspecialchars($_POST['state'] ?? '');
|
||||
$size = htmlspecialchars($_POST['size'] ?? '');
|
||||
@ -120,8 +116,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$prompt_parts[] = "Generate the output in English.";
|
||||
$prompt = implode("
|
||||
", $prompt_parts);
|
||||
$prompt = implode("\n", $prompt_parts);
|
||||
|
||||
// --- 4. Call AI API ---
|
||||
$ai_response = LocalAIApi::createResponse([
|
||||
|
||||
@ -57,6 +57,9 @@ $is_logged_in = isset($_SESSION['user_id']);
|
||||
<a href="terms.php"><?php echo t('nav_terms'); ?></a>
|
||||
<?php if ($is_logged_in): ?>
|
||||
<a href="history.php"><?php echo t('nav_history'); ?></a>
|
||||
<?php if (isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin'): ?>
|
||||
<a href="admin.php"><?= t('admin_panel_title') ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
|
||||
|
||||
@ -132,6 +132,23 @@ function get_translations() {
|
||||
'terms_section4_text' => 'We may terminate or suspend your account and bar access to the service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of the Terms.',
|
||||
'terms_section5_title' => '5. Changes to Terms',
|
||||
'terms_section5_text' => 'We reserve the right, at our sole discretion, to modify or replace these Terms at any time. We will provide at least 30 days notice prior to any new terms taking effect.',
|
||||
'subscription_success_message' => 'Your subscription has been activated successfully!',
|
||||
'no_active_subscription' => 'You do not have an active subscription.',
|
||||
'choose_plan_to_start' => 'Choose a plan to start generating.',
|
||||
'plan' => 'Plan',
|
||||
'expires_on' => 'Expires on',
|
||||
'change_plan' => 'Change Plan',
|
||||
'subscription_expired_error' => 'Your subscription has expired. Please renew your plan to continue generating.',
|
||||
'generation_limit_exceeded_error' => 'You have exceeded your monthly generation limit. Please upgrade your plan to continue.',
|
||||
'login_to_generate_error' => 'Please log in to generate descriptions.',
|
||||
'admin_panel_title' => 'Admin Panel',
|
||||
'admin_panel_subtitle' => 'Manage users and application settings.',
|
||||
'registered_users' => 'Registered Users',
|
||||
'role' => 'Role',
|
||||
'subscription_plan' => 'Subscription Plan',
|
||||
'subscription_expires_at' => 'Subscription Expires',
|
||||
'registered_on' => 'Registered On',
|
||||
'no_users_found' => 'No users found.',
|
||||
],
|
||||
'pl' => [
|
||||
'app_name' => 'Captionista',
|
||||
@ -264,6 +281,23 @@ function get_translations() {
|
||||
'terms_section4_text' => 'Możemy zamknąć lub zawiesić Twoje konto i zablokować dostęp do usługi natychmiast, bez uprzedniego powiadomienia lub odpowiedzialności, według naszego wyłącznego uznania, z dowolnego powodu i bez ograniczeń, w tym między innymi z powodu naruszenia Warunków.',
|
||||
'terms_section5_title' => '5. Zmiany w Regulaminie',
|
||||
'terms_section5_text' => 'Zastrzegamy sobie prawo, według naszego wyłącznego uznania, do modyfikowania lub zastępowania niniejszych Warunków w dowolnym momencie. Powiadomimy o wszelkich nowych warunkach co najmniej 30 dni przed ich wejściem w życie.',
|
||||
'subscription_success_message' => 'Twoja subskrypcja została aktywowana pomyślnie!',
|
||||
'no_active_subscription' => 'Nie masz aktywnej subskrypcji.',
|
||||
'choose_plan_to_start' => 'Wybierz plan, aby rozpocząć generowanie.',
|
||||
'plan' => 'Plan',
|
||||
'expires_on' => 'Wygasa',
|
||||
'change_plan' => 'Zmień plan',
|
||||
'subscription_expired_error' => 'Twoja subskrypcja wygasła. Odnów plan, aby kontynuować generowanie.',
|
||||
'generation_limit_exceeded_error' => 'Przekroczyłeś miesięczny limit generacji. Ulepsz swój plan, aby kontynuować.',
|
||||
'login_to_generate_error' => 'Zaloguj się, aby generować opisy.',
|
||||
'admin_panel_title' => 'Panel administratora',
|
||||
'admin_panel_subtitle' => 'Zarządzaj użytkownikami i ustawieniami aplikacji.',
|
||||
'registered_users' => 'Zarejestrowani użytkownicy',
|
||||
'role' => 'Rola',
|
||||
'subscription_plan' => 'Plan subskrypcji',
|
||||
'subscription_expires_at' => 'Subskrypcja wygasa',
|
||||
'registered_on' => 'Zarejestrowany',
|
||||
'no_users_found' => 'Nie znaleziono użytkowników.',
|
||||
],
|
||||
'es' => [
|
||||
'app_name' => 'Captionista',
|
||||
@ -396,6 +430,23 @@ function get_translations() {
|
||||
'terms_section4_text' => 'Podemos rescindir o suspender tu cuenta y prohibir el acceso al servicio de inmediato, sin previo aviso ni responsabilidad, a nuestra entera discreción, por cualquier motivo y sin limitación, incluido, entre otros, el incumplimiento de los Términos.',
|
||||
'terms_section5_title' => '5. Cambios en los Términos',
|
||||
'terms_section5_text' => 'Nos reservamos el derecho, a nuestra entera discreción, de modificar o reemplazar estos Términos en cualquier momento. Proporcionaremos un aviso de al menos 30 días antes de que los nuevos términos entren en vigor.',
|
||||
'subscription_success_message' => '¡Tu suscripción ha sido activada con éxito!',
|
||||
'no_active_subscription' => 'No tienes una suscripción activa.',
|
||||
'choose_plan_to_start' => 'Elige un plan para empezar a generar.',
|
||||
'plan' => 'Plan',
|
||||
'expires_on' => 'Expira el',
|
||||
'change_plan' => 'Cambiar de plan',
|
||||
'subscription_expired_error' => 'Tu suscripción ha expirado. Renueva tu plan para seguir generando.',
|
||||
'generation_limit_exceeded_error' => 'Has excedido tu límite de generación mensual. Actualiza tu plan para continuar.',
|
||||
'login_to_generate_error' => 'Inicia sesión para generar descripciones.',
|
||||
'admin_panel_title' => 'Panel de administración',
|
||||
'admin_panel_subtitle' => 'Gestionar usuarios y configuración de la aplicación.',
|
||||
'registered_users' => 'Usuarios registrados',
|
||||
'role' => 'Rol',
|
||||
'subscription_plan' => 'Plan de suscripción',
|
||||
'subscription_expires_at' => 'La suscripción expira',
|
||||
'registered_on' => 'Registrado el',
|
||||
'no_users_found' => 'No se encontraron usuarios.',
|
||||
],
|
||||
'fr' => [
|
||||
'app_name' => 'Captionista',
|
||||
@ -528,6 +579,23 @@ function get_translations() {
|
||||
'terms_section4_text' => "Nous pouvons résilier ou suspendre votre compte et interdire l'accès au service immédiatement, sans préavis ni responsabilité, à notre seule discrétion, pour quelque raison que ce soit et sans limitation, y compris, mais sans s'y limiter, une violation des Conditions.",
|
||||
'terms_section5_title' => '5. Modifications des conditions',
|
||||
'terms_section5_text' => "Nous nous réservons le droit, à notre seule discrétion, de modifier ou de remplacer ces Conditions à tout moment. Nous fournirons un préavis d'au moins 30 jours avant l'entrée en vigueur de nouvelles conditions.",
|
||||
'subscription_success_message' => 'Votre abonnement a été activé avec succès !',
|
||||
'no_active_subscription' => "Vous n'avez pas d'abonnement actif.",
|
||||
'choose_plan_to_start' => 'Choisissez un plan pour commencer à générer.',
|
||||
'plan' => 'Plan',
|
||||
'expires_on' => 'Expire le',
|
||||
'change_plan' => 'Changer de plan',
|
||||
'subscription_expired_error' => 'Votre abonnement a expiré. Veuillez renouveler votre plan pour continuer à générer.',
|
||||
'generation_limit_exceeded_error' => 'Vous avez dépassé votre limite de génération mensuelle. Veuillez mettre à niveau votre plan pour continuer.',
|
||||
'login_to_generate_error' => 'Veuillez vous connecter pour générer des descriptions.',
|
||||
'admin_panel_title' => "Panneau d'administration",
|
||||
'admin_panel_subtitle' => "Gérer les utilisateurs et les paramètres de l'application.",
|
||||
'registered_users' => 'Utilisateurs enregistrés',
|
||||
'role' => 'Rôle',
|
||||
'subscription_plan' => "Plan d'abonnement",
|
||||
'subscription_expires_at' => "L'abonnement expire",
|
||||
'registered_on' => 'Inscrit le',
|
||||
'no_users_found' => 'Aucun utilisateur trouvé.',
|
||||
],
|
||||
'de' => [
|
||||
'app_name' => 'Captionista',
|
||||
@ -660,6 +728,23 @@ function get_translations() {
|
||||
'terms_section4_text' => 'Wir können Ihr Konto kündigen oder sperren und den Zugang zum Dienst sofort, ohne vorherige Ankündigung oder Haftung, nach unserem alleinigen Ermessen, aus irgendeinem Grund und ohne Einschränkung, einschließlich, aber nicht beschränkt auf einen Verstoß gegen die Bedingungen, sperren.',
|
||||
'terms_section5_title' => '5. Änderungen der Bedingungen',
|
||||
'terms_section5_text' => 'Wir behalten uns das Recht vor, diese Bedingungen nach unserem alleinigen Ermessen jederzeit zu ändern oder zu ersetzen. Wir werden mindestens 30 Tage vor dem Inkrafttreten neuer Bedingungen eine entsprechende Mitteilung machen.',
|
||||
'subscription_success_message' => 'Ihr Abonnement wurde erfolgreich aktiviert!',
|
||||
'no_active_subscription' => 'Sie haben kein aktives Abonnement.',
|
||||
'choose_plan_to_start' => 'Wählen Sie einen Plan, um mit dem Generieren zu beginnen.',
|
||||
'plan' => 'Plan',
|
||||
'expires_on' => 'Läuft ab am',
|
||||
'change_plan' => 'Plan ändern',
|
||||
'subscription_expired_error' => 'Ihr Abonnement ist abgelaufen. Bitte erneuern Sie Ihren Plan, um weiterhin zu generieren.',
|
||||
'generation_limit_exceeded_error' => 'Sie haben Ihr monatliches Generierungslimit überschritten. Bitte aktualisieren Sie Ihren Plan, um fortzufahren.',
|
||||
'login_to_generate_error' => 'Bitte melden Sie sich an, um Beschreibungen zu generieren.',
|
||||
'admin_panel_title' => 'Administrationsbereich',
|
||||
'admin_panel_subtitle' => 'Benutzer und Anwendungseinstellungen verwalten.',
|
||||
'registered_users' => 'Registrierte Benutzer',
|
||||
'role' => 'Rolle',
|
||||
'subscription_plan' => 'Abonnementplan',
|
||||
'subscription_expires_at' => 'Abonnement läuft ab',
|
||||
'registered_on' => 'Registriert am',
|
||||
'no_users_found' => 'Keine Benutzer gefunden.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -14,13 +14,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$error = t('fill_all_fields');
|
||||
} else {
|
||||
try {
|
||||
$stmt = $pdo->prepare('SELECT id, password FROM users WHERE email = ?');
|
||||
$stmt = $pdo->prepare('SELECT id, password, role FROM users WHERE email = ?');
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['user_email'] = $email;
|
||||
$_SESSION['user_role'] = $user['role'];
|
||||
header('Location: index.php');
|
||||
exit();
|
||||
} else {
|
||||
|
||||
60
profile.php
60
profile.php
@ -11,8 +11,25 @@ if (!isset($_SESSION['user_id'])) {
|
||||
$error = null;
|
||||
$success = null;
|
||||
|
||||
// Check for flash messages
|
||||
if (isset($_SESSION['flash_message'])) {
|
||||
$success = $_SESSION['flash_message'];
|
||||
unset($_SESSION['flash_message']);
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Fetch user subscription data
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare('SELECT subscription_plan, subscription_expires_at FROM users WHERE id = ?');
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$error = "Database error: " . $e->getMessage();
|
||||
$user = ['subscription_plan' => null, 'subscription_expires_at' => null];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password_confirm = $_POST['password_confirm'] ?? '';
|
||||
@ -20,7 +37,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($password) && $password === $password_confirm) {
|
||||
try {
|
||||
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare('UPDATE users SET password = ? WHERE id = ?');
|
||||
$stmt->execute([$hashed_password, $user_id]);
|
||||
$success = t('profile_updated_successfully');
|
||||
@ -33,37 +49,53 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
?>
|
||||
|
||||
<main class="container">
|
||||
<section class="auth-form">
|
||||
<h1><?= t('profile_heading') ?></h1>
|
||||
<p><?= t('profile_subtitle') ?></p>
|
||||
<main class="container auth-container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2><?= t('profile_heading') ?></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-center"><?= t('profile_subtitle') ?></p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-message"><?= $error ?></div>
|
||||
<div class="alert alert-danger"><?= $error ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="success-message"><?= $success ?></div>
|
||||
<div class="alert alert-success"><?= $success ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card-section">
|
||||
<h4><?= t('nav_subscription') ?></h4>
|
||||
<?php if ($user && $user['subscription_plan']): ?>
|
||||
<p><strong><?= t('plan') ?>:</strong> <?= htmlspecialchars(ucfirst($user['subscription_plan'])) ?></p>
|
||||
<p><strong><?= t('expires_on') ?>:</strong> <?= date("F j, Y", strtotime($user['subscription_expires_at'])) ?></p>
|
||||
<a href="pricing.php" class="btn btn-secondary btn-sm"><?= t('change_plan') ?></a>
|
||||
<?php else: ?>
|
||||
<p><?= t('no_active_subscription') ?></p>
|
||||
<a href="pricing.php" class="btn btn-primary"><?= t('choose_plan_to_start') ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<form action="profile.php" method="POST" id="profile-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="email"><?= t('email') ?></label>
|
||||
<input type="email" id="email" name="email" value="<?= htmlspecialchars($_SESSION['user_email'] ?? '') ?>" disabled>
|
||||
<small><?= t('email_cannot_be_changed') ?></small>
|
||||
<input type="email" id="email" name="email" class="form-control" value="<?= htmlspecialchars($_SESSION['user_email'] ?? '') ?>" disabled>
|
||||
<small class="form-text text-muted"><?= t('email_cannot_be_changed') ?></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password"><?= t('new_password') ?></label>
|
||||
<input type="password" id="password" name="password">
|
||||
<input type="password" id="password" name="password" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password_confirm"><?= t('confirm_new_password') ?></label>
|
||||
<input type="password" id="password_confirm" name="password_confirm">
|
||||
<input type="password" id="password_confirm" name="password_confirm" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary"><?= t('update_profile_button') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
54
subscribe.php
Normal file
54
subscribe.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
session_start();
|
||||
|
||||
require_once 'includes/translations.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
// Require login
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$plan = $_GET['plan'] ?? null;
|
||||
$period = $_GET['period'] ?? 'monthly'; // Default to monthly
|
||||
|
||||
$allowed_plans = ['basic', 'premium'];
|
||||
|
||||
if (!$plan || !in_array($plan, $allowed_plans)) {
|
||||
header('Location: pricing.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
$expiryDate = '';
|
||||
|
||||
// Calculate expiry date
|
||||
$date = new DateTime();
|
||||
if ($period === 'annual') {
|
||||
$date->modify('+1 year');
|
||||
} else {
|
||||
$date->modify('+1 month');
|
||||
}
|
||||
$expiryDate = $date->format('Y-m-d');
|
||||
|
||||
// Update user record in the database
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE users SET subscription_plan = ?, subscription_expires_at = ? WHERE id = ?");
|
||||
$stmt->execute([$plan, $expiryDate, $userId]);
|
||||
|
||||
// Set a session flash message
|
||||
$_SESSION['flash_message'] = t('subscription_success_message');
|
||||
|
||||
// Redirect to profile page
|
||||
header('Location: profile.php');
|
||||
exit;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// In a real app, you would log this error
|
||||
die("Database error: " . $e->getMessage());
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user