Add login page and authentification JS
This commit is contained in:
parent
f88fed7c89
commit
fec4f513c8
8
backend/data/users.json
Normal file
8
backend/data/users.json
Normal file
@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"username": "josh",
|
||||
"password_hash": "scrypt:32768:8:1$HA70PiOwbBrIwlDq$2ab80bdc08bb3bb4214258566aded836062323380491a7f4c7f2e67bdccb8686367789f57b3c6c5eb3e2f08c8c07186f47f9c89d1e72179ddd3758b509f23fbe",
|
||||
"created_at": "2026-03-27T20:32:43.107028"
|
||||
}
|
||||
]
|
||||
1
backend/data/users/1/journeys.json
Normal file
1
backend/data/users/1/journeys.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
1
backend/data/users/1/posts.json
Normal file
1
backend/data/users/1/posts.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
124
blog-list.html
124
blog-list.html
@ -12,7 +12,7 @@
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
@ -28,7 +28,7 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header (same as map page) */
|
||||
/* Header */
|
||||
.site-header {
|
||||
background: var(--gray-9);
|
||||
padding: var(--size-4) var(--size-6);
|
||||
@ -70,6 +70,31 @@
|
||||
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;
|
||||
@ -157,35 +182,104 @@
|
||||
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;
|
||||
}
|
||||
</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);">
|
||||
<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="blog-post.html?new" class="btn"><i class="fas fa-plus"></i> New Post</a>
|
||||
<a href="blog-post-edit.html?new" class="btn"><i class="fas fa-plus"></i> New Post</a>
|
||||
</div>
|
||||
<div id="posts-grid" class="posts-grid">
|
||||
<!-- Posts loaded dynamically -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://127.0.0.1:5000/api';
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script>
|
||||
// ==================== AUTH ====================
|
||||
const API_BASE = 'http://127.0.0.1:5000/api';
|
||||
let currentUser = null;
|
||||
|
||||
async function checkAuthAndRedirect() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/me`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
currentUser = await res.json();
|
||||
return true;
|
||||
} else {
|
||||
window.location.href = 'login.html';
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
window.location.href = 'login.html';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserMenu() {
|
||||
const container = document.getElementById('user-menu');
|
||||
if (currentUser) {
|
||||
container.innerHTML = `
|
||||
<span class="username"><i class="fas fa-user"></i> ${escapeHtml(currentUser.username)}</span>
|
||||
<button id="logout-btn" class="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</button>
|
||||
`;
|
||||
document.getElementById('logout-btn')?.addEventListener('click', logout);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch(`${API_BASE}/logout`, { method: 'POST', credentials: 'include' });
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(msg, isError = false) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg;
|
||||
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
||||
}
|
||||
|
||||
// ==================== BLOG POSTS ====================
|
||||
async function loadPosts() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/blog-posts`);
|
||||
const res = await fetch(`${API_BASE}/blog-posts`, { credentials: 'include' });
|
||||
if (!res.ok) throw new Error('Failed to fetch posts');
|
||||
const posts = await res.json();
|
||||
renderPosts(posts);
|
||||
@ -206,7 +300,7 @@
|
||||
<article class="post-card">
|
||||
${post.image ? `<img class="post-card-image" src="${post.image}" alt="${post.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=${post.id}">${escapeHtml(post.title)}</a></h2>
|
||||
<h2 class="post-card-title"><a href="blog-post-edit.html?id=${post.id}">${escapeHtml(post.title)}</a></h2>
|
||||
<div class="post-card-meta">
|
||||
<i class="fas fa-calendar-alt"></i> ${new Date(post.created_at).toLocaleDateString()}
|
||||
${post.journeyId ? `<span style="margin-left: 12px;"><i class="fas fa-route"></i> Journey #${post.journeyId}</span>` : ''}
|
||||
@ -217,17 +311,13 @@
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
// ==================== INITIALIZATION ====================
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const authenticated = await checkAuthAndRedirect();
|
||||
if (!authenticated) return;
|
||||
updateUserMenu();
|
||||
loadPosts();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadPosts);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
370
blog-page.html
370
blog-page.html
@ -1,370 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blog Post – 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: Poppins (matching map page) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ===== GLOBAL RESET USING OPEN PROPS ===== */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--gray-0);
|
||||
color: var(--gray-9);
|
||||
line-height: var(--font-lineheight-3);
|
||||
}
|
||||
|
||||
/* ===== 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: 1200px;
|
||||
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;
|
||||
}
|
||||
.site-nav a:hover {
|
||||
color: var(--indigo-4);
|
||||
}
|
||||
|
||||
/* ===== MAIN LAYOUT (two‑column) ===== */
|
||||
.post-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-6);
|
||||
padding: var(--size-6) var(--size-4);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.post {
|
||||
background: var(--surface-1);
|
||||
border-radius: var(--radius-3);
|
||||
padding: var(--size-6);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: var(--font-size-6);
|
||||
font-weight: var(--font-weight-7);
|
||||
margin: 0 0 var(--size-2) 0;
|
||||
color: var(--gray-9);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
color: var(--gray-6);
|
||||
font-size: var(--font-size-1);
|
||||
margin-bottom: var(--size-4);
|
||||
border-bottom: 1px solid var(--surface-4);
|
||||
padding-bottom: var(--size-2);
|
||||
}
|
||||
|
||||
.post-hero {
|
||||
margin: var(--size-4) 0;
|
||||
}
|
||||
.post-hero img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius-2);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
line-height: var(--font-lineheight-4);
|
||||
color: var(--gray-7);
|
||||
}
|
||||
.post-content p {
|
||||
margin-bottom: var(--size-4);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background: var(--surface-1);
|
||||
border-radius: var(--radius-3);
|
||||
padding: var(--size-4);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
.sidebar section {
|
||||
margin-bottom: var(--size-6);
|
||||
}
|
||||
.sidebar h3 {
|
||||
font-size: var(--font-size-3);
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--size-3);
|
||||
color: var(--indigo-7);
|
||||
border-left: 3px solid var(--indigo-6);
|
||||
padding-left: var(--size-2);
|
||||
}
|
||||
|
||||
/* Posts list */
|
||||
#posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-3);
|
||||
}
|
||||
.post-preview {
|
||||
background: var(--surface-2);
|
||||
border-radius: var(--radius-2);
|
||||
padding: var(--size-3);
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.post-preview:hover {
|
||||
background: var(--surface-3);
|
||||
}
|
||||
.post-preview-title {
|
||||
font-weight: var(--font-weight-6);
|
||||
color: var(--gray-9);
|
||||
margin: 0 0 var(--size-1) 0;
|
||||
}
|
||||
.post-preview-meta {
|
||||
font-size: var(--font-size-0);
|
||||
color: var(--gray-6);
|
||||
}
|
||||
|
||||
/* Form for creating/editing posts */
|
||||
.form-group {
|
||||
margin-bottom: var(--size-3);
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: var(--size-1);
|
||||
font-weight: var(--font-weight-5);
|
||||
color: var(--gray-7);
|
||||
}
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: var(--size-2) var(--size-3);
|
||||
border: 1px solid var(--surface-4);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--surface-2);
|
||||
color: var(--text-1);
|
||||
font-family: inherit;
|
||||
font-size: var(--font-size-2);
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--indigo-6);
|
||||
box-shadow: 0 0 0 3px var(--indigo-2);
|
||||
}
|
||||
|
||||
/* Buttons (reuse map page classes) */
|
||||
.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);
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--indigo-7);
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--indigo-8);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: var(--gray-7);
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: var(--gray-8);
|
||||
}
|
||||
.btn-danger {
|
||||
background: var(--red-7);
|
||||
color: white;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: var(--red-8);
|
||||
}
|
||||
|
||||
/* Comments section */
|
||||
.comments {
|
||||
max-width: 1200px;
|
||||
margin: var(--size-6) auto 0;
|
||||
padding: 0 var(--size-4);
|
||||
}
|
||||
.comments h3 {
|
||||
font-size: var(--font-size-4);
|
||||
margin-bottom: var(--size-4);
|
||||
color: var(--indigo-7);
|
||||
}
|
||||
.comments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
.comment-item {
|
||||
background: var(--surface-1);
|
||||
border-radius: var(--radius-2);
|
||||
padding: var(--size-4);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
.comment-item .meta {
|
||||
color: var(--gray-6);
|
||||
font-size: var(--font-size-0);
|
||||
margin-bottom: var(--size-2);
|
||||
}
|
||||
.comment-item p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
background: var(--surface-2);
|
||||
padding: var(--size-4);
|
||||
margin-top: var(--size-6);
|
||||
text-align: center;
|
||||
color: var(--gray-6);
|
||||
font-size: var(--font-size-1);
|
||||
border-top: 1px solid var(--surface-4);
|
||||
}
|
||||
|
||||
/* Responsive two‑column layout */
|
||||
@media (min-width: 768px) {
|
||||
.post-page {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: var(--size-6);
|
||||
padding: var(--size-6);
|
||||
}
|
||||
.post {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
.sidebar {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: var(--size-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<h1 class="site-title"><a href="map-page.html">Journey Mapper</a></h1>
|
||||
<nav class="site-nav">
|
||||
<a href="map-page.html">Map</a>
|
||||
<a href="blog-page.html" class="active">Blog</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="post-page">
|
||||
<article class="post">
|
||||
<h2 class="post-title">Post Title</h2>
|
||||
<div class="post-meta">By <span class="author">Author</span> — <time datetime="2026-01-01">Jan 1, 2026</time></div>
|
||||
<figure class="post-hero">
|
||||
<img src="https://via.placeholder.com/800x350?text=Hero+Image" alt="Post image">
|
||||
</figure>
|
||||
<div class="post-content">
|
||||
<p>This is a placeholder paragraph for the blog post content. Replace with real content.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="sidebar">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<h3 style="margin:0">Posts</h3>
|
||||
<button id="create-post-btn" class="btn btn-primary">New</button>
|
||||
</div>
|
||||
|
||||
<div id="posts-list">
|
||||
<!-- JS will render list of posts here -->
|
||||
</div>
|
||||
|
||||
<section class="about" id="post-form" style="display:none;margin-top:16px;">
|
||||
<h3 id="form-title">New Post</h3>
|
||||
<form id="blog-form">
|
||||
<div class="form-group">
|
||||
<label for="post-title-input">Title</label>
|
||||
<input id="post-title-input" name="title" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post-journey-input">Journey ID</label>
|
||||
<input id="post-journey-input" name="journeyId" placeholder="optional" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post-image-input">Image URL</label>
|
||||
<input id="post-image-input" name="image" placeholder="https://..." />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post-content-input">Content</label>
|
||||
<textarea id="post-content-input" name="content" rows="6" required></textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" id="cancel-post-btn" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="about" style="margin-top:18px;">
|
||||
<h3>About</h3>
|
||||
<p>Short author bio or related links.</p>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<section class="comments">
|
||||
<h3>Comments</h3>
|
||||
<div class="comments-list"></div>
|
||||
</section>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">© 2026 My Blog</div>
|
||||
</footer>
|
||||
|
||||
<!-- JavaScript (keep your existing logic) -->
|
||||
<script src="js/main.js"></script>
|
||||
<script src="js/blog-posts.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,19 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Blog Post – 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"/>
|
||||
<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">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
@ -69,6 +69,31 @@
|
||||
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 */
|
||||
.post-container {
|
||||
max-width: 800px;
|
||||
@ -170,10 +195,13 @@
|
||||
<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);">
|
||||
<nav class="site-nav">
|
||||
<a href="map-page.html">Map</a>
|
||||
<a href="blog-list.html">Blog</a>
|
||||
<a href="blog-list.html" class="active">Blog</a>
|
||||
</nav>
|
||||
<div class="user-menu" id="user-menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -183,7 +211,7 @@
|
||||
<form id="post-form">
|
||||
<div class="form-group">
|
||||
<label for="post-title">Title</label>
|
||||
<input type="text" id="post-title" required>
|
||||
<input type="text" id="post-title" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post-content">Content</label>
|
||||
@ -191,13 +219,13 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post-journey">Associated Journey ID (optional)</label>
|
||||
<input type="text" id="post-journey" placeholder="e.g., 3">
|
||||
<input type="text" id="post-journey" placeholder="e.g., 3" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="post-image">Image URL or Upload</label>
|
||||
<input type="text" id="post-image-url" placeholder="https://...">
|
||||
<div style="margin: 8px 0; text-align: center;">or</div>
|
||||
<input type="file" id="image-upload" accept="image/*">
|
||||
<input type="text" id="post-image-url" placeholder="https://..." />
|
||||
<div style="margin: 8px 0; text-align: center">or</div>
|
||||
<input type="file" id="image-upload" accept="image/*" />
|
||||
<div class="image-preview" id="image-preview"></div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
@ -212,17 +240,69 @@
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script>
|
||||
// ==================== AUTH ====================
|
||||
const API_BASE = 'http://127.0.0.1:5000/api';
|
||||
let currentUser = null;
|
||||
let currentPostId = null;
|
||||
|
||||
// Get post id from URL
|
||||
async function checkAuthAndRedirect() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/me`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
currentUser = await res.json();
|
||||
return true;
|
||||
} else {
|
||||
window.location.href = 'login.html';
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
window.location.href = 'login.html';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserMenu() {
|
||||
const container = document.getElementById('user-menu');
|
||||
if (currentUser) {
|
||||
container.innerHTML = `
|
||||
<span class="username"><i class="fas fa-user"></i> ${escapeHtml(currentUser.username)}</span>
|
||||
<button id="logout-btn" class="logout-btn"><i class="fas fa-sign-out-alt"></i> Logout</button>
|
||||
`;
|
||||
document.getElementById('logout-btn')?.addEventListener('click', logout);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await fetch(`${API_BASE}/logout`, { method: 'POST', credentials: 'include' });
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message, isError = false) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
||||
}
|
||||
|
||||
// ==================== POST CRUD ====================
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const postId = urlParams.get('id');
|
||||
const isNew = urlParams.has('new');
|
||||
|
||||
async function loadPost(id) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/blog-posts/${id}`);
|
||||
const res = await fetch(`${API_BASE}/blog-posts/${id}`, { credentials: 'include' });
|
||||
if (!res.ok) throw new Error('Post not found');
|
||||
const post = await res.json();
|
||||
currentPostId = post.id;
|
||||
@ -258,12 +338,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
title,
|
||||
content,
|
||||
journeyId: journeyId || null,
|
||||
image: image || null
|
||||
};
|
||||
const payload = { title, content, journeyId: journeyId || null, image: image || null };
|
||||
|
||||
try {
|
||||
let url, method;
|
||||
@ -278,17 +353,14 @@
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(payload),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Save failed');
|
||||
const data = await res.json();
|
||||
showToast('Post saved successfully');
|
||||
// Redirect to the new post if it was created
|
||||
if (isNew) {
|
||||
window.location.href = `blog-post.html?id=${data.id}`;
|
||||
} else {
|
||||
// reload if needed
|
||||
window.location.href = `blog-post-edit.html?id=${data.id}`;
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Error saving post', true);
|
||||
@ -299,7 +371,10 @@
|
||||
if (!currentPostId) return;
|
||||
if (!confirm('Delete this post?')) return;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/blog-posts/${currentPostId}`, { method: 'DELETE' });
|
||||
const res = await fetch(`${API_BASE}/blog-posts/${currentPostId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) throw new Error('Delete failed');
|
||||
showToast('Post deleted');
|
||||
setTimeout(() => { window.location.href = 'blog-list.html'; }, 1000);
|
||||
@ -308,15 +383,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, isError = false) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
||||
}
|
||||
|
||||
// Image upload (convert to base64 and set as image URL)
|
||||
// Image upload (convert to base64)
|
||||
document.getElementById('image-upload').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
@ -332,6 +399,12 @@
|
||||
document.getElementById('post-form').addEventListener('submit', savePost);
|
||||
document.getElementById('delete-post').addEventListener('click', deletePost);
|
||||
|
||||
// ==================== INITIALIZATION ====================
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const authenticated = await checkAuthAndRedirect();
|
||||
if (!authenticated) return;
|
||||
updateUserMenu();
|
||||
|
||||
if (!isNew && postId) {
|
||||
loadPost(postId);
|
||||
} else if (isNew) {
|
||||
@ -339,9 +412,10 @@
|
||||
document.getElementById('form-title').textContent = 'New Post';
|
||||
document.getElementById('delete-post').style.display = 'none';
|
||||
} else {
|
||||
// No id and not new – maybe go to list
|
||||
// No id and not new – redirect to list
|
||||
window.location.href = 'blog-list.html';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
240
login.html
Normal file
240
login.html
Normal file
@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login – 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">
|
||||
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--gray-0);
|
||||
color: var(--gray-9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: var(--size-4);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
background: var(--surface-1);
|
||||
border-radius: var(--radius-3);
|
||||
padding: var(--size-6);
|
||||
box-shadow: var(--shadow-4);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--size-6);
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: var(--indigo-8);
|
||||
margin: 0;
|
||||
font-size: var(--font-size-5);
|
||||
}
|
||||
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
gap: var(--size-2);
|
||||
border-bottom: 1px solid var(--surface-4);
|
||||
margin-bottom: var(--size-4);
|
||||
}
|
||||
.auth-tab {
|
||||
padding: var(--size-2) var(--size-4);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: var(--font-size-2);
|
||||
font-weight: var(--font-weight-5);
|
||||
color: var(--gray-6);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.auth-tab.active {
|
||||
color: var(--indigo-7);
|
||||
border-bottom: 2px solid var(--indigo-7);
|
||||
}
|
||||
.auth-form {
|
||||
display: none;
|
||||
}
|
||||
.auth-form.active {
|
||||
display: block;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: var(--size-4);
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: var(--size-1);
|
||||
font-weight: var(--font-weight-5);
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: var(--size-2) var(--size-3);
|
||||
border: 1px solid var(--surface-4);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--surface-2);
|
||||
font-size: var(--font-size-2);
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: var(--size-2);
|
||||
border: none;
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--indigo-7);
|
||||
color: white;
|
||||
font-size: var(--font-size-2);
|
||||
font-weight: var(--font-weight-5);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
background: var(--indigo-8);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1><i class="fas fa-map-marked-alt"></i> Journey Mapper</h1>
|
||||
<p>Sign in to continue</p>
|
||||
</div>
|
||||
<div class="auth-tabs">
|
||||
<button class="auth-tab active" data-tab="login">Login</button>
|
||||
<button class="auth-tab" data-tab="register">Register</button>
|
||||
</div>
|
||||
<div id="login-form" class="auth-form active">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="login-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" id="login-password" required>
|
||||
</div>
|
||||
<button id="login-submit" class="btn">Login</button>
|
||||
</div>
|
||||
<div id="register-form" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="register-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" id="register-password" required>
|
||||
</div>
|
||||
<button id="register-submit" class="btn">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://127.0.0.1:5000/api';
|
||||
|
||||
function showToast(msg, isError = false) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg;
|
||||
toast.style.backgroundColor = isError ? 'var(--red-7)' : 'var(--green-7)';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Login failed');
|
||||
showToast(`Welcome, ${data.username}!`);
|
||||
window.location.href = 'map-page.html';
|
||||
} catch (err) {
|
||||
showToast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function register(username, password) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Registration failed');
|
||||
showToast(`Registered as ${data.username}. Logging in...`);
|
||||
window.location.href = 'map-page.html';
|
||||
} catch (err) {
|
||||
showToast(err.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.auth-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const target = tab.dataset.tab;
|
||||
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.querySelectorAll('.auth-form').forEach(f => f.classList.remove('active'));
|
||||
document.getElementById(`${target}-form`).classList.add('active');
|
||||
});
|
||||
});
|
||||
// Login button
|
||||
document.getElementById('login-submit').addEventListener('click', () => {
|
||||
const username = document.getElementById('login-username').value.trim();
|
||||
const password = document.getElementById('login-password').value;
|
||||
if (!username || !password) {
|
||||
showToast('Please enter username and password', true);
|
||||
return;
|
||||
}
|
||||
login(username, password);
|
||||
});
|
||||
// Register button
|
||||
document.getElementById('register-submit').addEventListener('click', () => {
|
||||
const username = document.getElementById('register-username').value.trim();
|
||||
const password = document.getElementById('register-password').value;
|
||||
if (!username || !password) {
|
||||
showToast('Please enter username and password', true);
|
||||
return;
|
||||
}
|
||||
if (password.length < 4) {
|
||||
showToast('Password must be at least 4 characters', true);
|
||||
return;
|
||||
}
|
||||
register(username, password);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
891
map-page.html
891
map-page.html
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user