325 lines
11 KiB
HTML
325 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Blog – Journey Mapper</title>
|
||
|
||
<!-- Open Props CSS -->
|
||
<link rel="stylesheet" href="https://unpkg.com/open-props"/>
|
||
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css"/>
|
||
|
||
<!-- Font Awesome -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
|
||
<!-- Google Fonts -->
|
||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
|
||
<!-- Responsive Design -->
|
||
<link rel="stylesheet" href="css/responsive.css">
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
font-family: var(--font-sans);
|
||
background: var(--gray-0);
|
||
color: var(--gray-9);
|
||
line-height: var(--font-lineheight-3);
|
||
margin: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* Header */
|
||
.site-header {
|
||
background: var(--gray-9);
|
||
padding: var(--size-4) var(--size-6);
|
||
border-bottom: 1px solid var(--surface-4);
|
||
}
|
||
.site-header .container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: var(--size-2);
|
||
}
|
||
.site-title {
|
||
margin: 0;
|
||
font-size: var(--font-size-4);
|
||
font-weight: var(--font-weight-6);
|
||
}
|
||
.site-title a {
|
||
color: var(--indigo-4);
|
||
text-decoration: none;
|
||
}
|
||
.site-nav {
|
||
display: flex;
|
||
gap: var(--size-4);
|
||
}
|
||
.site-nav a {
|
||
color: var(--gray-2);
|
||
text-decoration: none;
|
||
font-weight: var(--font-weight-5);
|
||
transition: color 0.2s;
|
||
padding: var(--size-1) var(--size-2);
|
||
border-radius: var(--radius-2);
|
||
}
|
||
.site-nav a:hover,
|
||
.site-nav a.active {
|
||
color: var(--indigo-4);
|
||
background: var(--surface-2);
|
||
}
|
||
|
||
/* User menu */
|
||
.user-menu {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--size-2);
|
||
}
|
||
.user-menu .username {
|
||
color: var(--gray-2);
|
||
font-weight: var(--font-weight-5);
|
||
}
|
||
.logout-btn {
|
||
background: var(--indigo-7);
|
||
color: white;
|
||
border: none;
|
||
border-radius: var(--radius-2);
|
||
padding: var(--size-1) var(--size-3);
|
||
cursor: pointer;
|
||
font-size: var(--font-size-1);
|
||
font-weight: var(--font-weight-5);
|
||
transition: background 0.2s;
|
||
}
|
||
.logout-btn:hover {
|
||
background: var(--indigo-8);
|
||
}
|
||
|
||
/* Main content */
|
||
.blog-container {
|
||
max-width: 1200px;
|
||
margin: var(--size-6) auto;
|
||
padding: 0 var(--size-4);
|
||
}
|
||
.blog-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: var(--size-6);
|
||
}
|
||
.blog-header h1 {
|
||
margin: 0;
|
||
font-size: var(--font-size-6);
|
||
color: var(--indigo-8);
|
||
}
|
||
.btn {
|
||
padding: var(--size-2) var(--size-4);
|
||
border: none;
|
||
border-radius: var(--radius-2);
|
||
cursor: pointer;
|
||
font-size: var(--font-size-2);
|
||
font-weight: var(--font-weight-5);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--size-2);
|
||
transition: all 0.2s var(--ease-2);
|
||
box-shadow: var(--shadow-2);
|
||
background: var(--indigo-7);
|
||
color: white;
|
||
text-decoration: none;
|
||
}
|
||
.btn:hover {
|
||
background: var(--indigo-8);
|
||
box-shadow: var(--shadow-3);
|
||
}
|
||
.posts-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: var(--size-6);
|
||
}
|
||
.post-card {
|
||
background: var(--surface-1);
|
||
border-radius: var(--radius-3);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-2);
|
||
transition: transform 0.2s;
|
||
}
|
||
.post-card:hover {
|
||
transform: translateY(-4px);
|
||
}
|
||
.post-card-image {
|
||
width: 100%;
|
||
height: 200px;
|
||
object-fit: cover;
|
||
background: var(--surface-3);
|
||
}
|
||
.post-card-content {
|
||
padding: var(--size-4);
|
||
}
|
||
.post-card-title {
|
||
margin: 0 0 var(--size-2) 0;
|
||
font-size: var(--font-size-4);
|
||
}
|
||
.post-card-title a {
|
||
color: var(--gray-9);
|
||
text-decoration: none;
|
||
}
|
||
.post-card-title a:hover {
|
||
color: var(--indigo-7);
|
||
}
|
||
.post-card-meta {
|
||
color: var(--gray-6);
|
||
font-size: var(--font-size-1);
|
||
margin-bottom: var(--size-3);
|
||
}
|
||
.post-card-excerpt {
|
||
color: var(--gray-7);
|
||
line-height: var(--font-lineheight-3);
|
||
}
|
||
.empty-state {
|
||
text-align: center;
|
||
color: var(--gray-6);
|
||
padding: var(--size-8);
|
||
font-style: italic;
|
||
}
|
||
.toast {
|
||
position: fixed;
|
||
bottom: var(--size-4);
|
||
right: var(--size-4);
|
||
background: var(--green-7);
|
||
color: white;
|
||
padding: var(--size-2) var(--size-4);
|
||
border-radius: var(--radius-2);
|
||
display: none;
|
||
z-index: 1100;
|
||
}
|
||
.filter-tabs {
|
||
display: flex;
|
||
gap: var(--size-2);
|
||
}
|
||
.filter-btn {
|
||
background: var(--surface-3);
|
||
color: var(--text-2);
|
||
border: none;
|
||
border-radius: var(--radius-2);
|
||
padding: var(--size-1) var(--size-3);
|
||
cursor: pointer;
|
||
font-size: var(--font-size-1);
|
||
font-weight: var(--font-weight-5);
|
||
transition: all 0.2s;
|
||
}
|
||
.filter-btn.active {
|
||
background: var(--indigo-7);
|
||
color: white;
|
||
}
|
||
.filter-btn:hover {
|
||
background: var(--surface-4);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="site-header">
|
||
<div class="container">
|
||
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
||
<div style="display: flex; align-items: center; gap: var(--size-4);">
|
||
<div class="filter-tabs">
|
||
<button class="filter-btn active" data-filter="all">All</button>
|
||
<button class="filter-btn" data-filter="own">My Posts</button>
|
||
<button class="filter-btn" data-filter="public">Public Posts</button>
|
||
</div>
|
||
<nav class="site-nav">
|
||
<a href="map-page.html">Map</a>
|
||
<a href="blog-list.html" class="active">Blog</a>
|
||
</nav>
|
||
<div class="user-menu" id="user-menu"></div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="blog-container">
|
||
<div class="blog-header">
|
||
<h1><i class="fas fa-newspaper"></i> Blog Posts</h1>
|
||
<a href="map-page.html" class="btn"><i class="fas fa-plus"></i> New Journey</a>
|
||
</div>
|
||
<div id="posts-grid" class="posts-grid">
|
||
<!-- Posts loaded dynamically -->
|
||
</div>
|
||
</main>
|
||
|
||
<div id="toast" class="toast"></div>
|
||
|
||
<script src="js/auth.js"></script>
|
||
<script>
|
||
let allJourneys = [];
|
||
|
||
// ==================== LOAD JOURNEYS ====================
|
||
async function loadJourneys() {
|
||
try {
|
||
const res = await fetch(`${API_BASE}/journeys`, { credentials: 'include' });
|
||
if (!res.ok) throw new Error('Failed to fetch journeys');
|
||
const journeys = await res.json();
|
||
allJourneys = journeys;
|
||
renderJourneys(journeys);
|
||
} catch (err) {
|
||
console.error(err);
|
||
document.getElementById('posts-grid').innerHTML = '<p class="empty-state">Failed to load journeys. Make sure the backend is running.</p>';
|
||
}
|
||
}
|
||
|
||
function renderJourneys(journeys) {
|
||
const container = document.getElementById('posts-grid');
|
||
if (!journeys.length) {
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<p>No journeys yet. Create one on the map!</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = journeys.map(journey => `
|
||
<article class="post-card">
|
||
${journey.image ? `<img class="post-card-image" src="${journey.image}" alt="${journey.title}">` : '<div class="post-card-image" style="background: var(--surface-3); display: flex; align-items: center; justify-content: center;"><i class="fas fa-image" style="font-size: 3rem; color: var(--gray-5);"></i></div>'}
|
||
<div class="post-card-content">
|
||
<h2 class="post-card-title"><a href="blog-post.html?id=${journey.id}">${escapeHtml(journey.title)}</a></h2>
|
||
<div class="post-card-meta">
|
||
<i class="fas fa-calendar-alt"></i> ${new Date(journey.created_at).toLocaleDateString()}
|
||
${journey.markers ? `<span style="margin-left: 12px;"><i class="fas fa-map-marker-alt"></i> ${journey.markers.length} chapters</span>` : ''}
|
||
${journey.visibility === 'public' ? '<span class="badge">Public</span>' : ''}
|
||
</div>
|
||
<div class="post-card-excerpt">${escapeHtml(journey.description || journey.markers?.[0]?.text?.substring(0, 150) + '…')}</div>
|
||
</div>
|
||
</article>
|
||
`).join('');
|
||
}
|
||
|
||
function filterJourneys(filter) {
|
||
if (filter === 'all') {
|
||
renderJourneys(allJourneys);
|
||
} else if (filter === 'own') {
|
||
renderJourneys(allJourneys.filter(j => j.owner_id === currentUser?.id));
|
||
} else if (filter === 'public') {
|
||
renderJourneys(allJourneys.filter(j => j.visibility === 'public'));
|
||
}
|
||
}
|
||
|
||
// ==================== INITIALIZATION ====================
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
const authenticated = await checkAuthAndRedirect();
|
||
if (!authenticated) return;
|
||
updateUserMenu();
|
||
loadJourneys();
|
||
|
||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
filterJourneys(btn.dataset.filter);
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |