add: admin, add: fully customizable information via admin side
This commit is contained in:
1 parent
2d5a7850d5
commit
7eb61827f5
23 files changed
+2160
-178
No files matched your search
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
|
||||
header("Location: login.php");
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,811 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../models/settings.php';
|
||||
require_once __DIR__ . '/../models/projects.php';
|
||||
require_once __DIR__ . '/../models/certifications.php';
|
||||
require_once __DIR__ . '/../models/articles.php';
|
||||
require_once __DIR__ . '/../models/techstack.php';
|
||||
|
||||
// Generate CSRF token if not exists
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
$success_msg = '';
|
||||
$error_msg = '';
|
||||
|
||||
if (isset($_SESSION['success'])) {
|
||||
$success_msg = $_SESSION['success'];
|
||||
unset($_SESSION['success']);
|
||||
}
|
||||
if (isset($_SESSION['error'])) {
|
||||
$error_msg = $_SESSION['error'];
|
||||
unset($_SESSION['error']);
|
||||
}
|
||||
|
||||
// Determine current active tab
|
||||
$active_tab = isset($_GET['tab']) ? trim($_GET['tab']) : 'profile';
|
||||
if (isset($_GET['edit_project_id'])) $active_tab = 'projects';
|
||||
if (isset($_GET['edit_cert_id'])) $active_tab = 'certifications';
|
||||
if (isset($_GET['edit_article_id'])) $active_tab = 'articles';
|
||||
|
||||
// Handle Form Submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF check
|
||||
$post_token = $_POST['csrf_token'] ?? '';
|
||||
if (!hash_equals($_SESSION['csrf_token'], $post_token)) {
|
||||
$_SESSION['error'] = 'Security check failed. CSRF token invalid.';
|
||||
header("Location: dashboard.php?tab=" . urlencode($active_tab));
|
||||
exit();
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'update_settings') {
|
||||
$about_text_1 = trim($_POST['about_text_1'] ?? '');
|
||||
$about_text_2 = trim($_POST['about_text_2'] ?? '');
|
||||
$years_coding = trim($_POST['years_coding'] ?? '');
|
||||
$projects_count = trim($_POST['projects_count'] ?? '');
|
||||
$certifications_count = trim($_POST['certifications_count'] ?? '');
|
||||
$tiktok_url = trim($_POST['tiktok_url'] ?? '');
|
||||
$facebook_url = trim($_POST['facebook_url'] ?? '');
|
||||
$instagram_url = trim($_POST['instagram_url'] ?? '');
|
||||
$linkedin_url = trim($_POST['linkedin_url'] ?? '');
|
||||
$github_url = trim($_POST['github_url'] ?? '');
|
||||
|
||||
if (updateSettings($mysqli, $about_text_1, $about_text_2, $years_coding, $projects_count, $certifications_count, $tiktok_url, $facebook_url, $instagram_url, $linkedin_url, $github_url)) {
|
||||
$_SESSION['success'] = 'Profile settings updated successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to update profile settings.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=profile");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Projects CRUD Handlers
|
||||
if ($action === 'add_project') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$des = trim($_POST['des'] ?? '');
|
||||
$techstack = trim($_POST['techstack'] ?? '');
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$img_url = trim($_POST['img_url'] ?? '');
|
||||
|
||||
if (!empty($title) && !empty($des)) {
|
||||
if (addProject($mysqli, $title, $des, $techstack, $url, $img_url)) {
|
||||
$_SESSION['success'] = 'Project added successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to add project.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Title and description are required.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=projects");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'edit_project') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$des = trim($_POST['des'] ?? '');
|
||||
$techstack = trim($_POST['techstack'] ?? '');
|
||||
$url = trim($_POST['url'] ?? '');
|
||||
$img_url = trim($_POST['img_url'] ?? '');
|
||||
|
||||
if ($id > 0 && !empty($title) && !empty($des)) {
|
||||
if (editProject($mysqli, $id, $title, $des, $techstack, $url, $img_url)) {
|
||||
$_SESSION['success'] = 'Project updated successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to update project.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Invalid project data.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=projects");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'delete_project') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
if (deleteProject($mysqli, $id)) {
|
||||
$_SESSION['success'] = 'Project deleted successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to delete project.';
|
||||
}
|
||||
}
|
||||
header("Location: dashboard.php?tab=projects");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Certifications CRUD Handlers
|
||||
if ($action === 'add_certification') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$issuer = trim($_POST['issuer'] ?? '');
|
||||
$date_issued = trim($_POST['date_issued'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
|
||||
if (!empty($title) && !empty($issuer)) {
|
||||
if (addCertification($mysqli, $title, $issuer, $date_issued, $icon)) {
|
||||
$_SESSION['success'] = 'Certification added successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to add certification.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Title and issuer are required.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=certifications");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'edit_certification') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$issuer = trim($_POST['issuer'] ?? '');
|
||||
$date_issued = trim($_POST['date_issued'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? '');
|
||||
|
||||
if ($id > 0 && !empty($title) && !empty($issuer)) {
|
||||
if (editCertification($mysqli, $id, $title, $issuer, $date_issued, $icon)) {
|
||||
$_SESSION['success'] = 'Certification updated successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to update certification.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Invalid certification data.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=certifications");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'delete_certification') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
if (deleteCertification($mysqli, $id)) {
|
||||
$_SESSION['success'] = 'Certification deleted successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to delete certification.';
|
||||
}
|
||||
}
|
||||
header("Location: dashboard.php?tab=certifications");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Articles CRUD Handlers
|
||||
if ($action === 'add_article') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$excerpt = trim($_POST['excerpt'] ?? '');
|
||||
$date_published = trim($_POST['date_published'] ?? '');
|
||||
$tag = trim($_POST['tag'] ?? '');
|
||||
$url = trim($_POST['url'] ?? '#');
|
||||
|
||||
if (!empty($title) && !empty($excerpt)) {
|
||||
if (addArticle($mysqli, $title, $excerpt, $date_published, $tag, $url)) {
|
||||
$_SESSION['success'] = 'Article added successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to add article.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Title and excerpt are required.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=articles");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'edit_article') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$excerpt = trim($_POST['excerpt'] ?? '');
|
||||
$date_published = trim($_POST['date_published'] ?? '');
|
||||
$tag = trim($_POST['tag'] ?? '');
|
||||
$url = trim($_POST['url'] ?? '#');
|
||||
|
||||
if ($id > 0 && !empty($title) && !empty($excerpt)) {
|
||||
if (editArticle($mysqli, $id, $title, $excerpt, $date_published, $tag, $url)) {
|
||||
$_SESSION['success'] = 'Article updated successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to update article.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Invalid article data.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=articles");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'delete_article') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
if (deleteArticle($mysqli, $id)) {
|
||||
$_SESSION['success'] = 'Article deleted successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to delete article.';
|
||||
}
|
||||
}
|
||||
header("Location: dashboard.php?tab=articles");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Tech Stack Handlers
|
||||
if ($action === 'add_techstack') {
|
||||
$tech_name = trim($_POST['tech_name'] ?? '');
|
||||
if (!empty($tech_name)) {
|
||||
if (addTechStackItem($mysqli, $tech_name)) {
|
||||
$_SESSION['success'] = 'Tech stack item added successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to add tech stack item.';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error'] = 'Tech name cannot be empty.';
|
||||
}
|
||||
header("Location: dashboard.php?tab=techstack");
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($action === 'delete_techstack') {
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
if (deleteTechStackItem($mysqli, $id)) {
|
||||
$_SESSION['success'] = 'Tech stack item deleted successfully.';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Failed to delete tech stack item.';
|
||||
}
|
||||
}
|
||||
header("Location: dashboard.php?tab=techstack");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Load Data for Display
|
||||
$settings = getSettings($mysqli);
|
||||
$projects = getProjects($mysqli);
|
||||
$certifications = getCertifications($mysqli);
|
||||
$articles = getArticles($mysqli);
|
||||
$techstack = getTechStackRaw($mysqli);
|
||||
|
||||
// Retrieve individual edit records if requested
|
||||
$edit_project = null;
|
||||
if (isset($_GET['edit_project_id'])) {
|
||||
$edit_project = getProjectById($mysqli, intval($_GET['edit_project_id']));
|
||||
}
|
||||
|
||||
$edit_cert = null;
|
||||
if (isset($_GET['edit_cert_id'])) {
|
||||
$edit_cert = getCertificationById($mysqli, intval($_GET['edit_cert_id']));
|
||||
}
|
||||
|
||||
$edit_article = null;
|
||||
if (isset($_GET['edit_article_id'])) {
|
||||
$edit_article = getArticleById($mysqli, intval($_GET['edit_article_id']));
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard | Portfolio</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Mobile Header -->
|
||||
<header class="mobile-top-bar" style="display: none; width: 100%; justify-content: space-between; align-items: center; padding: 15px 20px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); position: fixed; top: 0; left: 0; z-index: 101;">
|
||||
<div class="logo-text" style="font-size: 1.25rem;">
|
||||
<span>Admin Panel</span>
|
||||
<span class="logo-dot"></span>
|
||||
</div>
|
||||
<button class="mobile-nav-toggle" id="mobile-toggle-btn" style="border: none; background: none; font-size: 1.5rem; color: #fff; cursor: pointer;">☰</button>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar Navigation -->
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="logo-container">
|
||||
<div class="logo-text">
|
||||
<span>Admin Panel</span>
|
||||
<span class="logo-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<button class="nav-link <?php echo $active_tab === 'profile' ? 'active' : ''; ?>" onclick="switchTab('profile')">👤 Profile & Stats</button>
|
||||
<button class="nav-link <?php echo $active_tab === 'projects' ? 'active' : ''; ?>" onclick="switchTab('projects')">💻 Projects</button>
|
||||
<button class="nav-link <?php echo $active_tab === 'certifications' ? 'active' : ''; ?>" onclick="switchTab('certifications')">🏆 Certifications</button>
|
||||
<button class="nav-link <?php echo $active_tab === 'articles' ? 'active' : ''; ?>" onclick="switchTab('articles')">📰 Blog Articles</button>
|
||||
<button class="nav-link <?php echo $active_tab === 'techstack' ? 'active' : ''; ?>" onclick="switchTab('techstack')">⚡ Tech Stack</button>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<a href="logout.php" class="logout-btn">Logout ➔</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content" id="main-content">
|
||||
<div class="top-bar">
|
||||
<div class="page-title">
|
||||
<h1 id="tab-title-text">Profile & Stats Management</h1>
|
||||
<p>Welcome back, <?php echo htmlspecialchars($_SESSION['admin_username']); ?>.</p>
|
||||
</div>
|
||||
<div class="user-badge">
|
||||
<span class="status-dot"></span>
|
||||
<span>Active Session</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Alerts -->
|
||||
<?php if (!empty($success_msg)): ?>
|
||||
<div class="alert alert-success">
|
||||
<span>✓</span>
|
||||
<div><?php echo htmlspecialchars($success_msg); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($error_msg)): ?>
|
||||
<div class="alert alert-danger">
|
||||
<span>⚠️</span>
|
||||
<div><?php echo htmlspecialchars($error_msg); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- ================= TAB: PROFILE ================= -->
|
||||
<div id="tab-profile" class="tab-pane <?php echo $active_tab === 'profile' ? 'active' : ''; ?>">
|
||||
<form action="dashboard.php?tab=profile" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="update_settings">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">📝 About Me Bio Copy</div>
|
||||
<div class="form-group">
|
||||
<label for="about_text_1">About Paragraph 1</label>
|
||||
<textarea name="about_text_1" id="about_text_1" required><?php echo htmlspecialchars($settings['about_text_1'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="about_text_2">About Paragraph 2</label>
|
||||
<textarea name="about_text_2" id="about_text_2" required><?php echo htmlspecialchars($settings['about_text_2'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">📈 Statistics Counters</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="years_coding">Years Coding Count (e.g. 3+)</label>
|
||||
<input type="text" name="years_coding" id="years_coding" value="<?php echo htmlspecialchars($settings['years_coding'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="projects_count">Projects Built Count (e.g. 10+)</label>
|
||||
<input type="text" name="projects_count" id="projects_count" value="<?php echo htmlspecialchars($settings['projects_count'] ?? ''); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="certifications_count">Certifications Count (e.g. 6+)</label>
|
||||
<input type="text" name="certifications_count" id="certifications_count" value="<?php echo htmlspecialchars($settings['certifications_count'] ?? ''); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">🔗 Social Links</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="tiktok_url">TikTok URL</label>
|
||||
<input type="url" name="tiktok_url" id="tiktok_url" value="<?php echo htmlspecialchars($settings['tiktok_url'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="github_url">GitHub URL</label>
|
||||
<input type="url" name="github_url" id="github_url" value="<?php echo htmlspecialchars($settings['github_url'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="linkedin_url">LinkedIn URL</label>
|
||||
<input type="url" name="linkedin_url" id="linkedin_url" value="<?php echo htmlspecialchars($settings['linkedin_url'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="facebook_url">Facebook URL</label>
|
||||
<input type="url" name="facebook_url" id="facebook_url" value="<?php echo htmlspecialchars($settings['facebook_url'] ?? ''); ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="instagram_url">Instagram URL</label>
|
||||
<input type="url" name="instagram_url" id="instagram_url" value="<?php echo htmlspecialchars($settings['instagram_url'] ?? ''); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn primary">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ================= TAB: PROJECTS ================= -->
|
||||
<div id="tab-projects" class="tab-pane <?php echo $active_tab === 'projects' ? 'active' : ''; ?>">
|
||||
<!-- Add or Edit Project Form -->
|
||||
<div class="card <?php echo $edit_project ? 'form-overlay-card' : ''; ?>">
|
||||
<div class="card-title">
|
||||
<?php echo $edit_project ? '✏️ Edit Project' : '➕ Add New Project'; ?>
|
||||
</div>
|
||||
<form action="dashboard.php?tab=projects" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="<?php echo $edit_project ? 'edit_project' : 'add_project'; ?>">
|
||||
<?php if ($edit_project): ?>
|
||||
<input type="hidden" name="id" value="<?php echo $edit_project['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="proj_title">Project Title</label>
|
||||
<input type="text" name="title" id="proj_title" value="<?php echo htmlspecialchars($edit_project['title'] ?? ''); ?>" required placeholder="e.g. UniMarket E-Commerce">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proj_tech">Tech Stack (comma-separated list)</label>
|
||||
<input type="text" name="techstack" id="proj_tech" value="<?php echo htmlspecialchars($edit_project['techstack'] ?? ''); ?>" placeholder="e.g. React, Node.js, Firebase">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proj_url">Project Link / URL</label>
|
||||
<input type="text" name="url" id="proj_url" value="<?php echo htmlspecialchars($edit_project['url'] ?? ''); ?>" placeholder="e.g. https://github.com/...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="proj_img">Preview Image URL</label>
|
||||
<input type="text" name="img_url" id="proj_img" value="<?php echo htmlspecialchars($edit_project['img_url'] ?? ''); ?>" placeholder="e.g. https://images.unsplash.com/...">
|
||||
</div>
|
||||
<div class="form-group form-group-full">
|
||||
<label for="proj_des">Project Description</label>
|
||||
<textarea name="des" id="proj_des" required placeholder="Describe the application features, challenges and stacks used..."><?php echo htmlspecialchars($edit_project['des'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap: 12px; margin-top: 10px;">
|
||||
<button type="submit" class="btn primary"><?php echo $edit_project ? 'Update Project' : 'Add Project'; ?></button>
|
||||
<?php if ($edit_project): ?>
|
||||
<a href="dashboard.php?tab=projects" class="btn secondary">Cancel</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Projects List -->
|
||||
<div class="card">
|
||||
<div class="card-title">📁 Current Projects</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Title</th>
|
||||
<th>Tech Stack</th>
|
||||
<th>Description</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($projects)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center" style="color: var(--text-muted);">No projects found. Add one above.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($projects as $proj): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if (!empty($proj['img_url'])): ?>
|
||||
<img src="<?php echo htmlspecialchars($proj['img_url']); ?>" alt="" style="width: 50px; height: 35px; object-fit: cover; border-radius: 4px; border: 1px solid var(--border-color);">
|
||||
<?php else: ?>
|
||||
<span style="font-size: 1.2rem;">🖼️</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-weight: 600; color: #fff;"><?php echo htmlspecialchars($proj['title']); ?></td>
|
||||
<td style="font-size: 0.85rem; color: var(--accent);"><?php echo htmlspecialchars($proj['techstack']); ?></td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-muted); max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><?php echo htmlspecialchars($proj['des']); ?></td>
|
||||
<td>
|
||||
<div class="btn-actions">
|
||||
<a href="dashboard.php?edit_project_id=<?php echo $proj['id']; ?>" class="btn secondary btn-sm">Edit</a>
|
||||
<form action="dashboard.php?tab=projects" method="POST" onsubmit="return confirm('Are you sure you want to delete this project?');" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="delete_project">
|
||||
<input type="hidden" name="id" value="<?php echo $proj['id']; ?>">
|
||||
<button type="submit" class="btn danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= TAB: CERTIFICATIONS ================= -->
|
||||
<div id="tab-certifications" class="tab-pane <?php echo $active_tab === 'certifications' ? 'active' : ''; ?>">
|
||||
<!-- Add or Edit Cert Form -->
|
||||
<div class="card <?php echo $edit_cert ? 'form-overlay-card' : ''; ?>">
|
||||
<div class="card-title">
|
||||
<?php echo $edit_cert ? '✏️ Edit Certification' : '➕ Add New Certification'; ?>
|
||||
</div>
|
||||
<form action="dashboard.php?tab=certifications" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="<?php echo $edit_cert ? 'edit_certification' : 'add_certification'; ?>">
|
||||
<?php if ($edit_cert): ?>
|
||||
<input type="hidden" name="id" value="<?php echo $edit_cert['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="cert_title">Certification Title</label>
|
||||
<input type="text" name="title" id="cert_title" value="<?php echo htmlspecialchars($edit_cert['title'] ?? ''); ?>" required placeholder="e.g. Responsive Web Design">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cert_issuer">Issuer / Provider</label>
|
||||
<input type="text" name="issuer" id="cert_issuer" value="<?php echo htmlspecialchars($edit_cert['issuer'] ?? ''); ?>" required placeholder="e.g. FreeCodeCamp">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cert_date">Date Issued</label>
|
||||
<input type="text" name="date_issued" id="cert_date" value="<?php echo htmlspecialchars($edit_cert['date_issued'] ?? ''); ?>" placeholder="e.g. Dec 2024">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cert_icon">Icon (Single Emoji)</label>
|
||||
<input type="text" name="icon" id="cert_icon" value="<?php echo htmlspecialchars($edit_cert['icon'] ?? '🏆'); ?>" placeholder="e.g. 🏆">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap: 12px; margin-top: 10px;">
|
||||
<button type="submit" class="btn primary"><?php echo $edit_cert ? 'Update Certification' : 'Add Certification'; ?></button>
|
||||
<?php if ($edit_cert): ?>
|
||||
<a href="dashboard.php?tab=certifications" class="btn secondary">Cancel</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Certs List -->
|
||||
<div class="card">
|
||||
<div class="card-title">🏆 Current Certifications</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;" class="text-center">Icon</th>
|
||||
<th>Title</th>
|
||||
<th>Issuer</th>
|
||||
<th>Date</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($certifications)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center" style="color: var(--text-muted);">No certifications found. Add one above.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($certifications as $cert): ?>
|
||||
<tr>
|
||||
<td class="text-center" style="font-size: 1.5rem;"><?php echo htmlspecialchars($cert['icon']); ?></td>
|
||||
<td style="font-weight: 600; color: #fff;"><?php echo htmlspecialchars($cert['title']); ?></td>
|
||||
<td style="color: var(--text-muted);"><?php echo htmlspecialchars($cert['issuer']); ?></td>
|
||||
<td><?php echo htmlspecialchars($cert['date_issued']); ?></td>
|
||||
<td>
|
||||
<div class="btn-actions">
|
||||
<a href="dashboard.php?edit_cert_id=<?php echo $cert['id']; ?>" class="btn secondary btn-sm">Edit</a>
|
||||
<form action="dashboard.php?tab=certifications" method="POST" onsubmit="return confirm('Are you sure you want to delete this certification?');" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="delete_certification">
|
||||
<input type="hidden" name="id" value="<?php echo $cert['id']; ?>">
|
||||
<button type="submit" class="btn danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= TAB: ARTICLES ================= -->
|
||||
<div id="tab-articles" class="tab-pane <?php echo $active_tab === 'articles' ? 'active' : ''; ?>">
|
||||
<!-- Add or Edit Article Form -->
|
||||
<div class="card <?php echo $edit_article ? 'form-overlay-card' : ''; ?>">
|
||||
<div class="card-title">
|
||||
<?php echo $edit_article ? '✏️ Edit Article' : '➕ Add New Blog Article'; ?>
|
||||
</div>
|
||||
<form action="dashboard.php?tab=articles" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="<?php echo $edit_article ? 'edit_article' : 'add_article'; ?>">
|
||||
<?php if ($edit_article): ?>
|
||||
<input type="hidden" name="id" value="<?php echo $edit_article['id']; ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="art_title">Article Title</label>
|
||||
<input type="text" name="title" id="art_title" value="<?php echo htmlspecialchars($edit_article['title'] ?? ''); ?>" required placeholder="e.g. Guide to React Custom Hooks">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="art_date">Publish Date</label>
|
||||
<input type="text" name="date_published" id="art_date" value="<?php echo htmlspecialchars($edit_article['date_published'] ?? ''); ?>" placeholder="e.g. Feb 10, 2026">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="art_tag">Tag / Category</label>
|
||||
<input type="text" name="tag" id="art_tag" value="<?php echo htmlspecialchars($edit_article['tag'] ?? ''); ?>" placeholder="e.g. React">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="art_url">Article Link / URL (external or '#')</label>
|
||||
<input type="text" name="url" id="art_url" value="<?php echo htmlspecialchars($edit_article['url'] ?? '#'); ?>" placeholder="e.g. # or external blog URL">
|
||||
</div>
|
||||
<div class="form-group form-group-full">
|
||||
<label for="art_excerpt">Short Excerpt</label>
|
||||
<textarea name="excerpt" id="art_excerpt" required placeholder="A brief description of what this article is about..."><?php echo htmlspecialchars($edit_article['excerpt'] ?? ''); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap: 12px; margin-top: 10px;">
|
||||
<button type="submit" class="btn primary"><?php echo $edit_article ? 'Update Article' : 'Add Article'; ?></button>
|
||||
<?php if ($edit_article): ?>
|
||||
<a href="dashboard.php?tab=articles" class="btn secondary">Cancel</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Articles List -->
|
||||
<div class="card">
|
||||
<div class="card-title">📰 Current Blog Articles</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Tag</th>
|
||||
<th>Published Date</th>
|
||||
<th>Excerpt</th>
|
||||
<th class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($articles)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center" style="color: var(--text-muted);">No articles found. Add one above.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($articles as $art): ?>
|
||||
<tr>
|
||||
<td style="font-weight: 600; color: #fff;"><?php echo htmlspecialchars($art['title']); ?></td>
|
||||
<td><span style="font-size: 0.85rem; padding: 4px 8px; background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.15); border-radius: 4px; color: var(--accent);"><?php echo htmlspecialchars($art['tag']); ?></span></td>
|
||||
<td><?php echo htmlspecialchars($art['date_published']); ?></td>
|
||||
<td style="font-size: 0.85rem; color: var(--text-muted); max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><?php echo htmlspecialchars($art['excerpt']); ?></td>
|
||||
<td>
|
||||
<div class="btn-actions">
|
||||
<a href="dashboard.php?edit_article_id=<?php echo $art['id']; ?>" class="btn secondary btn-sm">Edit</a>
|
||||
<form action="dashboard.php?tab=articles" method="POST" onsubmit="return confirm('Are you sure you want to delete this article?');" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="delete_article">
|
||||
<input type="hidden" name="id" value="<?php echo $art['id']; ?>">
|
||||
<button type="submit" class="btn danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= TAB: TECH STACK ================= -->
|
||||
<div id="tab-techstack" class="tab-pane <?php echo $active_tab === 'techstack' ? 'active' : ''; ?>">
|
||||
<!-- Add Tech Item -->
|
||||
<div class="card">
|
||||
<div class="card-title">➕ Add Technology</div>
|
||||
<form action="dashboard.php?tab=techstack" method="POST" style="display: flex; gap: 12px; align-items: flex-end;">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="add_techstack">
|
||||
<div class="form-group" style="flex-grow: 1; margin-bottom: 0;">
|
||||
<label for="tech_name">Technology Name</label>
|
||||
<input type="text" name="tech_name" id="tech_name" required placeholder="e.g. Next.js, GraphQL, PostgreSQL">
|
||||
</div>
|
||||
<button type="submit" class="btn primary">Add Tech</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Badges List -->
|
||||
<div class="card">
|
||||
<div class="card-title">⚡ Current Technologies</div>
|
||||
<?php if (empty($techstack)): ?>
|
||||
<p style="color: var(--text-muted);">No technologies configured. Add one above.</p>
|
||||
<?php else: ?>
|
||||
<div class="tech-badges-grid">
|
||||
<?php foreach ($techstack as $tech): ?>
|
||||
<div class="tech-badge-item">
|
||||
<span><?php echo htmlspecialchars($tech['tech_name']); ?></span>
|
||||
<form action="dashboard.php?tab=techstack" method="POST" style="display: inline;" onsubmit="return confirm('Delete this technology?');">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="delete_techstack">
|
||||
<input type="hidden" name="id" value="<?php echo $tech['id']; ?>">
|
||||
<button type="submit" class="tech-badge-delete" title="Delete tag">×</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Simple JS tab switching logic
|
||||
const tabs = {
|
||||
'profile': 'Profile & Stats Management',
|
||||
'projects': 'Projects Management',
|
||||
'certifications': 'Certifications Management',
|
||||
'articles': 'Blog Articles Management',
|
||||
'techstack': 'Tech Stack Management'
|
||||
};
|
||||
|
||||
function switchTab(tabId) {
|
||||
// Hide all tab panes
|
||||
document.querySelectorAll('.tab-pane').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
// Show selected tab pane
|
||||
const activePane = document.getElementById('tab-' + tabId);
|
||||
if (activePane) {
|
||||
activePane.classList.add('active');
|
||||
}
|
||||
|
||||
// Remove active class from nav links
|
||||
document.querySelectorAll('.nav-link').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active class to corresponding link (both desktop and mobile sidebar click)
|
||||
const activeLinks = document.querySelectorAll('.sidebar-nav button');
|
||||
activeLinks.forEach(link => {
|
||||
if (link.getAttribute('onclick') === `switchTab('${tabId}')`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update title text
|
||||
const titleEl = document.getElementById('tab-title-text');
|
||||
if (titleEl && tabs[tabId]) {
|
||||
titleEl.textContent = tabs[tabId];
|
||||
}
|
||||
|
||||
// Set URL parameter without reloading page for consistency on refresh
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('tab', tabId);
|
||||
// Clear edit params on tab switch
|
||||
url.searchParams.delete('edit_project_id');
|
||||
url.searchParams.delete('edit_cert_id');
|
||||
url.searchParams.delete('edit_article_id');
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
// Close sidebar on mobile
|
||||
closeSidebar();
|
||||
}
|
||||
|
||||
// Mobile Sidebar Toggling logic
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const mobileToggleBtn = document.getElementById('mobile-toggle-btn');
|
||||
|
||||
if (mobileToggleBtn) {
|
||||
mobileToggleBtn.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
function toggleSidebar(e) {
|
||||
if (e) e.stopPropagation();
|
||||
sidebar.classList.toggle('open');
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
sidebar.classList.remove('open');
|
||||
}
|
||||
|
||||
// Close sidebar if user clicks outside of it on mobile
|
||||
document.addEventListener('click', function(event) {
|
||||
const isClickInsideSidebar = sidebar.contains(event.target);
|
||||
const isClickOnToggle = mobileToggleBtn && mobileToggleBtn.contains(event.target);
|
||||
|
||||
if (!isClickInsideSidebar && !isClickOnToggle && window.innerWidth <= 1024) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Redirect if already logged in
|
||||
if (isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true) {
|
||||
header("Location: dashboard.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
|
||||
if (!empty($username) && !empty($password)) {
|
||||
$stmt = $mysqli->prepare("SELECT * FROM admin WHERE username = ?");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("s", $username);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result && $result->num_rows > 0) {
|
||||
$user = $result->fetch_assoc();
|
||||
if (password_verify($password, $user['password'])) {
|
||||
// Login successful
|
||||
$_SESSION['admin_logged_in'] = true;
|
||||
$_SESSION['admin_username'] = $user['username'];
|
||||
header("Location: dashboard.php");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
$error = 'Invalid username or password.';
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error = 'Database error. Please try again.';
|
||||
}
|
||||
} else {
|
||||
$error = 'Please enter both username and password.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login | Portfolio</title>
|
||||
<link rel="stylesheet" href="../assets/css/style.css">
|
||||
<style>
|
||||
.login-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background: #090d16;
|
||||
}
|
||||
.login-card {
|
||||
background: rgba(30, 41, 59, 0.4);
|
||||
border: 1px solid rgba(148, 163, 184, 0.12);
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.login-card h2 {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
color: #f8fafc;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.login-card p.subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: #f8fafc;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.back-home {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.back-home:hover {
|
||||
color: #3b82f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="login-wrapper">
|
||||
|
||||
<div class="login-card">
|
||||
<h2>Admin Panel</h2>
|
||||
<p class="subtitle">Enter your credentials to continue</p>
|
||||
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="error-message"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="login.php" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" name="username" id="username" placeholder="admin" required autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" placeholder="••••••••" required autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn primary full-width">Login</button>
|
||||
</form>
|
||||
|
||||
<a href="../index.php" class="back-home">← Back to Portfolio</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$_SESSION = array();
|
||||
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params["path"], $params["domain"],
|
||||
$params["secure"], $params["httponly"]
|
||||
);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit();
|
||||
?>
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-primary: #090d16;
|
||||
--bg-secondary: #0f172a;
|
||||
--bg-card: rgba(30, 41, 59, 0.4);
|
||||
--border-color: rgba(148, 163, 184, 0.08);
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--accent-glow: rgba(59, 130, 246, 0.15);
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--transition-speed: 0.25s;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-main);
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar Styling */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
transition: transform var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
padding: 12px 8px 32px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logo-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
transition: all var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
.nav-link:hover, .nav-link.active {
|
||||
color: var(--text-main);
|
||||
background-color: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
color: var(--danger);
|
||||
background-color: rgba(239, 68, 68, 0.05);
|
||||
border: 1px solid rgba(239, 68, 68, 0.1);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all var(--transition-speed) ease;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
box-shadow: 0 0 12px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
margin-left: 260px;
|
||||
padding: 40px;
|
||||
min-height: 100vh;
|
||||
transition: margin var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-title p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-badge .status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--success);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Tabs & Panes */
|
||||
.tab-pane {
|
||||
display: none;
|
||||
animation: fadeIn 0.35s ease;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Forms and Inputs */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.form-group-full {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: #f8fafc;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="url"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||
border-radius: 8px;
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
transition: all var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
background-color: rgba(15, 23, 42, 0.9);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* Alert Banners */
|
||||
.alert {
|
||||
padding: 16px 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateY(-10px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-speed) ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background-color: var(--accent-hover);
|
||||
box-shadow: 0 0 16px var(--accent-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background-color: rgba(148, 163, 184, 0.1);
|
||||
color: var(--text-main);
|
||||
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||
}
|
||||
|
||||
.btn.secondary:hover {
|
||||
background-color: rgba(148, 163, 184, 0.18);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #fca5a5;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background-color: var(--danger);
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 14px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Management Tables / List Views */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-top: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba(15, 23, 42, 0.4);
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background-color: rgba(148, 163, 184, 0.02);
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Tech Stack Specifics */
|
||||
.tech-badges-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tech-badge-item {
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tech-badge-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tech-badge-delete:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Hamburger for Mobile Dashboard */
|
||||
.mobile-nav-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-main);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Form panel overlays (Modals / Edit forms) */
|
||||
.form-overlay-card {
|
||||
border: 1.5px solid rgba(59, 130, 246, 0.25);
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
position: relative;
|
||||
animation: zoomIn 0.3s ease;
|
||||
}
|
||||
|
||||
.form-overlay-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--accent), #8b5cf6);
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from { transform: scale(0.97); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.mobile-nav-toggle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.form-group-full {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.top-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.user-badge {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -43,7 +43,7 @@ html, body { overflow-x: hidden; max-width: 100%; }
|
||||
}
|
||||
|
||||
/* --- Light Mode Overrides --- */
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper {
|
||||
body:has(#dark-mode-toggle:not(:checked)) {
|
||||
--bg: #f8fafc;
|
||||
--bg-secondary: #f1f5f9;
|
||||
--card-bg: #ffffff;
|
||||
@@ -232,10 +232,10 @@ nav {
|
||||
}
|
||||
.mode-btn:hover { border-color: var(--border-hover); transform: scale(1.05); }
|
||||
|
||||
.toggle-checkbox:checked ~ .site-wrapper .icon-sun { display: none; }
|
||||
.toggle-checkbox:checked ~ .site-wrapper .icon-moon { display: flex; }
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .icon-moon { display: none; }
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .icon-sun { display: flex; }
|
||||
body:has(#dark-mode-toggle:checked) .icon-sun { display: none; }
|
||||
body:has(#dark-mode-toggle:checked) .icon-moon { display: flex; }
|
||||
body:has(#dark-mode-toggle:not(:checked)) .icon-moon { display: none; }
|
||||
body:has(#dark-mode-toggle:not(:checked)) .icon-sun { display: flex; }
|
||||
|
||||
/* Hamburger */
|
||||
.hamburger {
|
||||
@@ -429,11 +429,11 @@ h1 {
|
||||
.hero-img-container:hover .img-night-normal { opacity: 0; }
|
||||
|
||||
/* Light mode image states */
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .img-night-normal,
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .img-night-hover { opacity: 0 !important; }
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .img-day-normal { opacity: 1; z-index: 3; cursor: default; }
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .hero-img-container:hover .img-day-hover { opacity: 1; z-index: 4; cursor: default; }
|
||||
.toggle-checkbox:not(:checked) ~ .site-wrapper .hero-img-container:hover .img-day-normal { opacity: 0; }
|
||||
body:has(#dark-mode-toggle:not(:checked)) .img-night-normal,
|
||||
body:has(#dark-mode-toggle:not(:checked)) .img-night-hover { opacity: 0 !important; }
|
||||
body:has(#dark-mode-toggle:not(:checked)) .img-day-normal { opacity: 1; z-index: 3; cursor: default; }
|
||||
body:has(#dark-mode-toggle:not(:checked)) .hero-img-container:hover .img-day-hover { opacity: 1; z-index: 4; cursor: default; }
|
||||
body:has(#dark-mode-toggle:not(:checked)) .hero-img-container:hover .img-day-normal { opacity: 0; }
|
||||
|
||||
.glow-effect {
|
||||
position: absolute;
|
||||
|
||||
@@ -20,10 +20,21 @@
|
||||
|
||||
<div class="site-wrapper">
|
||||
|
||||
<nav>
|
||||
<nav id="main-nav">
|
||||
<div class="logo">CG.</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php">← Home</a>
|
||||
|
||||
<div class="hamburger" id="hamburger" aria-label="Toggle menu" aria-expanded="false" role="button" tabindex="0">
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
|
||||
<div class="nav-links" id="nav-links" role="navigation">
|
||||
<a href="index.php#about" class="nav-item">About</a>
|
||||
<a href="index.php#projects" class="nav-item">Work</a>
|
||||
<a href="index.php#certificates" class="nav-item">Certs</a>
|
||||
<a href="blog.php" class="nav-item">Blog</a>
|
||||
<a href="index.php#contact" class="nav-item">Contact</a>
|
||||
<label for="dark-mode-toggle" class="mode-btn" title="Toggle Theme" aria-label="Toggle dark/light mode">
|
||||
<span class="icon-moon">🌙</span>
|
||||
<span class="icon-sun">☀️</span>
|
||||
@@ -41,47 +52,7 @@
|
||||
|
||||
<section class="section-pad reveal">
|
||||
<div class="blog-grid">
|
||||
|
||||
<article class="blog-card reveal reveal-delay-1">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Feb 10, 2026</span>
|
||||
<span class="blog-tag">React</span>
|
||||
</div>
|
||||
<h3 class="blog-title">Guide to React Custom Hooks</h3>
|
||||
<p class="blog-excerpt">Learn how to write clean, reusable React components.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
|
||||
<article class="blog-card reveal reveal-delay-2">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Jan 22, 2026</span>
|
||||
<span class="blog-tag">DevOps</span>
|
||||
</div>
|
||||
<h3 class="blog-title">Using Docker for PHP Development</h3>
|
||||
<p class="blog-excerpt">How to set up PHP and Node.js projects using Docker.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
|
||||
<article class="blog-card reveal reveal-delay-3">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Dec 05, 2025</span>
|
||||
<span class="blog-tag">Career</span>
|
||||
</div>
|
||||
<h3 class="blog-title">My Experience as an IT Student</h3>
|
||||
<p class="blog-excerpt">Balancing school projects, freelance work, and learning new skills.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
|
||||
<article class="blog-card reveal reveal-delay-4">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Nov 15, 2025</span>
|
||||
<span class="blog-tag">CSS</span>
|
||||
</div>
|
||||
<h3 class="blog-title">Implementing Dark Mode with CSS</h3>
|
||||
<p class="blog-excerpt">How to build a simple dark mode toggle using pure CSS.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
|
||||
<?php include('views/articles.php'); ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -111,6 +82,41 @@
|
||||
const themeToggle = document.getElementById("dark-mode-toggle");
|
||||
themeToggle.checked = localStorage.getItem("theme") !== "light";
|
||||
themeToggle.addEventListener("change", () => localStorage.setItem("theme", themeToggle.checked ? "dark" : "light"));
|
||||
|
||||
// HAMBURGER MENU
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const navLinks = document.getElementById("nav-links");
|
||||
const navItems = document.querySelectorAll(".nav-item");
|
||||
|
||||
function closeMenu() {
|
||||
hamburger.classList.remove("active");
|
||||
navLinks.classList.remove("active");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
hamburger.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = hamburger.classList.toggle("active");
|
||||
navLinks.classList.toggle("active");
|
||||
hamburger.setAttribute("aria-expanded", isOpen);
|
||||
});
|
||||
|
||||
// Close on nav link click
|
||||
navItems.forEach(item => item.addEventListener("click", closeMenu));
|
||||
|
||||
// Close when clicking outside the menu
|
||||
document.addEventListener("click", (e) => {
|
||||
if (navLinks.classList.contains("active") && !navLinks.contains(e.target) && !hamburger.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
if (navLinks.classList.contains('active')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+50
-59
@@ -20,11 +20,21 @@
|
||||
|
||||
<div class="site-wrapper">
|
||||
|
||||
<nav>
|
||||
<nav id="main-nav">
|
||||
<div class="logo">CG.</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php">← Home</a>
|
||||
<a href="projects.php">Projects</a>
|
||||
|
||||
<div class="hamburger" id="hamburger" aria-label="Toggle menu" aria-expanded="false" role="button" tabindex="0">
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
|
||||
<div class="nav-links" id="nav-links" role="navigation">
|
||||
<a href="index.php#about" class="nav-item">About</a>
|
||||
<a href="index.php#projects" class="nav-item">Work</a>
|
||||
<a href="index.php#certificates" class="nav-item">Certs</a>
|
||||
<a href="blog.php" class="nav-item">Blog</a>
|
||||
<a href="index.php#contact" class="nav-item">Contact</a>
|
||||
<label for="dark-mode-toggle" class="mode-btn" title="Toggle Theme" aria-label="Toggle dark/light mode">
|
||||
<span class="icon-moon">🌙</span>
|
||||
<span class="icon-sun">☀️</span>
|
||||
@@ -42,61 +52,7 @@
|
||||
|
||||
<section class="section-pad">
|
||||
<div class="cert-grid">
|
||||
|
||||
<div class="cert-card reveal reveal-delay-1">
|
||||
<div class="cert-icon">🏆</div>
|
||||
<div class="cert-info">
|
||||
<h3>Responsive Web Design</h3>
|
||||
<p class="issuer">FreeCodeCamp</p>
|
||||
<span class="date">Issued Dec 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-card reveal reveal-delay-2">
|
||||
<div class="cert-icon">📜</div>
|
||||
<div class="cert-info">
|
||||
<h3>JavaScript Algorithms</h3>
|
||||
<p class="issuer">HackerRank</p>
|
||||
<span class="date">Issued Nov 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-card reveal reveal-delay-3">
|
||||
<div class="cert-icon">☁️</div>
|
||||
<div class="cert-info">
|
||||
<h3>Cloud Fundamentals</h3>
|
||||
<p class="issuer">AWS</p>
|
||||
<span class="date">Issued Oct 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-card reveal reveal-delay-1">
|
||||
<div class="cert-icon">🐍</div>
|
||||
<div class="cert-info">
|
||||
<h3>Python for Data Science</h3>
|
||||
<p class="issuer">Coursera</p>
|
||||
<span class="date">Issued Sept 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-card reveal reveal-delay-2">
|
||||
<div class="cert-icon">🔒</div>
|
||||
<div class="cert-info">
|
||||
<h3>Cybersecurity Essentials</h3>
|
||||
<p class="issuer">Cisco</p>
|
||||
<span class="date">Issued Aug 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cert-card reveal reveal-delay-3">
|
||||
<div class="cert-icon">⚛️</div>
|
||||
<div class="cert-info">
|
||||
<h3>React Advanced Patterns</h3>
|
||||
<p class="issuer">Udemy</p>
|
||||
<span class="date">Issued July 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('views/certifications.php'); ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -126,6 +82,41 @@
|
||||
const themeToggle = document.getElementById("dark-mode-toggle");
|
||||
themeToggle.checked = localStorage.getItem("theme") !== "light";
|
||||
themeToggle.addEventListener("change", () => localStorage.setItem("theme", themeToggle.checked ? "dark" : "light"));
|
||||
|
||||
// HAMBURGER MENU
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const navLinks = document.getElementById("nav-links");
|
||||
const navItems = document.querySelectorAll(".nav-item");
|
||||
|
||||
function closeMenu() {
|
||||
hamburger.classList.remove("active");
|
||||
navLinks.classList.remove("active");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
hamburger.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = hamburger.classList.toggle("active");
|
||||
navLinks.classList.toggle("active");
|
||||
hamburger.setAttribute("aria-expanded", isOpen);
|
||||
});
|
||||
|
||||
// Close on nav link click
|
||||
navItems.forEach(item => item.addEventListener("click", closeMenu));
|
||||
|
||||
// Close when clicking outside the menu
|
||||
document.addEventListener("click", (e) => {
|
||||
if (navLinks.classList.contains("active") && !navLinks.contains(e.target) && !hamburger.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
if (navLinks.classList.contains('active')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../models/articles.php";
|
||||
|
||||
function showArticles($mysqli) {
|
||||
return getArticles($mysqli);
|
||||
}
|
||||
|
||||
function showThreeArticles($mysqli) {
|
||||
return getArticles($mysqli, 3);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../models/certifications.php";
|
||||
|
||||
function showCertifications($mysqli) {
|
||||
return getCertifications($mysqli);
|
||||
}
|
||||
|
||||
function showThreeCertifications($mysqli) {
|
||||
return getCertifications($mysqli, 3);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../models/settings.php";
|
||||
|
||||
function showSettings($mysqli) {
|
||||
return getSettings($mysqli);
|
||||
}
|
||||
?>
|
||||
@@ -17,6 +17,25 @@ if (!function_exists('loadEnv')) {
|
||||
}
|
||||
loadEnv(__DIR__ . '/.env');
|
||||
|
||||
require_once __DIR__ . '/config/db.php';
|
||||
require_once __DIR__ . '/controllers/SettingsController.php';
|
||||
|
||||
$settings = showSettings($mysqli);
|
||||
if (!$settings) {
|
||||
$settings = [
|
||||
'about_text_1' => 'I am a senior IT student majoring in Software Development. I write code to solve real-world problems.',
|
||||
'about_text_2' => 'My primary stack includes PHP, React, Node.js, and MySQL. I enjoy database design, front-end development, and bug fixing.',
|
||||
'years_coding' => '3+',
|
||||
'projects_count' => '10+',
|
||||
'certifications_count' => '6+',
|
||||
'tiktok_url' => 'https://www.tiktok.com/@crischarlesgarcia',
|
||||
'facebook_url' => 'https://facebook.com/kristyarls345',
|
||||
'instagram_url' => 'https://instagram.com/crischarlesgarcia',
|
||||
'linkedin_url' => 'https://www.linkedin.com/in/cris-charles-garcia-187415303',
|
||||
'github_url' => 'https://github.com/ssbg04'
|
||||
];
|
||||
}
|
||||
|
||||
$recaptchaSiteKey = getenv('RECAPTCHA_SITE_KEY') ?: ($_ENV['RECAPTCHA_SITE_KEY'] ?? '');
|
||||
$contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL'] ?? '');
|
||||
?>
|
||||
@@ -101,8 +120,8 @@ $contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL']
|
||||
</div>
|
||||
<div class="about-content">
|
||||
<div class="about-text">
|
||||
<p>I am a senior <strong>IT student majoring in Software Development</strong>. I write code to solve real-world problems.</p>
|
||||
<p>My primary stack includes <strong>PHP</strong>, <strong>React</strong>, <strong>Node.js</strong>, and <strong>MySQL</strong>. I enjoy database design, front-end development, and bug fixing.</p>
|
||||
<p><?php echo nl2br(htmlspecialchars($settings['about_text_1'])); ?></p>
|
||||
<p><?php echo nl2br(htmlspecialchars($settings['about_text_2'])); ?></p>
|
||||
</div>
|
||||
<div class="tech-stack">
|
||||
<h3>Tech Stack</h3>
|
||||
@@ -112,15 +131,15 @@ $contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL']
|
||||
|
||||
<div class="stats-row reveal reveal-delay-2">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">3+</div>
|
||||
<div class="stat-number"><?php echo htmlspecialchars($settings['years_coding']); ?></div>
|
||||
<div class="stat-label">Years Coding</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">10+</div>
|
||||
<div class="stat-number"><?php echo htmlspecialchars($settings['projects_count']); ?></div>
|
||||
<div class="stat-label">Projects Built</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">5+</div>
|
||||
<div class="stat-number"><?php echo htmlspecialchars($settings['certifications_count']); ?></div>
|
||||
<div class="stat-label">Certifications</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,30 +170,7 @@ $contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL']
|
||||
<h2 class="section-title">My Certifications</h2>
|
||||
</div>
|
||||
<div class="cert-grid">
|
||||
<div class="cert-card reveal reveal-delay-1">
|
||||
<div class="cert-icon">🏆</div>
|
||||
<div class="cert-info">
|
||||
<h3>Responsive Web Design</h3>
|
||||
<p class="issuer">FreeCodeCamp</p>
|
||||
<span class="date">Issued Dec 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-card reveal reveal-delay-2">
|
||||
<div class="cert-icon">📜</div>
|
||||
<div class="cert-info">
|
||||
<h3>JavaScript Algorithms</h3>
|
||||
<p class="issuer">HackerRank</p>
|
||||
<span class="date">Issued Nov 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cert-card reveal reveal-delay-3">
|
||||
<div class="cert-icon">☁️</div>
|
||||
<div class="cert-info">
|
||||
<h3>Cloud Fundamentals</h3>
|
||||
<p class="issuer">AWS</p>
|
||||
<span class="date">Issued Oct 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('views/tcertifications.php'); ?>
|
||||
</div>
|
||||
<div class="view-all-container">
|
||||
<a href="certifications.php" class="btn secondary" id="btn-all-certs">All Certifications →</a>
|
||||
@@ -190,33 +186,7 @@ $contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL']
|
||||
<h2 class="section-title">Latest Articles</h2>
|
||||
</div>
|
||||
<div class="blog-grid">
|
||||
<article class="blog-card reveal reveal-delay-1">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Feb 10, 2026</span>
|
||||
<span class="blog-tag">React</span>
|
||||
</div>
|
||||
<h3 class="blog-title">Guide to React Custom Hooks</h3>
|
||||
<p class="blog-excerpt">Learn how to write clean, reusable React components.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
<article class="blog-card reveal reveal-delay-2">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Jan 22, 2026</span>
|
||||
<span class="blog-tag">DevOps</span>
|
||||
</div>
|
||||
<h3 class="blog-title">Using Docker for PHP Development</h3>
|
||||
<p class="blog-excerpt">How to set up PHP and Node.js projects using Docker.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
<article class="blog-card reveal reveal-delay-3">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date">Dec 05, 2025</span>
|
||||
<span class="blog-tag">Career</span>
|
||||
</div>
|
||||
<h3 class="blog-title">My Experience as an IT Student</h3>
|
||||
<p class="blog-excerpt">Balancing school projects, freelance work, and learning new skills.</p>
|
||||
<a href="#" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
<?php include('views/tarticles.php'); ?>
|
||||
</div>
|
||||
<div class="view-all-container">
|
||||
<a href="blog.php" class="btn secondary" id="btn-all-articles">All Articles →</a>
|
||||
@@ -234,22 +204,43 @@ $contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL']
|
||||
<div class="contact-container">
|
||||
<p>Feel free to reach out if you have a project idea, a job opportunity, or any questions.</p>
|
||||
|
||||
<?php if (isset($_GET['status'])): ?>
|
||||
<?php if ($_GET['status'] === 'success'): ?>
|
||||
<p style="color: #22c55e; font-weight: 500; background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center;">Your message has been sent successfully!</p>
|
||||
<?php elseif ($_GET['status'] === 'error'): ?>
|
||||
<p style="color: #ef4444; font-weight: 500; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center;"><?php echo htmlspecialchars($_GET['msg'] ?? 'An error occurred. Please try again.'); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="social-links">
|
||||
<a href="https://github.com/ssbg04" target="_blank" class="social-btn" aria-label="GitHub" id="link-github">
|
||||
<?php if (!empty($settings['github_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($settings['github_url']); ?>" target="_blank" class="social-btn" aria-label="GitHub" id="link-github">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/in/cris-charles-garcia-187415303" target="_blank" class="social-btn" aria-label="LinkedIn" id="link-linkedin">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($settings['linkedin_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($settings['linkedin_url']); ?>" target="_blank" class="social-btn" aria-label="LinkedIn" id="link-linkedin">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>
|
||||
</a>
|
||||
<a href="https://facebook.com/kristyarls345" target="_blank" class="social-btn" aria-label="Facebook" id="link-facebook">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($settings['facebook_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($settings['facebook_url']); ?>" target="_blank" class="social-btn" aria-label="Facebook" id="link-facebook">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
|
||||
</a>
|
||||
<a href="https://instagram.com/crischarlesgarcia" target="_blank" class="social-btn" aria-label="Instagram" id="link-instagram">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($settings['instagram_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($settings['instagram_url']); ?>" target="_blank" class="social-btn" aria-label="Instagram" id="link-instagram">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($settings['tiktok_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($settings['tiktok_url']); ?>" target="_blank" class="social-btn" aria-label="TikTok" id="link-tiktok">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5"></path></svg>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form action="https://formsubmit.co/<?php echo htmlspecialchars($contactEmail); ?>" method="POST" class="email-form" id="contact-form">
|
||||
<form action="send_contact.php" method="POST" class="email-form" id="contact-form">
|
||||
<input type="hidden" name="_captcha" value="false">
|
||||
<input type="text" name="name" id="form-name" placeholder="Name" required>
|
||||
<input type="text" name="subject" id="form-subject" placeholder="Subject" required>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
|
||||
function getArticles($mysqli, $limit = null) {
|
||||
$articles = [];
|
||||
$sql = "SELECT * FROM articles ORDER BY id DESC";
|
||||
if ($limit !== null) {
|
||||
$sql .= " LIMIT " . intval($limit);
|
||||
}
|
||||
$result = $mysqli->query($sql);
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$articles[] = $row;
|
||||
}
|
||||
}
|
||||
return $articles;
|
||||
}
|
||||
|
||||
// Function alias for compatibility with any controllers using showThreeArticles
|
||||
function getThreeArticles($mysqli) {
|
||||
return getArticles($mysqli, 3);
|
||||
}
|
||||
|
||||
function getArticleById($mysqli, $id) {
|
||||
$sql = "SELECT * FROM articles WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addArticle($mysqli, $title, $excerpt, $date_published, $tag, $url) {
|
||||
$sql = "INSERT INTO articles (title, excerpt, date_published, tag, url) VALUES (?, ?, ?, ?, ?)";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("sssss", $title, $excerpt, $date_published, $tag, $url);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function editArticle($mysqli, $id, $title, $excerpt, $date_published, $tag, $url) {
|
||||
$sql = "UPDATE articles SET title = ?, excerpt = ?, date_published = ?, tag = ?, url = ? WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("sssssi", $title, $excerpt, $date_published, $tag, $url, $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteArticle($mysqli, $id) {
|
||||
$sql = "DELETE FROM articles WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
|
||||
function getCertifications($mysqli, $limit = null) {
|
||||
$certs = [];
|
||||
$sql = "SELECT * FROM certifications ORDER BY id DESC";
|
||||
if ($limit !== null) {
|
||||
$sql .= " LIMIT " . intval($limit);
|
||||
}
|
||||
$result = $mysqli->query($sql);
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$certs[] = $row;
|
||||
}
|
||||
}
|
||||
return $certs;
|
||||
}
|
||||
|
||||
function getCertificationById($mysqli, $id) {
|
||||
$sql = "SELECT * FROM certifications WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addCertification($mysqli, $title, $issuer, $date_issued, $icon) {
|
||||
$sql = "INSERT INTO certifications (title, issuer, date_issued, icon) VALUES (?, ?, ?, ?)";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("ssss", $title, $issuer, $date_issued, $icon);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function editCertification($mysqli, $id, $title, $issuer, $date_issued, $icon) {
|
||||
$sql = "UPDATE certifications SET title = ?, issuer = ?, date_issued = ?, icon = ? WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("ssssi", $title, $issuer, $date_issued, $icon, $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteCertification($mysqli, $id) {
|
||||
$sql = "DELETE FROM certifications WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -31,4 +31,45 @@ function getThreeProjects($mysqli){
|
||||
return $projects;
|
||||
}
|
||||
|
||||
function getProjectById($mysqli, $id) {
|
||||
$sql = "SELECT * FROM projects WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addProject($mysqli, $title, $des, $techstack, $url, $img_url) {
|
||||
$sql = "INSERT INTO projects (title, des, techstack, url, img_url) VALUES (?, ?, ?, ?, ?)";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("sssss", $title, $des, $techstack, $url, $img_url);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function editProject($mysqli, $id, $title, $des, $techstack, $url, $img_url) {
|
||||
$sql = "UPDATE projects SET title = ?, des = ?, techstack = ?, url = ?, img_url = ? WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("sssssi", $title, $des, $techstack, $url, $img_url, $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteProject($mysqli, $id) {
|
||||
$sql = "DELETE FROM projects WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
|
||||
function getSettings($mysqli) {
|
||||
$sql = "SELECT * FROM settings WHERE id = 1";
|
||||
$result = $mysqli->query($sql);
|
||||
if ($result && $result->num_rows > 0) {
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateSettings($mysqli, $about_text_1, $about_text_2, $years_coding, $projects_count, $certifications_count, $tiktok_url, $facebook_url, $instagram_url, $linkedin_url, $github_url) {
|
||||
$sql = "UPDATE settings SET
|
||||
about_text_1 = ?,
|
||||
about_text_2 = ?,
|
||||
years_coding = ?,
|
||||
projects_count = ?,
|
||||
certifications_count = ?,
|
||||
tiktok_url = ?,
|
||||
facebook_url = ?,
|
||||
instagram_url = ?,
|
||||
linkedin_url = ?,
|
||||
github_url = ?
|
||||
WHERE id = 1";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("ssssssssss", $about_text_1, $about_text_2, $years_coding, $projects_count, $certifications_count, $tiktok_url, $facebook_url, $instagram_url, $linkedin_url, $github_url);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -15,4 +15,36 @@ function getTechStack($mysqli){
|
||||
|
||||
return $techs;
|
||||
}
|
||||
|
||||
function getTechStackRaw($mysqli) {
|
||||
$sql = "SELECT * FROM techstack ORDER BY id ASC";
|
||||
$result = $mysqli->query($sql);
|
||||
$techs = [];
|
||||
if ($result) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$techs[] = $row;
|
||||
}
|
||||
}
|
||||
return $techs;
|
||||
}
|
||||
|
||||
function addTechStackItem($mysqli, $tech_name) {
|
||||
$sql = "INSERT INTO techstack (tech_name) VALUES (?)";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("s", $tech_name);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteTechStackItem($mysqli, $id) {
|
||||
$sql = "DELETE FROM techstack WHERE id = ?";
|
||||
$stmt = $mysqli->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
+49
-4
@@ -20,11 +20,21 @@
|
||||
|
||||
<div class="site-wrapper">
|
||||
|
||||
<nav>
|
||||
<nav id="main-nav">
|
||||
<div class="logo">CG.</div>
|
||||
<div class="nav-links">
|
||||
<a href="index.php">← Home</a>
|
||||
<a href="certifications.php">Certs</a>
|
||||
|
||||
<div class="hamburger" id="hamburger" aria-label="Toggle menu" aria-expanded="false" role="button" tabindex="0">
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
|
||||
<div class="nav-links" id="nav-links" role="navigation">
|
||||
<a href="index.php#about" class="nav-item">About</a>
|
||||
<a href="index.php#projects" class="nav-item">Work</a>
|
||||
<a href="index.php#certificates" class="nav-item">Certs</a>
|
||||
<a href="blog.php" class="nav-item">Blog</a>
|
||||
<a href="index.php#contact" class="nav-item">Contact</a>
|
||||
<label for="dark-mode-toggle" class="mode-btn" title="Toggle Theme" aria-label="Toggle dark/light mode">
|
||||
<span class="icon-moon">🌙</span>
|
||||
<span class="icon-sun">☀️</span>
|
||||
@@ -72,6 +82,41 @@
|
||||
const themeToggle = document.getElementById("dark-mode-toggle");
|
||||
themeToggle.checked = localStorage.getItem("theme") !== "light";
|
||||
themeToggle.addEventListener("change", () => localStorage.setItem("theme", themeToggle.checked ? "dark" : "light"));
|
||||
|
||||
// HAMBURGER MENU
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const navLinks = document.getElementById("nav-links");
|
||||
const navItems = document.querySelectorAll(".nav-item");
|
||||
|
||||
function closeMenu() {
|
||||
hamburger.classList.remove("active");
|
||||
navLinks.classList.remove("active");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
hamburger.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = hamburger.classList.toggle("active");
|
||||
navLinks.classList.toggle("active");
|
||||
hamburger.setAttribute("aria-expanded", isOpen);
|
||||
});
|
||||
|
||||
// Close on nav link click
|
||||
navItems.forEach(item => item.addEventListener("click", closeMenu));
|
||||
|
||||
// Close when clicking outside the menu
|
||||
document.addEventListener("click", (e) => {
|
||||
if (navLinks.classList.contains("active") && !navLinks.contains(e.target) && !hamburger.contains(e.target)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
if (navLinks.classList.contains('active')) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config/db.php';
|
||||
|
||||
// Retrieve client IP address
|
||||
function getClientIp() {
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
return $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
// In case of multiple IPs, get the first one
|
||||
$ipList = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ipList[0]);
|
||||
} else {
|
||||
return $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header("Location: index.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$clientIp = getClientIp();
|
||||
$rateLimit = intval(getenv('RATE_LIMIT') ?: ($_ENV['RATE_LIMIT'] ?? 10));
|
||||
|
||||
// Query submissions count from this IP address in the last hour
|
||||
$stmt = $mysqli->prepare("SELECT COUNT(*) as count FROM email_submissions WHERE ip_address = ? AND submitted_at >= NOW() - INTERVAL 1 HOUR");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("s", $clientIp);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
$submissionCount = $result['count'] ?? 0;
|
||||
$stmt->close();
|
||||
} else {
|
||||
$submissionCount = 0;
|
||||
}
|
||||
|
||||
if ($submissionCount >= $rateLimit) {
|
||||
header("Location: index.php?status=error&msg=Rate limit exceeded. You can only send $rateLimit emails per hour.#contact");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Log submission to database
|
||||
$logStmt = $mysqli->prepare("INSERT INTO email_submissions (ip_address) VALUES (?)");
|
||||
if ($logStmt) {
|
||||
$logStmt->bind_param("s", $clientIp);
|
||||
$logStmt->execute();
|
||||
$logStmt->close();
|
||||
}
|
||||
|
||||
// Forward submission to FormSubmit via cURL
|
||||
$contactEmail = getenv('CONTACT_EMAIL') ?: ($_ENV['CONTACT_EMAIL'] ?? 'crischarlesgarcia345@gmail.com');
|
||||
$url = "https://formsubmit.co/" . $contactEmail;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($_POST));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false || $httpCode >= 400) {
|
||||
header("Location: index.php?status=error&msg=Failed to send message via mail service. Please try again later.#contact");
|
||||
} else {
|
||||
header("Location: index.php?status=success#contact");
|
||||
}
|
||||
exit();
|
||||
?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
require_once __DIR__ . "/../controllers/ArticlesController.php";
|
||||
|
||||
$articles = showArticles($mysqli);
|
||||
?>
|
||||
|
||||
<?php if (!empty($articles)): ?>
|
||||
<?php
|
||||
$delay = 1;
|
||||
foreach ($articles as $art):
|
||||
$delayClass = "reveal-delay-" . (($delay % 3 === 0) ? 3 : ($delay % 3));
|
||||
?>
|
||||
<article class="blog-card reveal <?php echo $delayClass; ?>">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date"><?php echo htmlspecialchars($art['date_published']); ?></span>
|
||||
<span class="blog-tag"><?php echo htmlspecialchars($art['tag']); ?></span>
|
||||
</div>
|
||||
<h3 class="blog-title"><?php echo htmlspecialchars($art['title']); ?></h3>
|
||||
<p class="blog-excerpt"><?php echo htmlspecialchars($art['excerpt']); ?></p>
|
||||
<a href="<?php echo htmlspecialchars($art['url']); ?>" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
<?php
|
||||
$delay++;
|
||||
endforeach;
|
||||
?>
|
||||
<?php else: ?>
|
||||
<p style="color: var(--text-muted); grid-column: 1/-1; text-align:center; padding: 40px 0;">No articles found.</p>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
require_once __DIR__ . "/../controllers/CertificationsController.php";
|
||||
|
||||
$certifications = showCertifications($mysqli);
|
||||
?>
|
||||
|
||||
<?php if (!empty($certifications)): ?>
|
||||
<?php
|
||||
$delay = 1;
|
||||
foreach ($certifications as $cert):
|
||||
$delayClass = "reveal-delay-" . (($delay % 3 === 0) ? 3 : ($delay % 3));
|
||||
?>
|
||||
<div class="cert-card reveal <?php echo $delayClass; ?>">
|
||||
<div class="cert-icon"><?php echo htmlspecialchars($cert['icon']); ?></div>
|
||||
<div class="cert-info">
|
||||
<h3><?php echo htmlspecialchars($cert['title']); ?></h3>
|
||||
<p class="issuer"><?php echo htmlspecialchars($cert['issuer']); ?></p>
|
||||
<span class="date">Issued <?php echo htmlspecialchars($cert['date_issued']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$delay++;
|
||||
endforeach;
|
||||
?>
|
||||
<?php else: ?>
|
||||
<p style="color: var(--text-muted); grid-column: 1/-1; text-align:center; padding: 40px 0;">No certifications found.</p>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
require_once __DIR__ . "/../controllers/ArticlesController.php";
|
||||
|
||||
$articles = showThreeArticles($mysqli);
|
||||
?>
|
||||
|
||||
<?php if (!empty($articles)): ?>
|
||||
<?php
|
||||
$delay = 1;
|
||||
foreach ($articles as $art):
|
||||
$delayClass = "reveal-delay-" . (($delay % 3 === 0) ? 3 : ($delay % 3));
|
||||
?>
|
||||
<article class="blog-card reveal <?php echo $delayClass; ?>">
|
||||
<div class="blog-meta">
|
||||
<span class="blog-date"><?php echo htmlspecialchars($art['date_published']); ?></span>
|
||||
<span class="blog-tag"><?php echo htmlspecialchars($art['tag']); ?></span>
|
||||
</div>
|
||||
<h3 class="blog-title"><?php echo htmlspecialchars($art['title']); ?></h3>
|
||||
<p class="blog-excerpt"><?php echo htmlspecialchars($art['excerpt']); ?></p>
|
||||
<a href="<?php echo htmlspecialchars($art['url']); ?>" class="read-more">Read Article →</a>
|
||||
</article>
|
||||
<?php
|
||||
$delay++;
|
||||
endforeach;
|
||||
?>
|
||||
<?php else: ?>
|
||||
<p style="color: var(--text-muted); grid-column: 1/-1; text-align:center; padding: 40px 0;">No articles found.</p>
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/db.php";
|
||||
require_once __DIR__ . "/../controllers/CertificationsController.php";
|
||||
|
||||
$certifications = showThreeCertifications($mysqli);
|
||||
?>
|
||||
|
||||
<?php if (!empty($certifications)): ?>
|
||||
<?php
|
||||
$delay = 1;
|
||||
foreach ($certifications as $cert):
|
||||
$delayClass = "reveal-delay-" . (($delay % 3 === 0) ? 3 : ($delay % 3));
|
||||
?>
|
||||
<div class="cert-card reveal <?php echo $delayClass; ?>">
|
||||
<div class="cert-icon"><?php echo htmlspecialchars($cert['icon']); ?></div>
|
||||
<div class="cert-info">
|
||||
<h3><?php echo htmlspecialchars($cert['title']); ?></h3>
|
||||
<p class="issuer"><?php echo htmlspecialchars($cert['issuer']); ?></p>
|
||||
<span class="date">Issued <?php echo htmlspecialchars($cert['date_issued']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$delay++;
|
||||
endforeach;
|
||||
?>
|
||||
<?php else: ?>
|
||||
<p style="color: var(--text-muted); grid-column: 1/-1; text-align:center; padding: 40px 0;">No certifications found.</p>
|
||||
<?php endif; ?>
|
||||
Reference in new issue
Block a user