Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

13 changed files with 585 additions and 1208 deletions

View File

@ -1,175 +1,403 @@
:root { body {
--bg: #0d0f12; background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
--surface: #14181f; background-size: 400% 400%;
--surface-2: #1b2029; animation: gradient 15s ease infinite;
--border: #232a34; color: #212529;
--text: #e6e9ef; font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--muted: #9aa3b2; font-size: 14px;
--accent: #3ea6ff; margin: 0;
--accent-2: #7dd3fc; min-height: 100vh;
--success: #2dd4bf; }
--warning: #fbbf24;
} .main-wrapper {
display: flex;
* { align-items: center;
box-sizing: border-box; justify-content: center;
} min-height: 100vh;
width: 100%;
body.app-body { padding: 20px;
margin: 0; box-sizing: border-box;
min-height: 100vh; position: relative;
background: var(--bg); z-index: 1;
color: var(--text); }
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 15px; @keyframes gradient {
line-height: 1.6; 0% {
} background-position: 0% 50%;
}
a { 50% {
color: var(--accent); background-position: 100% 50%;
text-decoration: none; }
} 100% {
background-position: 0% 50%;
a:hover { }
color: var(--accent-2); }
}
.chat-container {
.navbar { width: 100%;
background: rgba(13, 15, 18, 0.96); max-width: 600px;
border-bottom: 1px solid var(--border); background: rgba(255, 255, 255, 0.85);
} border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
.navbar-brand { display: flex;
font-weight: 600; flex-direction: column;
letter-spacing: 0.3px; height: 85vh;
} box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
.nav-link { -webkit-backdrop-filter: blur(15px);
color: var(--muted); overflow: hidden;
} }
.nav-link.active, .chat-header {
.nav-link:hover { padding: 1.5rem;
color: var(--text); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
} background: rgba(255, 255, 255, 0.5);
font-weight: 700;
.hero { font-size: 1.1rem;
background: var(--surface); display: flex;
border: 1px solid var(--border); justify-content: space-between;
border-radius: 12px; align-items: center;
padding: 2.5rem; }
}
.chat-messages {
.stat-card, flex: 1;
.app-card { overflow-y: auto;
background: var(--surface); padding: 1.5rem;
border: 1px solid var(--border); display: flex;
border-radius: 10px; flex-direction: column;
padding: 1.5rem; gap: 1.25rem;
} }
.stat-card h3 { /* Custom Scrollbar */
font-size: 1.3rem; ::-webkit-scrollbar {
margin: 0 0 0.5rem; width: 6px;
} }
.badge-soft { ::-webkit-scrollbar-track {
background: var(--surface-2); background: transparent;
color: var(--muted); }
border: 1px solid var(--border);
font-weight: 500; ::-webkit-scrollbar-thumb {
} background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
.table-dark { }
--bs-table-bg: var(--surface);
--bs-table-border-color: var(--border); ::-webkit-scrollbar-thumb:hover {
--bs-table-color: var(--text); background: rgba(255, 255, 255, 0.5);
} }
.form-control, .message {
.form-select, max-width: 85%;
.form-check-input { padding: 0.85rem 1.1rem;
background: var(--surface); border-radius: 16px;
border: 1px solid var(--border); line-height: 1.5;
color: var(--text); font-size: 0.95rem;
border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);
} animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.form-control:focus,
.form-select:focus, @keyframes fadeIn {
.form-check-input:focus { from { opacity: 0; transform: translateY(20px) scale(0.95); }
border-color: var(--accent); to { opacity: 1; transform: translateY(0) scale(1); }
box-shadow: 0 0 0 0.2rem rgba(62, 166, 255, 0.15); }
}
.message.visitor {
.form-text { align-self: flex-end;
color: var(--muted); background: linear-gradient(135deg, #212529 0%, #343a40 100%);
} color: #fff;
border-bottom-right-radius: 4px;
.btn-primary { }
background: var(--accent);
border-color: var(--accent); .message.bot {
color: #0b1117; align-self: flex-start;
font-weight: 600; background: #ffffff;
border-radius: 8px; color: #212529;
} border-bottom-left-radius: 4px;
}
.btn-primary:hover {
background: #62b6ff; .chat-input-area {
border-color: #62b6ff; padding: 1.25rem;
color: #0b1117; background: rgba(255, 255, 255, 0.5);
} border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.btn-outline-light {
border-color: var(--border); .chat-input-area form {
color: var(--text); display: flex;
border-radius: 8px; gap: 0.75rem;
} }
.btn-outline-light:hover { .chat-input-area input {
background: var(--surface-2); flex: 1;
color: var(--text); border: 1px solid rgba(0, 0, 0, 0.1);
} border-radius: 12px;
padding: 0.75rem 1rem;
.alert { outline: none;
border-radius: 10px; background: rgba(255, 255, 255, 0.9);
border: 1px solid var(--border); transition: all 0.3s ease;
background: var(--surface-2); }
color: var(--text);
} .chat-input-area input:focus {
border-color: #23a6d5;
.alert-success { box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
border-color: rgba(45, 212, 191, 0.4); }
}
.chat-input-area button {
.alert-warning { background: #212529;
border-color: rgba(251, 191, 36, 0.4); color: #fff;
} border: none;
padding: 0.75rem 1.5rem;
.tag { border-radius: 12px;
background: rgba(62, 166, 255, 0.12); cursor: pointer;
color: var(--accent); font-weight: 600;
border: 1px solid rgba(62, 166, 255, 0.25); transition: all 0.3s ease;
border-radius: 999px; }
padding: 0.2rem 0.6rem;
font-size: 0.75rem; .chat-input-area button:hover {
font-weight: 600; background: #000;
letter-spacing: 0.2px; transform: translateY(-2px);
} box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.footer {
border-top: 1px solid var(--border); /* Background Animations */
background: var(--surface); .bg-animations {
color: var(--muted); position: fixed;
} top: 0;
left: 0;
.muted { width: 100%;
color: var(--muted); height: 100%;
} z-index: 0;
overflow: hidden;
.section-title { pointer-events: none;
font-size: 1.1rem; }
letter-spacing: 0.4px;
text-transform: uppercase; .blob {
color: var(--muted); position: absolute;
} width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.header-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.header-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-links {
display: flex;
gap: 1rem;
}
.admin-card {
background: rgba(255, 255, 255, 0.6);
padding: 2rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
margin-bottom: 2.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.admin-card h3 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 700;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
.btn-save {
background: #0088cc;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
width: 100%;
transition: all 0.3s ease;
}
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
}
.history-table-container {
overflow-x: auto;
background: rgba(255, 255, 255, 0.4);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.history-table {
width: 100%;
}
.history-table-time {
width: 15%;
white-space: nowrap;
font-size: 0.85em;
color: #555;
}
.history-table-user {
width: 35%;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px;
}
.history-table-ai {
width: 50%;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 8px;
}
.no-messages {
text-align: center;
color: #777;
}

View File

@ -1,9 +1,39 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const alerts = document.querySelectorAll('.alert[data-autohide="true"]'); const chatForm = document.getElementById('chat-form');
alerts.forEach((alert) => { const chatInput = document.getElementById('chat-input');
setTimeout(() => { const chatMessages = document.getElementById('chat-messages');
alert.classList.add('fade');
alert.classList.remove('show'); const appendMessage = (text, sender) => {
}, 4000); const msgDiv = document.createElement('div');
}); msgDiv.classList.add('message', sender);
}); msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
}
});
});

153
build.php
View File

@ -1,153 +0,0 @@
<?php
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pageTitle = 'Build Detail';
$active = 'builds';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$notice = '';
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$author = trim($_POST['author'] ?? '');
$body = trim($_POST['body'] ?? '');
if ($author === '') {
$errors[] = 'Name is required for a comment.';
}
if ($body === '' || strlen($body) < 3) {
$errors[] = 'Comment must be at least 3 characters.';
}
if (!$errors && $id) {
$stmt = $pdo->prepare("INSERT INTO build_comments (build_id, author, body) VALUES (:build_id, :author, :body)");
$stmt->execute([
':build_id' => $id,
':author' => $author,
':body' => $body,
]);
header('Location: build.php?id=' . $id . '&commented=1');
exit;
}
}
$stmt = $pdo->prepare("SELECT * FROM builds WHERE id = :id");
$stmt->execute([':id' => $id]);
$build = $stmt->fetch();
if (!$build) {
$pageTitle = 'Build Not Found';
}
$comments = [];
if ($build) {
$stmt = $pdo->prepare("SELECT * FROM build_comments WHERE build_id = :id ORDER BY created_at DESC");
$stmt->execute([':id' => $id]);
$comments = $stmt->fetchAll();
}
include __DIR__ . '/includes/header.php';
?>
<?php if (!$build): ?>
<div class="app-card">
<h1 class="h4">Build not found</h1>
<p class="muted">The build you requested does not exist. Try browsing the build library.</p>
<a class="btn btn-outline-light" href="builds.php">Back to builds</a>
</div>
<?php else: ?>
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<span class="badge badge-soft mb-2"><?= h($build['game']) ?></span>
<?php if (!empty($build['patch'])): ?>
<span class="badge badge-soft mb-2">Patch <?= h($build['patch']) ?></span>
<?php endif; ?>
<h1 class="h3 mb-1"><?= h($build['title']) ?></h1>
<p class="muted mb-0"><?= h($build['class_name']) ?> · Authored by <?= h($build['author']) ?> · <?= h(format_date($build['created_at'])) ?></p>
</div>
<a class="btn btn-outline-light" href="create_build.php">Publish new build</a>
</div>
<?php if (!empty($_GET['created'])): ?>
<div class="alert alert-success alert-dismissible show" role="alert" data-autohide="true">
Build published successfully. Share it with the community.
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (!empty($_GET['commented'])): ?>
<div class="alert alert-success alert-dismissible show" role="alert" data-autohide="true">
Comment added. Thanks for contributing.
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning" role="alert">
<?= h(implode(' ', $errors)) ?>
</div>
<?php endif; ?>
<div class="row g-3 mb-4">
<div class="col-lg-7">
<div class="app-card h-100">
<h2 class="h5">Build summary</h2>
<p class="muted"><?= h($build['summary'] ?: 'No summary provided yet.') ?></p>
<hr class="border-secondary">
<h3 class="h6">Skill priorities</h3>
<p><?= nl2br(h($build['skills'] ?: 'List core skills, rotation, and passive synergies.')) ?></p>
<h3 class="h6 mt-3">Gear & stats</h3>
<p><?= nl2br(h($build['gear'] ?: 'Highlight items, stat thresholds, and legendary priorities.')) ?></p>
</div>
</div>
<div class="col-lg-5">
<div class="app-card h-100">
<h2 class="h5">Patch impact notes</h2>
<p class="muted">Track changes and explain why this build remains optimal.</p>
<ul class="list-unstyled mb-0">
<li class="mb-2"><span class="tag">Meta</span> Recommended for current season.</li>
<li class="mb-2"><span class="tag">Stats</span> Focus on cooldown + crit.</li>
<li><span class="tag">Tips</span> Positioning advice included in skills block.</li>
</ul>
</div>
</div>
</div>
<div class="app-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Community comments</h2>
<span class="muted"><?= count($comments) ?> comments</span>
</div>
<form method="post" class="mb-4">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input class="form-control" type="text" name="author" required>
</div>
<div class="col-md-8">
<label class="form-label">Comment</label>
<input class="form-control" type="text" name="body" maxlength="1000" required>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Add comment</button>
</form>
<?php if (!$comments): ?>
<p class="muted mb-0">No comments yet. Share first impressions.</p>
<?php else: ?>
<div class="d-grid gap-3">
<?php foreach ($comments as $comment): ?>
<div class="app-card">
<div class="d-flex justify-content-between">
<strong><?= h($comment['author']) ?></strong>
<span class="muted"><?= h(format_date($comment['created_at'])) ?></span>
</div>
<p class="mb-0"><?= h($comment['body']) ?></p>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,126 +0,0 @@
<?php
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pageTitle = 'Builds';
$active = 'builds';
$pdo = db();
$filters = [
'search' => trim($_GET['search'] ?? ''),
'game' => trim($_GET['game'] ?? ''),
'class' => trim($_GET['class'] ?? ''),
'patch' => trim($_GET['patch'] ?? ''),
];
$sql = "SELECT id, title, game, class_name, patch, summary, author, created_at FROM builds WHERE 1=1";
$params = [];
if ($filters['search']) {
$sql .= " AND (title LIKE :search OR summary LIKE :search)";
$params[':search'] = '%' . $filters['search'] . '%';
}
if ($filters['game']) {
$sql .= " AND game = :game";
$params[':game'] = $filters['game'];
}
if ($filters['class']) {
$sql .= " AND class_name = :class";
$params[':class'] = $filters['class'];
}
if ($filters['patch']) {
$sql .= " AND patch = :patch";
$params[':patch'] = $filters['patch'];
}
$sql .= " ORDER BY created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$builds = $stmt->fetchAll();
$games = $pdo->query("SELECT DISTINCT game FROM builds ORDER BY game")->fetchAll(PDO::FETCH_COLUMN);
$classes = $pdo->query("SELECT DISTINCT class_name FROM builds ORDER BY class_name")->fetchAll(PDO::FETCH_COLUMN);
$patches = $pdo->query("SELECT DISTINCT patch FROM builds WHERE patch IS NOT NULL AND patch != '' ORDER BY patch")->fetchAll(PDO::FETCH_COLUMN);
include __DIR__ . '/includes/header.php';
?>
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<div>
<h1 class="h3 mb-1">Build Library</h1>
<p class="muted mb-0">Filter by game, class, and patch for fast comparisons.</p>
</div>
<a class="btn btn-primary" href="create_build.php">Publish build</a>
</div>
<form class="app-card mb-4" method="get">
<div class="row g-3 align-items-end">
<div class="col-lg-4">
<label class="form-label">Search builds</label>
<input class="form-control" type="text" name="search" placeholder="Title or summary" value="<?= h($filters['search']) ?>">
</div>
<div class="col-sm-6 col-lg-2">
<label class="form-label">Game</label>
<select class="form-select" name="game">
<option value="">All</option>
<?php foreach ($games as $game): ?>
<option value="<?= h($game) ?>" <?= $filters['game'] === $game ? 'selected' : '' ?>><?= h($game) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-6 col-lg-2">
<label class="form-label">Class</label>
<select class="form-select" name="class">
<option value="">All</option>
<?php foreach ($classes as $class): ?>
<option value="<?= h($class) ?>" <?= $filters['class'] === $class ? 'selected' : '' ?>><?= h($class) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-6 col-lg-2">
<label class="form-label">Patch</label>
<select class="form-select" name="patch">
<option value="">All</option>
<?php foreach ($patches as $patch): ?>
<option value="<?= h($patch) ?>" <?= $filters['patch'] === $patch ? 'selected' : '' ?>><?= h($patch) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-6 col-lg-2 d-grid">
<button class="btn btn-outline-light" type="submit">Apply</button>
</div>
</div>
</form>
<div class="row g-3">
<?php if (!$builds): ?>
<div class="col-12">
<div class="app-card">
<h2 class="h5">No matches yet</h2>
<p class="muted">Try broadening filters or publish a new build to seed the library.</p>
<a class="btn btn-outline-light" href="create_build.php">Publish build</a>
</div>
</div>
<?php else: ?>
<?php foreach ($builds as $build): ?>
<div class="col-md-6 col-lg-4">
<div class="app-card h-100">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge badge-soft"><?= h($build['game']) ?></span>
<?php if (!empty($build['patch'])): ?>
<span class="badge badge-soft">Patch <?= h($build['patch']) ?></span>
<?php endif; ?>
</div>
<h2 class="h5"><?= h($build['title']) ?></h2>
<p class="muted mb-3"><?= h($build['summary'] ?: 'Build summary pending.') ?></p>
<div class="d-flex justify-content-between align-items-center">
<span class="muted"><?= h($build['class_name']) ?> · <?= h($build['author']) ?></span>
<a class="btn btn-sm btn-outline-light" href="build.php?id=<?= h((string)$build['id']) ?>">View</a>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,114 +0,0 @@
<?php
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pageTitle = 'Publish Build';
$active = 'create-build';
$errors = [];
$values = [
'title' => '',
'game' => '',
'class_name' => '',
'patch' => '',
'summary' => '',
'skills' => '',
'gear' => '',
'author' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($values as $key => $value) {
$values[$key] = trim($_POST[$key] ?? '');
}
if ($values['title'] === '') {
$errors[] = 'Title is required.';
}
if ($values['game'] === '') {
$errors[] = 'Game is required.';
}
if ($values['class_name'] === '') {
$errors[] = 'Class is required.';
}
if ($values['author'] === '') {
$errors[] = 'Author name is required.';
}
if (!$errors) {
$stmt = db()->prepare(
"INSERT INTO builds (title, game, class_name, patch, summary, skills, gear, author)
VALUES (:title, :game, :class_name, :patch, :summary, :skills, :gear, :author)"
);
$stmt->execute([
':title' => $values['title'],
':game' => $values['game'],
':class_name' => $values['class_name'],
':patch' => $values['patch'] ?: null,
':summary' => $values['summary'] ?: null,
':skills' => $values['skills'] ?: null,
':gear' => $values['gear'] ?: null,
':author' => $values['author'],
]);
$id = (int)db()->lastInsertId();
header('Location: build.php?id=' . $id . '&created=1');
exit;
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<div>
<h1 class="h3 mb-1">Publish a Build</h1>
<p class="muted mb-0">Share your strategy with a concise overview, skills, and gear notes.</p>
</div>
<a class="btn btn-outline-light" href="builds.php">Back to builds</a>
</div>
<?php if ($errors): ?>
<div class="alert alert-warning" role="alert">
<?= h(implode(' ', $errors)) ?>
</div>
<?php endif; ?>
<form method="post" class="app-card">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Build title</label>
<input class="form-control" type="text" name="title" maxlength="160" required value="<?= h($values['title']) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Patch</label>
<input class="form-control" type="text" name="patch" maxlength="40" placeholder="e.g. 1.2.3" value="<?= h($values['patch']) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Game</label>
<input class="form-control" type="text" name="game" required value="<?= h($values['game']) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Class</label>
<input class="form-control" type="text" name="class_name" required value="<?= h($values['class_name']) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Author</label>
<input class="form-control" type="text" name="author" required value="<?= h($values['author']) ?>">
</div>
<div class="col-12">
<label class="form-label">Summary</label>
<textarea class="form-control" name="summary" rows="2" maxlength="240"><?= h($values['summary']) ?></textarea>
<div class="form-text">Keep it short: 1-2 sentences.</div>
</div>
<div class="col-md-6">
<label class="form-label">Skill priorities</label>
<textarea class="form-control" name="skills" rows="5"><?= h($values['skills']) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label">Gear & stats</label>
<textarea class="form-control" name="gear" rows="5"><?= h($values['gear']) ?></textarea>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Publish build</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,95 +0,0 @@
<?php
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pageTitle = 'Start Thread';
$active = 'create-thread';
$errors = [];
$values = [
'title' => '',
'game' => '',
'tag' => '',
'body' => '',
'author' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($values as $key => $value) {
$values[$key] = trim($_POST[$key] ?? '');
}
if ($values['title'] === '') {
$errors[] = 'Title is required.';
}
if ($values['game'] === '') {
$errors[] = 'Game is required.';
}
if ($values['body'] === '' || strlen($values['body']) < 10) {
$errors[] = 'Opening post must be at least 10 characters.';
}
if ($values['author'] === '') {
$errors[] = 'Author name is required.';
}
if (!$errors) {
$stmt = db()->prepare(
"INSERT INTO forum_threads (title, game, tag, body, author)
VALUES (:title, :game, :tag, :body, :author)"
);
$stmt->execute([
':title' => $values['title'],
':game' => $values['game'],
':tag' => $values['tag'] ?: null,
':body' => $values['body'],
':author' => $values['author'],
]);
$id = (int)db()->lastInsertId();
header('Location: thread.php?id=' . $id . '&created=1');
exit;
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<div>
<h1 class="h3 mb-1">Start a Thread</h1>
<p class="muted mb-0">Lead the conversation on builds, patches, or loot.</p>
</div>
<a class="btn btn-outline-light" href="forums.php">Back to forums</a>
</div>
<?php if ($errors): ?>
<div class="alert alert-warning" role="alert">
<?= h(implode(' ', $errors)) ?>
</div>
<?php endif; ?>
<form method="post" class="app-card">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Thread title</label>
<input class="form-control" type="text" name="title" maxlength="160" required value="<?= h($values['title']) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Tag</label>
<input class="form-control" type="text" name="tag" maxlength="40" placeholder="Patch, Meta, Guide" value="<?= h($values['tag']) ?>">
</div>
<div class="col-md-6">
<label class="form-label">Game</label>
<input class="form-control" type="text" name="game" required value="<?= h($values['game']) ?>">
</div>
<div class="col-md-6">
<label class="form-label">Author</label>
<input class="form-control" type="text" name="author" required value="<?= h($values['author']) ?>">
</div>
<div class="col-12">
<label class="form-label">Opening post</label>
<textarea class="form-control" name="body" rows="5" required><?= h($values['body']) ?></textarea>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Publish thread</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,43 +0,0 @@
-- Initial builds + forums tables
CREATE TABLE IF NOT EXISTS builds (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(160) NOT NULL,
game VARCHAR(80) NOT NULL,
class_name VARCHAR(80) NOT NULL,
patch VARCHAR(40) DEFAULT NULL,
summary VARCHAR(240) DEFAULT NULL,
skills TEXT,
gear TEXT,
author VARCHAR(80) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS build_comments (
id INT AUTO_INCREMENT PRIMARY KEY,
build_id INT NOT NULL,
author VARCHAR(80) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (build_id),
CONSTRAINT fk_build_comment FOREIGN KEY (build_id) REFERENCES builds(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS forum_threads (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(160) NOT NULL,
game VARCHAR(80) NOT NULL,
tag VARCHAR(40) DEFAULT NULL,
body TEXT NOT NULL,
author VARCHAR(80) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS forum_posts (
id INT AUTO_INCREMENT PRIMARY KEY,
thread_id INT NOT NULL,
author VARCHAR(80) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (thread_id),
CONSTRAINT fk_thread_post FOREIGN KEY (thread_id) REFERENCES forum_threads(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,108 +0,0 @@
<?php
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pageTitle = 'Forums';
$active = 'forums';
$pdo = db();
$filters = [
'search' => trim($_GET['search'] ?? ''),
'game' => trim($_GET['game'] ?? ''),
'tag' => trim($_GET['tag'] ?? ''),
];
$sql = "SELECT id, title, game, tag, author, created_at FROM forum_threads WHERE 1=1";
$params = [];
if ($filters['search']) {
$sql .= " AND (title LIKE :search OR body LIKE :search)";
$params[':search'] = '%' . $filters['search'] . '%';
}
if ($filters['game']) {
$sql .= " AND game = :game";
$params[':game'] = $filters['game'];
}
if ($filters['tag']) {
$sql .= " AND tag = :tag";
$params[':tag'] = $filters['tag'];
}
$sql .= " ORDER BY created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$threads = $stmt->fetchAll();
$games = $pdo->query("SELECT DISTINCT game FROM forum_threads ORDER BY game")->fetchAll(PDO::FETCH_COLUMN);
$tags = $pdo->query("SELECT DISTINCT tag FROM forum_threads WHERE tag IS NOT NULL AND tag != '' ORDER BY tag")->fetchAll(PDO::FETCH_COLUMN);
include __DIR__ . '/includes/header.php';
?>
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<div>
<h1 class="h3 mb-1">Community Forums</h1>
<p class="muted mb-0">Patch reaction, class strategy, and loot discussions.</p>
</div>
<a class="btn btn-primary" href="create_thread.php">Start thread</a>
</div>
<form class="app-card mb-4" method="get">
<div class="row g-3 align-items-end">
<div class="col-lg-6">
<label class="form-label">Search threads</label>
<input class="form-control" type="text" name="search" placeholder="Title or body" value="<?= h($filters['search']) ?>">
</div>
<div class="col-sm-6 col-lg-3">
<label class="form-label">Game</label>
<select class="form-select" name="game">
<option value="">All</option>
<?php foreach ($games as $game): ?>
<option value="<?= h($game) ?>" <?= $filters['game'] === $game ? 'selected' : '' ?>><?= h($game) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-6 col-lg-3">
<label class="form-label">Tag</label>
<select class="form-select" name="tag">
<option value="">All</option>
<?php foreach ($tags as $tag): ?>
<option value="<?= h($tag) ?>" <?= $filters['tag'] === $tag ? 'selected' : '' ?>><?= h($tag) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="mt-3 d-flex justify-content-end">
<button class="btn btn-outline-light" type="submit">Apply filters</button>
</div>
</form>
<div class="row g-3">
<?php if (!$threads): ?>
<div class="col-12">
<div class="app-card">
<h2 class="h5">No threads yet</h2>
<p class="muted">Start a discussion about the latest patch or class strategy.</p>
<a class="btn btn-outline-light" href="create_thread.php">Start thread</a>
</div>
</div>
<?php else: ?>
<?php foreach ($threads as $thread): ?>
<div class="col-md-6">
<div class="app-card h-100">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge badge-soft"><?= h($thread['game']) ?></span>
<?php if (!empty($thread['tag'])): ?>
<span class="badge badge-soft"><?= h($thread['tag']) ?></span>
<?php endif; ?>
</div>
<h2 class="h5"><?= h($thread['title']) ?></h2>
<p class="muted mb-3">By <?= h($thread['author']) ?> · <?= h(format_date($thread['created_at'])) ?></p>
<a class="btn btn-sm btn-outline-light" href="thread.php?id=<?= h((string)$thread['id']) ?>">Open thread</a>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,67 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
function h(?string $value): string {
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
}
function ensure_tables(): void {
$pdo = db();
$pdo->exec(
"CREATE TABLE IF NOT EXISTS builds (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(160) NOT NULL,
game VARCHAR(80) NOT NULL,
class_name VARCHAR(80) NOT NULL,
patch VARCHAR(40) DEFAULT NULL,
summary VARCHAR(240) DEFAULT NULL,
skills TEXT,
gear TEXT,
author VARCHAR(80) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS build_comments (
id INT AUTO_INCREMENT PRIMARY KEY,
build_id INT NOT NULL,
author VARCHAR(80) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (build_id),
CONSTRAINT fk_build_comment FOREIGN KEY (build_id) REFERENCES builds(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS forum_threads (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(160) NOT NULL,
game VARCHAR(80) NOT NULL,
tag VARCHAR(40) DEFAULT NULL,
body TEXT NOT NULL,
author VARCHAR(80) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS forum_posts (
id INT AUTO_INCREMENT PRIMARY KEY,
thread_id INT NOT NULL,
author VARCHAR(80) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (thread_id),
CONSTRAINT fk_thread_post FOREIGN KEY (thread_id) REFERENCES forum_threads(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
}
function format_date(string $value): string {
return date('M j, Y H:i', strtotime($value));
}

View File

@ -1,14 +0,0 @@
</main>
<footer class="footer py-4 mt-4">
<div class="container d-flex flex-column flex-md-row justify-content-between gap-3">
<div>
<div class="fw-semibold">Community pulse stays here.</div>
<div class="muted">Builds, threads, and strategy snapshots update in real time.</div>
</div>
<div class="muted">UTC <?= h(date('M j, Y H:i')) ?></div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
</body>
</html>

View File

@ -1,48 +0,0 @@
<?php
$projectName = $_SERVER['PROJECT_NAME'] ?? 'ArcaneForge';
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'A dark, high-contrast hub for player builds, forums, and community intel.';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$fullTitle = $pageTitle ? ($pageTitle . ' · ' . $projectName) : $projectName;
$active = $active ?? '';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($fullTitle) ?></title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= h($projectDescription) ?>" />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
</head>
<body class="app-body">
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container py-2">
<a class="navbar-brand d-flex align-items-center gap-2" href="index.php">
<span class="badge rounded-pill text-bg-dark border border-secondary">AF</span>
<?= h($projectName) ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<div class="navbar-nav ms-auto gap-lg-2">
<a class="nav-link <?= $active === 'home' ? 'active' : '' ?>" href="index.php">Home</a>
<a class="nav-link <?= $active === 'builds' ? 'active' : '' ?>" href="builds.php">Builds</a>
<a class="nav-link <?= $active === 'forums' ? 'active' : '' ?>" href="forums.php">Forums</a>
<a class="nav-link <?= $active === 'create-build' ? 'active' : '' ?>" href="create_build.php">Publish Build</a>
<a class="nav-link <?= $active === 'create-thread' ? 'active' : '' ?>" href="create_thread.php">Start Thread</a>
</div>
</div>
</div>
</nav>
<main class="container py-4">

253
index.php
View File

@ -1,117 +1,150 @@
<?php <?php
require_once __DIR__ . '/includes/app.php'; declare(strict_types=1);
ensure_tables(); @ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$pageTitle = 'Home'; $phpVersion = PHP_VERSION;
$active = 'home'; $now = date('Y-m-d H:i:s');
$pdo = db();
$builds = $pdo->query("SELECT id, title, game, class_name, patch, summary, author, created_at FROM builds ORDER BY created_at DESC LIMIT 3")->fetchAll();
$threads = $pdo->query("SELECT id, title, game, tag, author, created_at FROM forum_threads ORDER BY created_at DESC LIMIT 3")->fetchAll();
include __DIR__ . '/includes/header.php';
?> ?>
<!doctype html>
<?php if (!empty($_GET['registered'])): ?> <html lang="en">
<div class="alert alert-success" role="alert">Account created. You are now logged in.</div> <head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?> <?php endif; ?>
<?php if (!empty($_GET['logged_out'])): ?> <?php if ($projectImageUrl): ?>
<div class="alert alert-success" role="alert">You have been logged out.</div> <!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?> <?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<section class="hero mb-4"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<div class="row align-items-center g-4"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<div class="col-lg-7"> <style>
<span class="tag mb-3 d-inline-flex">Patch-ready community hub</span> :root {
<h1 class="display-6 fw-semibold">Track the best builds and keep the meta in focus.</h1> --bg-color-start: #6a11cb;
<p class="muted">Publish build guides, track patch notes, and keep tactical discussions organized. This MVP delivers searchable builds, build detail pages with comments, and live forum threads.</p> --bg-color-end: #2575fc;
<div class="d-flex flex-wrap gap-2 mt-4"> --text-color: #ffffff;
<a class="btn btn-primary" href="builds.php">Browse builds</a> --card-bg-color: rgba(255, 255, 255, 0.01);
<a class="btn btn-outline-light" href="create_build.php">Publish a build</a> --card-border-color: rgba(255, 255, 255, 0.1);
<a class="btn btn-outline-light" href="forums.php">Visit forums</a> }
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
<div class="col-lg-5"> </main>
<div class="app-card"> <footer>
<h2 class="h5 mb-3">Live pulse</h2> Page updated: <?= htmlspecialchars($now) ?> (UTC)
<ul class="list-unstyled mb-0"> </footer>
<li class="d-flex justify-content-between mb-2"><span class="muted">Active builds</span><span class="fw-semibold"><?= h((string)$pdo->query("SELECT COUNT(*) FROM builds")->fetchColumn()) ?></span></li> </body>
<li class="d-flex justify-content-between mb-2"><span class="muted">Forum threads</span><span class="fw-semibold"><?= h((string)$pdo->query("SELECT COUNT(*) FROM forum_threads")->fetchColumn()) ?></span></li> </html>
<li class="d-flex justify-content-between"><span class="muted">Latest update</span><span class="fw-semibold"><?= h(date('M j, Y')) ?></span></li>
</ul>
</div>
</div>
</div>
</section>
<section class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="section-title">Featured builds</h2>
<a class="muted" href="builds.php">View all </a>
</div>
<div class="row g-3">
<?php if (!$builds): ?>
<div class="col-12">
<div class="app-card">
<h3 class="h5">No builds yet</h3>
<p class="muted">Start by publishing the first build so the community can discuss skills, gear, and patch impact.</p>
<a class="btn btn-primary" href="create_build.php">Publish first build</a>
</div>
</div>
<?php else: ?>
<?php foreach ($builds as $build): ?>
<div class="col-md-4">
<div class="app-card h-100">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge badge-soft"><?= h($build['game']) ?></span>
<?php if (!empty($build['patch'])): ?>
<span class="badge badge-soft">Patch <?= h($build['patch']) ?></span>
<?php endif; ?>
</div>
<h3 class="h5"><?= h($build['title']) ?></h3>
<p class="muted mb-3"><?= h($build['summary'] ?: 'Build summary coming soon.') ?></p>
<div class="d-flex justify-content-between align-items-center">
<span class="muted"><?= h($build['class_name']) ?> · <?= h($build['author']) ?></span>
<a class="btn btn-sm btn-outline-light" href="build.php?id=<?= h((string)$build['id']) ?>">Open</a>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
<section>
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="section-title">Latest forum threads</h2>
<a class="muted" href="forums.php">View all </a>
</div>
<div class="row g-3">
<?php if (!$threads): ?>
<div class="col-12">
<div class="app-card">
<h3 class="h5">No threads yet</h3>
<p class="muted">Kick off the first discussion with patch feedback or class strategy.</p>
<a class="btn btn-outline-light" href="create_thread.php">Start thread</a>
</div>
</div>
<?php else: ?>
<?php foreach ($threads as $thread): ?>
<div class="col-md-4">
<div class="app-card h-100">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge badge-soft"><?= h($thread['game']) ?></span>
<?php if (!empty($thread['tag'])): ?>
<span class="badge badge-soft"><?= h($thread['tag']) ?></span>
<?php endif; ?>
</div>
<h3 class="h5"><?= h($thread['title']) ?></h3>
<p class="muted mb-3">Started by <?= h($thread['author']) ?> · <?= h(format_date($thread['created_at'])) ?></p>
<a class="btn btn-sm btn-outline-light" href="thread.php?id=<?= h((string)$thread['id']) ?>">Open thread</a>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,146 +0,0 @@
<?php
require_once __DIR__ . '/includes/app.php';
ensure_tables();
$pageTitle = 'Thread Detail';
$active = 'forums';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$author = trim($_POST['author'] ?? '');
$body = trim($_POST['body'] ?? '');
if ($author === '') {
$errors[] = 'Name is required for a reply.';
}
if ($body === '' || strlen($body) < 3) {
$errors[] = 'Reply must be at least 3 characters.';
}
if (!$errors && $id) {
$stmt = $pdo->prepare("INSERT INTO forum_posts (thread_id, author, body) VALUES (:thread_id, :author, :body)");
$stmt->execute([
':thread_id' => $id,
':author' => $author,
':body' => $body,
]);
header('Location: thread.php?id=' . $id . '&posted=1');
exit;
}
}
$stmt = $pdo->prepare("SELECT * FROM forum_threads WHERE id = :id");
$stmt->execute([':id' => $id]);
$thread = $stmt->fetch();
if (!$thread) {
$pageTitle = 'Thread Not Found';
}
$posts = [];
if ($thread) {
$stmt = $pdo->prepare("SELECT * FROM forum_posts WHERE thread_id = :id ORDER BY created_at DESC");
$stmt->execute([':id' => $id]);
$posts = $stmt->fetchAll();
}
include __DIR__ . '/includes/header.php';
?>
<?php if (!$thread): ?>
<div class="app-card">
<h1 class="h4">Thread not found</h1>
<p class="muted">This thread may have been removed or never existed.</p>
<a class="btn btn-outline-light" href="forums.php">Back to forums</a>
</div>
<?php else: ?>
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<span class="badge badge-soft mb-2"><?= h($thread['game']) ?></span>
<?php if (!empty($thread['tag'])): ?>
<span class="badge badge-soft mb-2"><?= h($thread['tag']) ?></span>
<?php endif; ?>
<h1 class="h3 mb-1"><?= h($thread['title']) ?></h1>
<p class="muted mb-0">Started by <?= h($thread['author']) ?> · <?= h(format_date($thread['created_at'])) ?></p>
</div>
<a class="btn btn-outline-light" href="create_thread.php">Start new thread</a>
</div>
<?php if (!empty($_GET['created'])): ?>
<div class="alert alert-success alert-dismissible show" role="alert" data-autohide="true">
Thread published. Invite others to join the discussion.
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (!empty($_GET['posted'])): ?>
<div class="alert alert-success alert-dismissible show" role="alert" data-autohide="true">
Reply posted. Keep the conversation moving.
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning" role="alert">
<?= h(implode(' ', $errors)) ?>
</div>
<?php endif; ?>
<div class="row g-3 mb-4">
<div class="col-lg-8">
<div class="app-card">
<h2 class="h5">Opening post</h2>
<p><?= nl2br(h($thread['body'])) ?></p>
</div>
</div>
<div class="col-lg-4">
<div class="app-card">
<h2 class="h6">Thread focus</h2>
<ul class="list-unstyled mb-0 muted">
<li class="mb-2">Game: <?= h($thread['game']) ?></li>
<li class="mb-2">Tag: <?= h($thread['tag'] ?: 'General') ?></li>
<li>Replies: <?= count($posts) ?></li>
</ul>
</div>
</div>
</div>
<div class="app-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Replies</h2>
<span class="muted"><?= count($posts) ?> replies</span>
</div>
<form method="post" class="mb-4">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input class="form-control" type="text" name="author" required>
</div>
<div class="col-md-8">
<label class="form-label">Reply</label>
<input class="form-control" type="text" name="body" maxlength="1000" required>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Post reply</button>
</form>
<?php if (!$posts): ?>
<p class="muted mb-0">No replies yet. Be the first to respond.</p>
<?php else: ?>
<div class="d-grid gap-3">
<?php foreach ($posts as $post): ?>
<div class="app-card">
<div class="d-flex justify-content-between">
<strong><?= h($post['author']) ?></strong>
<span class="muted"><?= h(format_date($post['created_at'])) ?></span>
</div>
<p class="mb-0"><?= h($post['body']) ?></p>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php include __DIR__ . '/includes/footer.php'; ?>