refractor blogpost and cdn chall
This commit is contained in:
1 parent
2d51b73a1e
commit
bb3f6e74aa
47 files changed
-544
No files matched your search
@@ -0,0 +1,82 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
|
||||
<div class="card" style="padding:20px; max-width:900px; margin:auto;">
|
||||
<h2 style="margin-top:0">Create post (upload image)</h2>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="card" style="padding:16px; gap:12px; display:flex; flex-direction:column;">
|
||||
<label>
|
||||
<div class="label-muted">Title</div>
|
||||
<input name="title" placeholder="Post title" type="text">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<div class="label-muted">Content</div>
|
||||
<textarea name="content" rows="6" placeholder="Write something..."></textarea>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<div class="label-muted" style="margin-bottom:6px;">Image</div>
|
||||
|
||||
<div class="file-wrap" style="align-items:center;">
|
||||
<label class="file-btn" for="image">Choose image</label>
|
||||
<span class="label-muted" id="file-name">No file chosen</span>
|
||||
<input id="image" type="file" name="image" accept=".png,.jpg,.jpeg,.bmp" onchange="handleFileChange(this)">
|
||||
</div>
|
||||
|
||||
<div id="preview-wrap" style="margin-top:12px; display:none;">
|
||||
<div class="label-muted" style="margin-bottom:6px;">Preview</div>
|
||||
<img id="preview" alt="preview" style="max-width:320px; border-radius:8px; border:1px solid rgba(255,255,255,0.03);">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:10px; align-items:center;">
|
||||
<button class="btn" type="submit">Create</button>
|
||||
<button type="button" class="button ghost" onclick="resetFile()" style="background:transparent; color:var(--muted); border:1px solid rgba(255,255,255,0.04); padding:8px 12px; border-radius:8px;">
|
||||
Clear file
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function handleFileChange(input) {
|
||||
const file = input.files && input.files[0];
|
||||
const nameSpan = document.getElementById('file-name');
|
||||
const previewWrap = document.getElementById('preview-wrap');
|
||||
const preview = document.getElementById('preview');
|
||||
|
||||
if (!file) {
|
||||
nameSpan.innerText = 'No file chosen';
|
||||
previewWrap.style.display = 'none';
|
||||
preview.src = '';
|
||||
return;
|
||||
}
|
||||
|
||||
nameSpan.innerText = file.name;
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
preview.src = e.target.result;
|
||||
previewWrap.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
previewWrap.style.display = 'none';
|
||||
preview.src = '';
|
||||
}
|
||||
}
|
||||
|
||||
function resetFile() {
|
||||
const input = document.getElementById('image');
|
||||
input.value = '';
|
||||
document.getElementById('file-name').innerText = 'No file chosen';
|
||||
document.getElementById('preview-wrap').style.display = 'none';
|
||||
document.getElementById('preview').src = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,120 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
|
||||
<style>
|
||||
/* Small, page-specific responsive tweaks (safe to keep) */
|
||||
.posts-container{
|
||||
max-width:1200px;
|
||||
margin:18px auto;
|
||||
padding:0 16px;
|
||||
}
|
||||
|
||||
/* responsive grid: auto-fit columns, each at least 320px wide */
|
||||
.posts-grid{
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* each post card is a flexible grid: image column (if present) + content */
|
||||
.post-card{
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr;
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
padding: 14px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* if no image, make content span full width */
|
||||
.post-card.no-image{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* thumbnail styling */
|
||||
.post-card img{
|
||||
width:100%;
|
||||
height:120px;
|
||||
object-fit:cover;
|
||||
border-radius:8px;
|
||||
display:block;
|
||||
border:1px solid rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
/* small adjustments for tighter screens */
|
||||
@media (max-width:640px){
|
||||
.post-card{ grid-template-columns: 1fr; }
|
||||
.post-card img{ height:200px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="posts-container">
|
||||
|
||||
<!-- Search -->
|
||||
<form method="GET" action="{{ url_for('index') }}" style="display:flex; gap:10px; margin-bottom:14px;">
|
||||
<input name="q" placeholder="Search posts..." value="{{ q|default('') }}" style="flex:1; padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,0.03); background:rgba(255,255,255,0.02); color:inherit;">
|
||||
<button class="btn" type="submit" style="min-width:100px;">Search</button>
|
||||
</form>
|
||||
|
||||
<h2 style="margin:0 0 12px 0">Posts</h2>
|
||||
|
||||
<div class="posts-grid">
|
||||
{% for p in posts %}
|
||||
{# determine if image exists to add no-image class #}
|
||||
<article class="card post-card {% if not p['image_filename'] %}no-image{% endif %}">
|
||||
{% if p['image_filename'] %}
|
||||
<div>
|
||||
<a href="{{ url_for('view_post', pid=p['id']) }}">
|
||||
<img src="{{ url_for('uploaded_file', filename=p['image_filename']) }}" alt="img">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h3 style="margin:0 0 6px 0; font-size:18px;">
|
||||
<a href="{{ url_for('view_post', pid=p['id']) }}" style="color:inherit; text-decoration:none;">
|
||||
{{ p['title'] or 'Untitled' }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
|
||||
<span class="label-muted">By {{ p['author'] or 'unknown' }}</span>
|
||||
{% if p['author'] and p['author'] == session.get('username') %}
|
||||
<span class="badge">you</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p style="margin:0 0 10px 0; color:var(--muted);">
|
||||
{% if p['content'] %}
|
||||
{{ (p['content'][:200] + '...') if p['content']|length > 200 else p['content'] }}
|
||||
{% else %}
|
||||
<span class="label-muted">No content</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div style="display:flex; gap:10px; align-items:center; margin-top:8px;">
|
||||
<a class="btn" href="{{ url_for('view_post', pid=p['id']) }}" style="padding:8px 12px; font-size:14px;">Read</a>
|
||||
<span class="label-muted" style="font-size:13px;">Post ID: {{ p['id'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="card" style="text-align:center; padding:24px;">
|
||||
<p style="margin:0; color:var(--muted);">No posts yet.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const q = document.querySelector('input[name="q"]');
|
||||
if (q) q.focus();
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Blogpost</title>
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Blogpost</h1>
|
||||
<nav>
|
||||
<a href="{{ url_for('index') }}">Home</a>
|
||||
{% if session.get('user_id') %}
|
||||
<a href="{{ url_for('create_post') }}">Create</a>
|
||||
<a href="{{ url_for('profile') }}">Profile</a>
|
||||
<a href="{{ url_for('logout') }}">Logout ({{ session.get('username') }})</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}">Login</a>
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for m in messages %}
|
||||
<li>{{ m }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>keii</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<!-- safe to include even if layout already loads it -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
|
||||
<div style="max-width:520px; margin:28px auto;">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for m in messages %}
|
||||
<li>{{ m }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card" style="padding:22px;">
|
||||
<h2 style="margin-top:0">Login</h2>
|
||||
|
||||
<form method="post" action="{{ url_for('login') }}" style="display:flex; flex-direction:column; gap:12px;">
|
||||
<label>
|
||||
<div class="label-muted">Username</div>
|
||||
<input name="username" id="username" type="text" placeholder="your username" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<div class="label-muted" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span>Password</span>
|
||||
<a href="{{ url_for('register') }}" style="font-size:13px; color:var(--muted); text-decoration:none;">Create account</a>
|
||||
</div>
|
||||
<div style="position:relative;">
|
||||
<input name="password" id="password" type="password" placeholder="your password" required style="padding-right:92px;">
|
||||
<button type="button" id="pw-toggle" style="position:absolute; right:6px; top:6px; height:36px; border-radius:8px; border:1px solid rgba(255,255,255,0.04); background:transparent; color:var(--muted); padding:6px 10px; cursor:pointer;">
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div style="display:flex; gap:10px; align-items:center; justify-content:space-between;">
|
||||
<label style="display:flex; gap:8px; align-items:center; font-size:14px; color:var(--muted);">
|
||||
<input type="checkbox" name="remember" style="width:16px; height:16px;">
|
||||
Remember
|
||||
</label>
|
||||
|
||||
<button class="btn" type="submit" style="min-width:120px;">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// autofocus username
|
||||
document.getElementById('username')?.focus();
|
||||
|
||||
// password toggle
|
||||
(function(){
|
||||
const pw = document.getElementById('password');
|
||||
const btn = document.getElementById('pw-toggle');
|
||||
if (!pw || !btn) return;
|
||||
btn.addEventListener('click', () => {
|
||||
if (pw.type === 'password') {
|
||||
pw.type = 'text';
|
||||
btn.innerText = 'Hide';
|
||||
} else {
|
||||
pw.type = 'password';
|
||||
btn.innerText = 'Show';
|
||||
}
|
||||
pw.focus();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<h2>Profile: {{ user['username'] }}</h2>
|
||||
<p>Role: {{ user['role'] }}</p>
|
||||
|
||||
{% if flag %}
|
||||
<h3>FLAG (admin only):</h3>
|
||||
<pre>{{ flag }}</pre>
|
||||
{% else %}
|
||||
<p>No special access.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
|
||||
<div style="max-width:520px; margin:28px auto;">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for m in messages %}
|
||||
<li>{{ m }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card" style="padding:22px;">
|
||||
<h2 style="margin-top:0">Register</h2>
|
||||
|
||||
<form method="post" action="{{ url_for('register') }}" style="display:flex; flex-direction:column; gap:12px;">
|
||||
<label>
|
||||
<div class="label-muted">Username</div>
|
||||
<input name="username" id="reg-username" type="text" placeholder="choose a username" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<div class="label-muted">Password</div>
|
||||
<div style="position:relative;">
|
||||
<input name="password" id="reg-password" type="password" placeholder="create a password" required style="padding-right:92px;">
|
||||
<button type="button" id="reg-pw-toggle" style="position:absolute; right:6px; top:6px; height:36px; border-radius:8px; border:1px solid rgba(255,255,255,0.04); background:transparent; color:var(--muted); padding:6px 10px; cursor:pointer;">
|
||||
Show
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div style="display:flex; gap:10px; align-items:center; justify-content:flex-end;">
|
||||
<a href="{{ url_for('login') }}" class="label-muted" style="text-decoration:none; color:var(--muted);">Already have an account?</a>
|
||||
<button class="btn" type="submit" style="min-width:120px;">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// autofocus username
|
||||
document.getElementById('reg-username')?.focus();
|
||||
|
||||
// password toggle
|
||||
(function(){
|
||||
const pw = document.getElementById('reg-password');
|
||||
const btn = document.getElementById('reg-pw-toggle');
|
||||
if (!pw || !btn) return;
|
||||
btn.addEventListener('click', () => {
|
||||
if (pw.type === 'password') {
|
||||
pw.type = 'text';
|
||||
btn.innerText = 'Hide';
|
||||
} else {
|
||||
pw.type = 'password';
|
||||
btn.innerText = 'Show';
|
||||
}
|
||||
pw.focus();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<article>
|
||||
<h2>{{ post['title'] or 'Untitled' }}</h2>
|
||||
<p>By {{ post['author'] or 'unknown' }}</p>
|
||||
|
||||
{% if post['image_filename'] %}
|
||||
<img src="{{ url_for('uploaded_file', filename=post['image_filename']) }}" style="max-width:400px">
|
||||
{% endif %}
|
||||
|
||||
<p>{{ post['content'] }}</p>
|
||||
</article>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user