Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -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;
|
||||||
|
}
|
||||||
@ -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
153
build.php
@ -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'; ?>
|
|
||||||
126
builds.php
126
builds.php
@ -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'; ?>
|
|
||||||
114
create_build.php
114
create_build.php
@ -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'; ?>
|
|
||||||
@ -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'; ?>
|
|
||||||
@ -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;
|
|
||||||
108
forums.php
108
forums.php
@ -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'; ?>
|
|
||||||
@ -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));
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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
253
index.php
@ -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'; ?>
|
|
||||||
|
|||||||
146
thread.php
146
thread.php
@ -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'; ?>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user