Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
b8a5b3c462 1 2026-02-20 11:14:19 +00:00
8 changed files with 1143 additions and 390 deletions

43
admin/api_generate.php Normal file
View File

@ -0,0 +1,43 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../ai/LocalAIApi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Invalid method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$title = $input['title'] ?? '';
if (empty(trim($title))) {
echo json_encode(['success' => false, 'error' => 'Please enter a Title first to generate content.']);
exit;
}
$prompt = "Write a comprehensive, engaging, and well-structured blog post article based on the following title: \"$title\".
Format the text strictly using HTML tags suitable for a WYSIWYG editor (like <h2>, <h3>, <p>, <ul>, <li>, <strong>).
Do not use markdown formatting. Output only raw HTML code for the article body without any markdown blocks or explanations.";
$response = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'You are an expert copywriter and professional blog author. You write high-quality, engaging content formatted as HTML.'],
['role' => 'user', 'content' => $prompt]
]
], ['poll_interval' => 3, 'poll_timeout' => 180]);
if (!empty($response['success'])) {
$text = LocalAIApi::extractText($response);
if ($text === '') {
$decoded = LocalAIApi::decodeJsonFromResponse($response);
$text = $decoded ? json_encode($decoded, JSON_UNESCAPED_UNICODE) : (string)($response['data'] ?? '');
}
// Clean up markdown block if the model returned it anyway
$text = preg_replace('/^```(?:html)?\s*|\s*```$/i', '', trim($text));
echo json_encode(['success' => true, 'content' => trim($text)]);
} else {
echo json_encode(['success' => false, 'error' => $response['error'] ?? 'AI generation failed']);
}

10
admin/delete.php Normal file
View File

@ -0,0 +1,10 @@
<?php
require_once '../db/config.php';
$id = $_GET['id'] ?? null;
if ($id) {
$db = db();
$stmt = $db->prepare("DELETE FROM posts WHERE id = ?");
$stmt->execute([$id]);
}
header("Location: index.php");
exit;

232
admin/edit.php Normal file
View File

@ -0,0 +1,232 @@
<?php
require_once '../db/config.php';
$id = $_GET['id'] ?? null;
$db = db();
$post = null;
if ($id) {
$stmt = $db->prepare("SELECT * FROM posts WHERE id = ?");
$stmt->execute([$id]);
$post = $stmt->fetch();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = $_POST['title'];
$slug = $_POST['slug'];
$summary = $_POST['summary'];
$content = $_POST['content'];
$image_url = $_POST['image_url'];
if ($id) {
$stmt = $db->prepare("UPDATE posts SET title = ?, slug = ?, summary = ?, content = ?, image_url = ? WHERE id = ?");
$stmt->execute([$title, $slug, $summary, $content, $image_url, $id]);
} else {
$stmt = $db->prepare("INSERT INTO posts (title, slug, summary, content, image_url) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $slug, $summary, $content, $image_url]);
}
header("Location: index.php");
exit;
}
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Our Blog';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= $id ? 'Edit Post' : 'New Post' ?> | Admin Panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<!-- Quill Editor CSS -->
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
</head>
<body>
<div class="bg-shapes">
<canvas id="bg-canvas"></canvas>
<div class="shape shape-1" data-speed="2"></div>
<div class="shape shape-2" data-speed="-1.5" style="opacity: 0.15; top: -10%; right: -10%; background: #3b82f6;"></div>
<div class="shape shape-mouse"></div>
</div>
<header>
<div class="container nav">
<a href="../index.php" class="logo">Admin Panel</a>
<div class="nav-links">
<a href="index.php">Back to Dashboard</a>
</div>
</div>
</header>
<main class="container">
<div class="admin-header">
<h1><?= $id ? 'Edit Post' : 'Create New Post' ?></h1>
</div>
<form method="POST" class="form-container" id="post-form">
<div class="form-group">
<label for="title">Title</label>
<input type="text" name="title" id="title" class="form-control" value="<?= htmlspecialchars($post['title'] ?? '') ?>" placeholder="e.g. 10 Tips for Modern Web Design" required>
</div>
<div class="form-group">
<label for="slug">URL Slug</label>
<input type="text" name="slug" id="slug" class="form-control" value="<?= htmlspecialchars($post['slug'] ?? '') ?>" placeholder="e.g. 10-tips-for-modern-web-design" required>
<small style="color: var(--secondary); margin-top: 0.25rem; display: block; font-size: 0.85rem;">This will be used in the URL: /post.php?slug=<b>your-slug</b></small>
</div>
<div class="form-group">
<label for="summary">Summary</label>
<textarea name="summary" id="summary" class="form-control" style="min-height: 100px;" placeholder="A short description of your article." required><?= htmlspecialchars($post['summary'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="image_url">Cover Image URL</label>
<input type="url" name="image_url" id="image_url" class="form-control" value="<?= htmlspecialchars($post['image_url'] ?? '') ?>" placeholder="https://example.com/image.jpg">
</div>
<div class="form-group">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<label for="content" style="margin-bottom: 0;">Content</label>
<button type="button" id="btn-generate-ai" class="btn" style="background: linear-gradient(135deg, #a855f7, #ec4899); color: white; border: none; padding: 0.4rem 0.8rem; font-size: 0.85rem; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 0.4rem; box-shadow: 0 4px 12px rgba(236, 72, 153, 0.3); transition: transform 0.2s, box-shadow 0.2s;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
<span>Generate with AI</span>
</button>
</div>
<!-- Editor Container -->
<div id="editor-container"></div>
<!-- Hidden textarea to store the HTML for form submission -->
<textarea name="content" id="hidden-content" style="display: none;"></textarea>
<!-- Hidden textarea to safely load initial content -->
<textarea id="initial-content" style="display: none;"><?= htmlspecialchars($post['content'] ?? '') ?></textarea>
</div>
<div class="actions" style="margin-top: 2.5rem; padding-top: 2rem; border-top: 1px solid var(--border);">
<button type="submit" class="btn btn-primary" style="padding-left: 2rem; padding-right: 2rem;"><?= $id ? 'Update Post' : 'Publish Post' ?></button>
<a href="index.php" class="btn btn-outline" style="padding-left: 1.5rem; padding-right: 1.5rem;">Cancel</a>
</div>
</form>
</main>
<footer style="background: transparent; border-top: none; margin-top: 0;">
<div class="container">
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($projectName) ?> Admin.</p>
</div>
</footer>
<!-- Background scripts and Quill logic -->
<script src="../assets/js/main.js?v=<?= time() ?>"></script>
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
<script>
const titleInput = document.getElementById('title');
const slugInput = document.getElementById('slug');
const btnGenerateAI = document.getElementById('btn-generate-ai');
titleInput.addEventListener('input', () => {
if (!<?= $id ? 'true' : 'false' ?> || slugInput.value === '') {
slugInput.value = titleInput.value
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '');
}
});
// Initialize Quill Editor
const quill = new Quill('#editor-container', {
theme: 'snow',
placeholder: 'Write your amazing article here... You can format text, add lists, and embed images!',
modules: {
toolbar: [
[{ 'header': [2, 3, 4, false] }],
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image', 'video'],
['clean']
]
}
});
// Load initial content if editing
const initialContent = document.getElementById('initial-content').value;
if (initialContent) {
quill.clipboard.dangerouslyPasteHTML(initialContent);
}
// Sync Quill content to hidden textarea on submit
const form = document.getElementById('post-form');
form.addEventListener('submit', function(e) {
const hiddenContent = document.getElementById('hidden-content');
hiddenContent.value = quill.root.innerHTML;
// Basic validation to prevent empty submissions
if (quill.getText().trim().length === 0 && !quill.root.innerHTML.includes('<img') && !quill.root.innerHTML.includes('<video')) {
e.preventDefault();
alert('Please write some content before publishing.');
}
});
// AI Generation Logic
btnGenerateAI.addEventListener('click', async () => {
const title = titleInput.value.trim();
if (!title) {
alert('Пожалуйста, введите заголовок (Title) статьи перед генерацией текста.');
titleInput.focus();
return;
}
const originalBtnHTML = btnGenerateAI.innerHTML;
btnGenerateAI.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"></path></svg> <span>Generating...</span>';
btnGenerateAI.disabled = true;
btnGenerateAI.style.opacity = '0.7';
try {
const response = await fetch('api_generate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: title })
});
const data = await response.json();
if (data.success && data.content) {
// Insert into Quill
// If editor is empty, replace. Otherwise append? Let's just set it or append.
const currentText = quill.getText().trim();
if (currentText.length > 0) {
if (confirm('Редактор уже содержит текст. Заменить его сгенерированным (ОК) или добавить в конец (Отмена)?')) {
quill.clipboard.dangerouslyPasteHTML(data.content);
} else {
const html = quill.root.innerHTML + '<br>' + data.content;
quill.clipboard.dangerouslyPasteHTML(html);
}
} else {
quill.clipboard.dangerouslyPasteHTML(data.content);
}
} else {
alert('Ошибка генерации: ' + (data.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('AI Request failed:', error);
alert('Произошла ошибка при обращении к серверу ИИ. Попробуйте еще раз.');
} finally {
btnGenerateAI.innerHTML = originalBtnHTML;
btnGenerateAI.disabled = false;
btnGenerateAI.style.opacity = '1';
}
});
</script>
<style>
@keyframes spin { 100% { transform: rotate(360deg); } }
.spin { animation: spin 1s linear infinite; }
</style>
</body>
</html>

87
admin/index.php Normal file
View File

@ -0,0 +1,87 @@
<?php
require_once '../db/config.php';
$db = db();
$stmt = $db->query("SELECT * FROM posts ORDER BY created_at DESC");
$posts = $stmt->fetchAll();
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Our Blog';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin Dashboard | <?= htmlspecialchars($projectName) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
</head>
<body>
<div class="bg-shapes">
<canvas id="bg-canvas"></canvas>
<div class="shape shape-1" data-speed="2" style="opacity: 0.15; top: -20%; background: #06b6d4;"></div>
<div class="shape shape-2" data-speed="-1.5"></div>
<div class="shape shape-mouse"></div>
</div>
<header>
<div class="container nav">
<a href="../index.php" class="logo">Admin Panel</a>
<div class="nav-links">
<a href="../index.php">View Site</a>
</div>
</div>
</header>
<main class="container">
<div class="admin-header">
<h1>Manage Posts</h1>
<a href="edit.php" class="btn btn-primary">Create New Post</a>
</div>
<div style="background: var(--surface-solid); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); border: 1px solid var(--border); overflow: hidden;">
<table class="admin-table" style="margin-top: 0; border: none; box-shadow: none; border-radius: 0;">
<thead>
<tr>
<th>Article details</th>
<th>Date</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($posts as $post): ?>
<tr>
<td>
<strong style="font-size: 1.1rem; color: var(--primary); font-family: 'Outfit', sans-serif;"><?= htmlspecialchars($post['title']) ?></strong>
<br>
<small style="color: var(--secondary); font-family: monospace; background: #f1f5f9; padding: 2px 6px; border-radius: 4px;"><?= htmlspecialchars($post['slug']) ?></small>
</td>
<td style="color: var(--secondary);"><?= date('M j, Y', strtotime($post['created_at'])) ?></td>
<td class="actions" style="justify-content: flex-end;">
<a href="edit.php?id=<?= $post['id'] ?>" class="btn btn-outline" style="padding: 0.4rem 1rem;">Edit</a>
<a href="delete.php?id=<?= $post['id'] ?>" class="btn btn-outline" style="padding: 0.4rem 1rem; color: #ef4444; border-color: rgba(239,68,68,0.3);" onclick="return confirm('Are you sure you want to delete this post?')">Delete</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($posts)): ?>
<tr>
<td colspan="3" style="text-align: center; padding: 3rem;">
<div style="color: var(--secondary); margin-bottom: 1rem;">Your blog is empty.</div>
<a href="edit.php" class="btn btn-primary">Write your first post</a>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</main>
<footer style="background: transparent; border-top: none; margin-top: 2rem;">
<div class="container">
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($projectName) ?> Admin.</p>
</div>
</footer>
<script src="../assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>

View File

@ -1,302 +1,524 @@
:root {
--primary: #0f172a;
--secondary: #475569;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--accent-gradient: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
--bg: #f8fafc;
--surface: rgba(255, 255, 255, 0.85);
--surface-solid: #ffffff;
--border: rgba(15, 23, 42, 0.08);
--text-main: #0f172a;
--text-muted: #64748b;
--radius-lg: 24px;
--radius-md: 16px;
--radius-sm: 8px;
--shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01);
--shadow-hover: 0 25px 30px -5px rgba(0, 0, 0, 0.08), 0 10px 15px -6px rgba(0, 0, 0, 0.04);
}
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text-main);
line-height: 1.6;
margin: 0;
overflow-x: hidden;
}
.main-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
h1, h2, h3, h4, h5, h6, .logo {
font-family: 'Outfit', 'Inter', system-ui, -apple-system, sans-serif;
color: var(--primary);
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
/* Decorative Background Elements */
.bg-shapes {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
z-index: -1;
pointer-events: none;
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
#bg-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.6;
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
.shape {
position: absolute;
border-radius: 50%;
filter: blur(90px);
opacity: 0.45;
transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
will-change: transform;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
.shape-1 {
top: -10%;
left: -10%;
width: 500px;
height: 500px;
background: var(--accent);
animation: float 15s ease-in-out infinite;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
.shape-2 {
top: 40%;
right: -5%;
width: 450px;
height: 450px;
background: #06b6d4;
animation: float 18s ease-in-out infinite reverse;
}
::-webkit-scrollbar-track {
background: transparent;
.shape-mouse {
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.6) 0%, rgba(37, 99, 235, 0.1) 60%, transparent 100%);
filter: blur(60px);
top: -150px;
left: -150px;
opacity: 0;
transition: opacity 0.5s ease;
animation: pulse 4s ease-in-out infinite alternate;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
@keyframes float {
0% { transform: translateY(0) scale(1) translateX(0); }
33% { transform: translateY(40px) scale(1.05) translateX(20px); }
66% { transform: translateY(-20px) scale(0.95) translateX(-20px); }
100% { transform: translateY(0) scale(1) translateX(0); }
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
@keyframes pulse {
0% { opacity: 0.3; transform: scale(0.9); }
100% { opacity: 0.6; transform: scale(1.1); }
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
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);
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 1.5rem;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
header {
background: var(--surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 1.2rem 0;
position: sticky;
top: 0;
z-index: 50;
transition: all 0.3s ease;
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
.nav-links {
display: flex;
gap: 2rem;
align-items: center;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
.nav-links a {
text-decoration: none;
color: var(--secondary);
font-weight: 500;
font-size: 0.95rem;
transition: color 0.2s, transform 0.2s;
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
.nav-links a:hover {
color: var(--accent);
transform: translateY(-1px);
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
.logo {
font-weight: 800;
font-size: 1.5rem;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-decoration: none;
letter-spacing: -0.5px;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
.hero {
padding: 6rem 0 4rem;
text-align: center;
position: relative;
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
h1 {
font-size: 3.5rem;
font-weight: 800;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
line-height: 1.1;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
.subtitle {
color: var(--secondary);
font-size: 1.2rem;
max-width: 600px;
margin: 0 auto;
font-weight: 400;
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
.post-grid {
display: grid;
gap: 2.5rem;
margin-bottom: 6rem;
}
.blob {
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);
.post-card {
background: var(--surface-solid);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-sm);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
.post-card:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-hover);
border-color: rgba(37, 99, 235, 0.2);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
.post-card-image {
height: 240px;
background-size: cover;
background-position: center;
border-bottom: 1px solid var(--border);
transition: transform 0.5s ease;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
.post-card:hover .post-card-image {
transform: scale(1.02);
}
@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); }
.post-card-content {
padding: 2rem;
background: var(--surface-solid);
position: relative;
z-index: 1;
}
.admin-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;
.post-card-date {
font-size: 0.85rem;
font-weight: 600;
color: var(--accent);
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
.post-card-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.3;
}
/* 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;
.post-card-summary {
color: var(--secondary);
font-size: 1rem;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-md);
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
text-decoration: none;
font-family: 'Outfit', sans-serif;
letter-spacing: 0.02em;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
.btn-primary {
background: var(--primary);
color: #fff;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15);
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
.btn-primary:hover {
background: var(--accent);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.25);
color: #fff;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
.btn-outline {
border-color: var(--border);
background: var(--surface-solid);
color: var(--text-main);
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.btn-outline:hover {
background: var(--bg);
border-color: var(--secondary);
transform: translateY(-1px);
}
footer {
border-top: 1px solid var(--border);
padding: 4rem 0;
text-align: center;
color: var(--text-muted);
font-size: 0.95rem;
background: var(--surface-solid);
}
/* Post Detail */
.post-detail {
background-color: var(--surface-solid);
}
.post-detail header {
border: none;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
padding: 2rem 0;
position: static;
}
.post-detail .hero-image {
width: 100%;
height: 40vh;
min-height: 300px;
background-size: cover;
background-position: center;
border-radius: var(--radius-lg);
margin-bottom: 3rem;
box-shadow: var(--shadow-lg);
}
.post-detail h1 {
font-size: 3rem;
margin-bottom: 1.5rem;
}
.post-content {
font-size: 1.15rem;
color: var(--secondary);
line-height: 1.8;
padding-bottom: 4rem;
}
.post-content p {
margin-bottom: 1.8rem;
}
.post-content h2 {
font-size: 2rem;
margin-top: 3rem;
margin-bottom: 1.2rem;
color: var(--primary);
}
.post-content img {
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
margin: 2rem 0;
max-width: 100%;
}
/* Admin */
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 3rem;
margin-bottom: 2rem;
}
.admin-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 1rem;
background: var(--surface-solid);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.admin-table th, .admin-table td {
padding: 1.25rem 1.5rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.admin-table th {
background: #f1f5f9;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--secondary);
}
.admin-table tr:last-child td {
border-bottom: none;
}
.admin-table tbody tr {
transition: background-color 0.2s;
}
.admin-table tbody tr:hover {
background-color: #f8fafc;
}
.form-container {
background: var(--surface-solid);
padding: 2.5rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
margin-bottom: 4rem;
}
.form-group {
margin-bottom: 1.25rem;
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.95rem;
color: var(--primary);
}
.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;
width: 100%;
padding: 0.8rem 1rem;
border: 2px solid var(--border);
border-radius: var(--radius-md);
font-size: 1rem;
font-family: inherit;
box-sizing: border-box;
transition: all 0.2s;
background: #f8fafc;
color: var(--text-main);
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
}
outline: none;
border-color: var(--accent);
background: var(--surface-solid);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
textarea.form-control {
min-height: 250px;
line-height: 1.6;
}
.actions {
display: flex;
gap: 0.75rem;
}
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: #10b981;
color: #fff;
padding: 1rem 1.5rem;
border-radius: var(--radius-md);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
display: none;
z-index: 100;
font-weight: 500;
animation: slideUp 0.3s ease-out forwards;
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 640px) {
h1 { font-size: 2.5rem; }
.post-detail h1 { font-size: 2.2rem; }
.nav-links { gap: 1rem; }
.hero { padding: 4rem 0 3rem; }
}
/* Quill Editor Overrides */
.ql-toolbar.ql-snow {
border: 2px solid var(--border);
border-bottom: none;
border-top-left-radius: var(--radius-md);
border-top-right-radius: var(--radius-md);
background: #f8fafc;
font-family: 'Inter', sans-serif;
padding: 12px;
}
.ql-container.ql-snow {
border: 2px solid var(--border);
border-bottom-left-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
background: var(--surface-solid);
font-family: 'Inter', sans-serif;
font-size: 1.1rem;
transition: all 0.2s;
min-height: 400px;
}
.ql-container.ql-snow:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.ql-editor {
min-height: 400px;
line-height: 1.8;
color: var(--text-main);
padding: 1.5rem;
}
.ql-editor p {
margin-bottom: 1.2rem;
}
.ql-editor h2, .ql-editor h3, .ql-editor h4 {
color: var(--primary);
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.ql-editor img {
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
max-width: 100%;
}

View File

@ -1,39 +1,179 @@
// Main JS file for blog interactivity
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const appendMessage = (text, sender) => {
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');
}
});
// Show toast message from URL parameter if present
const urlParams = new URLSearchParams(window.location.search);
const msg = urlParams.get('msg');
if (msg) {
showToast(msg);
}
});
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
toast.style.display = 'block';
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transition = 'opacity 0.5s';
setTimeout(() => toast.remove(), 500);
}, 3000);
}
// --- Interactive Background ---
document.addEventListener('DOMContentLoaded', () => {
initInteractiveBackground();
});
function initInteractiveBackground() {
// 1. Mouse Glow & Parallax shapes
const mouseShape = document.querySelector('.shape-mouse');
const bgShapes = document.querySelectorAll('.bg-shapes .shape:not(.shape-mouse)');
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let currentX = mouseX;
let currentY = mouseY;
// Parallax variables
const windowCenterX = window.innerWidth / 2;
const windowCenterY = window.innerHeight / 2;
document.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
// Make the mouse glow visible once mouse moves
if (mouseShape && mouseShape.style.opacity === '0') {
mouseShape.style.opacity = '1';
}
// Simple parallax for the background blobs
bgShapes.forEach(shape => {
const speed = parseFloat(shape.getAttribute('data-speed') || 1);
const x = (windowCenterX - mouseX) * speed * 0.05;
const y = (windowCenterY - mouseY) * speed * 0.05;
// Use translation combined with the existing animation (in CSS, we'll just layer it)
// Note: Since css animation overrides transform, we can apply parallax translation
// by updating margin or a nested div. Alternatively, since shape-1 and shape-2
// have keyframes using transform, we shouldn't overwrite transform directly.
// Let's use margin instead for a simple offset!
shape.style.marginLeft = `${x}px`;
shape.style.marginTop = `${y}px`;
});
});
// Smooth follow for the glowing orb
function animateGlow() {
if (mouseShape) {
currentX += (mouseX - currentX) * 0.1;
currentY += (mouseY - currentY) * 0.1;
// Center the 300x300 shape on the cursor (so subtract half width/height)
mouseShape.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
requestAnimationFrame(animateGlow);
}
animateGlow();
// 2. Interactive Canvas Particles
const canvas = document.getElementById('bg-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let particles = [];
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initParticles();
}
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 0.5;
this.baseX = this.x;
this.baseY = this.y;
this.density = (Math.random() * 30) + 1;
this.speedX = (Math.random() - 0.5) * 0.5;
this.speedY = (Math.random() - 0.5) * 0.5;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
// Bounce off edges
if (this.x > canvas.width || this.x < 0) this.speedX = -this.speedX;
if (this.y > canvas.height || this.y < 0) this.speedY = -this.speedY;
// Mouse interaction (repel)
let dx = mouseX - this.x;
let dy = mouseY - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
let forceDirectionX = dx / distance;
let forceDirectionY = dy / distance;
let maxDistance = 150;
let force = (maxDistance - distance) / maxDistance;
let directionX = forceDirectionX * force * this.density;
let directionY = forceDirectionY * force * this.density;
if (distance < maxDistance) {
this.x -= directionX;
this.y -= directionY;
}
}
draw() {
ctx.fillStyle = 'rgba(37, 99, 235, 0.4)';
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
}
function initParticles() {
particles = [];
let numParticles = Math.min((canvas.width * canvas.height) / 10000, 100); // cap at 100 for perf
for (let i = 0; i < numParticles; i++) {
particles.push(new Particle());
}
}
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw();
// Connect particles
for (let j = i; j < particles.length; j++) {
let dx = particles[i].x - particles[j].x;
let dy = particles[i].y - particles[j].y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120) {
ctx.beginPath();
ctx.strokeStyle = `rgba(37, 99, 235, ${0.1 * (1 - distance/120)})`;
ctx.lineWidth = 1;
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
ctx.closePath();
}
}
}
requestAnimationFrame(animateParticles);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
animateParticles();
}

199
index.php
View File

@ -1,150 +1,91 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once 'db/config.php';
$db = db();
$stmt = $db->query("SELECT * FROM posts ORDER BY created_at DESC");
$posts = $stmt->fetchAll();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Our Blog';
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Sharing updates and insights on design and technology.';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? 'https://images.unsplash.com/photo-1499750310107-5fef28a66643?auto=format&fit=crop&q=80&w=1000';
?>
<!doctype html>
<html lang="en">
<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 -->
<title><?= htmlspecialchars($projectName) ?></title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="og:title" content="<?= htmlspecialchars($projectName) ?>" />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="<?= htmlspecialchars($projectName) ?>" />
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
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>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
</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 class="bg-shapes">
<canvas id="bg-canvas"></canvas>
<div class="shape shape-1" data-speed="2"></div>
<div class="shape shape-2" data-speed="-1.5"></div>
<div class="shape shape-mouse"></div>
</div>
<header>
<div class="container nav">
<a href="index.php" class="logo"><?= htmlspecialchars($projectName) ?></a>
<div class="nav-links">
<a href="index.php">Home</a>
<a href="admin/index.php">Admin</a>
</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>
</header>
<main>
<section class="hero">
<div class="container">
<h1>Welcome to <span style="background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"><?= htmlspecialchars($projectName) ?></span></h1>
<p class="subtitle"><?= htmlspecialchars($projectDescription) ?></p>
</div>
</section>
<section class="container">
<div class="post-grid">
<?php foreach ($posts as $post): ?>
<a href="post.php?slug=<?= htmlspecialchars($post['slug']) ?>" class="post-card">
<?php if ($post['image_url']): ?>
<div class="post-card-image" style="background-image: url('<?= htmlspecialchars($post['image_url']) ?>')"></div>
<?php else: ?>
<div class="post-card-image" style="background-image: url('https://images.unsplash.com/photo-1555066931-4365d14bab8c?auto=format&fit=crop&q=80&w=800')"></div>
<?php endif; ?>
<div class="post-card-content">
<div class="post-card-date"><?= date('M j, Y', strtotime($post['created_at'])) ?></div>
<h2 class="post-card-title"><?= htmlspecialchars($post['title']) ?></h2>
<p class="post-card-summary"><?= htmlspecialchars($post['summary']) ?></p>
</div>
</a>
<?php endforeach; ?>
<?php if (empty($posts)): ?>
<div style="text-align: center; padding: 4rem; background: var(--surface-solid); border-radius: var(--radius-lg); border: 1px dashed var(--border);">
<h3>No posts yet.</h3>
<p class="subtitle">Head to the Admin panel to write your first article.</p>
</div>
<?php endif; ?>
</div>
</section>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
<div class="container">
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($projectName) ?>. Beautifully crafted with Flatlogic AI.</p>
</div>
</footer>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>
</html>

78
post.php Normal file
View File

@ -0,0 +1,78 @@
<?php
require_once 'db/config.php';
$slug = $_GET['slug'] ?? '';
$db = db();
$stmt = $db->prepare("SELECT * FROM posts WHERE slug = ?");
$stmt->execute([$slug]);
$post = $stmt->fetch();
if (!$post) {
header("Location: index.php");
exit;
}
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Our Blog';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($post['title']) ?> | <?= htmlspecialchars($projectName) ?></title>
<meta name="description" content="<?= htmlspecialchars($post['summary']) ?>" />
<meta property="og:title" content="<?= htmlspecialchars($post['title']) ?> | <?= htmlspecialchars($projectName) ?>" />
<meta property="og:description" content="<?= htmlspecialchars($post['summary']) ?>" />
<meta property="og:image" content="<?= htmlspecialchars($post['image_url']) ?>" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="<?= htmlspecialchars($post['title']) ?> | <?= htmlspecialchars($projectName) ?>" />
<meta property="twitter:description" content="<?= htmlspecialchars($post['summary']) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($post['image_url']) ?>" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
</head>
<body class="post-detail">
<div class="bg-shapes">
<canvas id="bg-canvas"></canvas>
<div class="shape shape-1" data-speed="2"></div>
<div class="shape shape-2" data-speed="-1.5"></div>
<div class="shape shape-mouse"></div>
</div>
<header>
<div class="container nav">
<a href="index.php" class="logo"><?= htmlspecialchars($projectName) ?></a>
<div class="nav-links">
<a href="index.php">Home</a>
<a href="admin/index.php">Admin</a>
</div>
</div>
</header>
<main>
<article class="container">
<div style="text-align: center; margin-bottom: 3rem; margin-top: 1rem;">
<div class="post-card-date" style="font-size: 1rem; margin-bottom: 1rem;"><?= date('F j, Y', strtotime($post['created_at'])) ?></div>
<h1><?= htmlspecialchars($post['title']) ?></h1>
<p class="subtitle"><?= htmlspecialchars($post['summary']) ?></p>
</div>
<?php if ($post['image_url']): ?>
<div class="hero-image" style="background-image: url('<?= htmlspecialchars($post['image_url']) ?>');"></div>
<?php endif; ?>
<div class="post-content">
<?= $post['content'] ?>
</div>
</article>
</main>
<footer>
<div class="container">
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($projectName) ?>. Built with Flatlogic AI.</p>
</div>
</footer>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>