{% extends "app.html" %}
|
||
|
||
{% block title %}Forum{% endblock %}
|
||
|
||
{% block header_text %}<nav class="breadcrumb" id="breadcrumb">
|
||
<!-- Breadcrumb will be rendered here -->
|
||
</nav>{% endblock %}
|
||
{% block main %}
|
||
<style>
|
||
snek-forum, .container {
|
||
height: 100%;
|
||
width: 90%;
|
||
background: transparent !important;
|
||
}
|
||
</style>
|
||
<script>
|
||
// snek-forum.js - Forum Web Component (Tech-centric Dark Theme)
|
||
class SnekForum extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.attachShadow({ mode: 'open' });
|
||
this.ws = null;
|
||
this.currentView = 'forums';
|
||
this.currentForum = null;
|
||
this.currentThread = null;
|
||
this.currentPage = 1;
|
||
}
|
||
|
||
connectedCallback() {
|
||
this.render();
|
||
this.connectWebSocket();
|
||
this.loadForums();
|
||
}
|
||
|
||
disconnectedCallback() {
|
||
if (this.ws) {
|
||
this.ws.close();
|
||
}
|
||
}
|
||
|
||
connectWebSocket() {
|
||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/forum/ws`;
|
||
this.ws = new WebSocket(wsUrl);
|
||
|
||
this.ws.onopen = () => {
|
||
console.log('WebSocket connected');
|
||
};
|
||
|
||
this.ws.onmessage = (event) => {
|
||
const data = JSON.parse(event.data);
|
||
this.handleWebSocketMessage(data);
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
console.log('WebSocket disconnected');
|
||
setTimeout(() => this.connectWebSocket(), 3000);
|
||
};
|
||
}
|
||
|
||
handleWebSocketMessage(data) {
|
||
switch (data.type) {
|
||
case 'post_created':
|
||
if (this.currentView === 'thread' && this.currentThread?.uid === data.data.thread_uid) {
|
||
this.addNewPost(data.data.post);
|
||
}
|
||
break;
|
||
case 'post_edited':
|
||
this.updatePost(data.data.post);
|
||
break;
|
||
case 'post_deleted':
|
||
this.removePost(data.data.post.uid);
|
||
break;
|
||
case 'post_liked':
|
||
case 'post_unliked':
|
||
this.updatePostLikes(data.data.post_uid);
|
||
break;
|
||
case 'thread_created':
|
||
if (this.currentView === 'forum' && this.currentForum?.uid === data.data.forum_uid) {
|
||
this.loadForum(this.currentForum.slug);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
subscribe(type, id) {
|
||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||
this.ws.send(JSON.stringify({
|
||
action: 'subscribe',
|
||
type: type,
|
||
id: id
|
||
}));
|
||
}
|
||
}
|
||
|
||
unsubscribe(type, id) {
|
||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||
this.ws.send(JSON.stringify({
|
||
action: 'unsubscribe',
|
||
type: type,
|
||
id: id
|
||
}));
|
||
}
|
||
}
|
||
|
||
async fetchAPI(url, options = {}) {
|
||
const response = await fetch(url, {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options.headers
|
||
}
|
||
});
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
}
|
||
|
||
async loadForums() {
|
||
try {
|
||
const data = await this.fetchAPI('/forum/api/forums');
|
||
this.currentView = 'forums';
|
||
this.renderForums(data.forums);
|
||
this.updateBreadcrumb();
|
||
} catch (error) {
|
||
console.error('Error loading forums:', error);
|
||
}
|
||
}
|
||
|
||
async loadForum(slug, page = 1) {
|
||
try {
|
||
this.currentPage = page;
|
||
const data = await this.fetchAPI(`/forum/api/forums/${slug}?page=${this.currentPage}`);
|
||
this.currentView = 'forum';
|
||
this.currentForum = data.forum;
|
||
this.subscribe('forum', data.forum.uid);
|
||
this.renderForum(data);
|
||
this.updateBreadcrumb();
|
||
} catch (error) {
|
||
console.error('Error loading forum:', error);
|
||
}
|
||
}
|
||
|
||
async loadThread(slug, page = 1) {
|
||
try {
|
||
this.currentPage = page;
|
||
const data = await this.fetchAPI(`/forum/api/threads/${slug}?page=${this.currentPage}`);
|
||
this.currentView = 'thread';
|
||
this.currentThread = data.thread;
|
||
if (this.currentForum) {
|
||
this.unsubscribe('forum', this.currentForum.uid);
|
||
}
|
||
this.subscribe('thread', data.thread.uid);
|
||
this.renderThread(data);
|
||
this.updateBreadcrumb();
|
||
} catch (error) {
|
||
console.error('Error loading thread:', error);
|
||
}
|
||
}
|
||
|
||
async createThread(forumSlug, title, content) {
|
||
try {
|
||
const data = await this.fetchAPI(`/forum/api/forums/${forumSlug}/threads`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ title, content })
|
||
});
|
||
this.loadThread(data.thread.slug);
|
||
} catch (error) {
|
||
console.error('Error creating thread:', error);
|
||
}
|
||
}
|
||
|
||
async createPost(threadUid, content) {
|
||
try {
|
||
await this.fetchAPI(`/forum/api/threads/${threadUid}/posts`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ content })
|
||
});
|
||
// Post will be added via WebSocket
|
||
} catch (error) {
|
||
console.error('Error creating post:', error);
|
||
}
|
||
}
|
||
|
||
async toggleLike(postUid) {
|
||
try {
|
||
const data = await this.fetchAPI(`/forum/api/posts/${postUid}/like`, {
|
||
method: 'POST'
|
||
});
|
||
const postEl = this.shadowRoot.querySelector(`[data-post-uid="${postUid}"]`);
|
||
if (postEl) {
|
||
const likeBtn = postEl.querySelector('.like-button');
|
||
const likeCount = postEl.querySelector('.like-count');
|
||
likeBtn.classList.toggle('liked', data.is_liked);
|
||
likeCount.textContent = data.like_count;
|
||
likeBtn.querySelector('span').textContent = data.is_liked ? '❤️' : '🤍';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error toggling like:', error);
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const accent = "#00FFFF";
|
||
const accent_alt = "#FF6200";
|
||
const isCyan = true; // switch between cyan and orange here
|
||
|
||
// Generate subtle star background (canvas dots)
|
||
const bgSvg = encodeURIComponent(`
|
||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||
<g>
|
||
${Array.from({length: 40}).map(() => {
|
||
const x = Math.random() * 100;
|
||
const y = Math.random() * 100;
|
||
const r = Math.random() * 1.4 + 0.2;
|
||
return `<circle cx="${x}vw" cy="${y}vh" r="${r}" fill="white" opacity="0.2" />`;
|
||
}).join('')}
|
||
</g>
|
||
</svg>
|
||
`);
|
||
this.shadowRoot.innerHTML = `
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css?family=Montserrat:400,700&display=swap');
|
||
:host {
|
||
display: block;
|
||
min-height: 100vh;
|
||
font-family: 'Montserrat', 'Poppins', 'Roboto', Arial, sans-serif;
|
||
background: transparent !important;
|
||
color: #fff;
|
||
}
|
||
.container {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: transparent !important;
|
||
}
|
||
.header {
|
||
margin-bottom: 24px;
|
||
text-align: center;
|
||
}
|
||
.breadcrumb {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
font-size: 14pt;
|
||
justify-content: center;
|
||
color: ${accent};
|
||
letter-spacing: 0.1em;
|
||
}
|
||
.breadcrumb a {
|
||
color: #fff;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.breadcrumb a:hover {
|
||
text-decoration: underline;
|
||
color: ${accent_alt};
|
||
}
|
||
.breadcrumb span {
|
||
color: #A9A9A9;
|
||
font-weight: 700;
|
||
}
|
||
.content {
|
||
min-height: 120px;
|
||
background: transparent !important;
|
||
}
|
||
/* Forums List */
|
||
.forums-list {
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
.forum-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24px 18px;
|
||
cursor: pointer;
|
||
transition: background 0.12s;
|
||
background: transparent !important;
|
||
}
|
||
.forum-item:hover {
|
||
background: #23282D;
|
||
}
|
||
.forum-item:last-child { border-bottom: none; }
|
||
.forum-icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
margin-right: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #202B2E;
|
||
border-radius: 10px;
|
||
font-size: 30px;
|
||
color: ${accent};
|
||
border: 1.5px solid ${accent};
|
||
box-shadow: 0 1px 4px 0 #00FFFF22;
|
||
}
|
||
.forum-info { flex: 1; }
|
||
.forum-name {
|
||
font-size: 22pt;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
margin-bottom: 3px;
|
||
color: #fff;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
.forum-description {
|
||
font-size: 16pt;
|
||
color: #A9A9A9;
|
||
margin-bottom: 5px;
|
||
font-weight: 400;
|
||
}
|
||
.forum-stats {
|
||
font-size: 12pt;
|
||
color: #00FFFF;
|
||
opacity: 0.7;
|
||
font-weight: 500;
|
||
}
|
||
/* Threads List */
|
||
.thread-item {
|
||
display: flex;
|
||
padding: 18px 18px;
|
||
cursor: pointer;
|
||
transition: background 0.12s;
|
||
background: transparent !important;
|
||
}
|
||
.thread-item:hover {
|
||
background: #20262B;
|
||
}
|
||
.thread-item.pinned {
|
||
border-left: 5px solid ${accent};
|
||
}
|
||
.thread-info { flex: 1; }
|
||
.thread-title {
|
||
font-size: 18pt;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
text-transform: uppercase;
|
||
color: #fff;
|
||
}
|
||
.thread-meta {
|
||
font-size: 12pt;
|
||
color: #A9A9A9;
|
||
margin-top: 3px;
|
||
}
|
||
.thread-stats {
|
||
text-align: right;
|
||
font-size: 11pt;
|
||
color: #A9A9A9;
|
||
min-width: 120px;
|
||
}
|
||
.badge {
|
||
font-size: 10pt;
|
||
padding: 2px 10px;
|
||
border-radius: 4px;
|
||
background: #232323;
|
||
color: ${accent};
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
margin-left: 4px;
|
||
}
|
||
.badge.pinned {
|
||
background: ${accent};
|
||
color: #181E22;
|
||
}
|
||
.badge.locked {
|
||
background: #fff;
|
||
color: #181E22;
|
||
border: 1px solid ${accent_alt};
|
||
}
|
||
/* Posts */
|
||
.posts-list { margin: 0; }
|
||
.post {
|
||
display: flex;
|
||
padding: 24px 18px;
|
||
background: transparent !important;
|
||
}
|
||
.post-author {
|
||
width: 120px;
|
||
margin-right: 30px;
|
||
text-align: center;
|
||
}
|
||
.author-avatar {
|
||
width: 62px;
|
||
height: 62px;
|
||
border-radius: 50%;
|
||
background: #232323;
|
||
margin: 0 auto 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 27pt;
|
||
font-weight: bold;
|
||
border: 2px solid ${accent};
|
||
box-shadow: 0 2px 12px 0 #00FFFF11;
|
||
}
|
||
.author-name {
|
||
font-weight: 700;
|
||
font-size: 13pt;
|
||
color: #fff;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
.post-content { flex: 1; }
|
||
.post-header {
|
||
font-size: 12pt;
|
||
color: #A9A9A9;
|
||
margin-bottom: 10px;
|
||
}
|
||
.post-body {
|
||
line-height: 1.7;
|
||
font-size: 15pt;
|
||
color: #fff;
|
||
white-space: pre-wrap;
|
||
}
|
||
.post-footer {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-top: 18px;
|
||
padding-top: 14px;
|
||
}
|
||
.post-action {
|
||
font-size: 13pt;
|
||
color: #A9A9A9;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.03em;
|
||
transition: color 0.12s;
|
||
}
|
||
.post-action:hover, .post-action.liked {
|
||
color: ${accent};
|
||
}
|
||
.like-button.liked span:first-child {
|
||
color: ${accent_alt};
|
||
text-shadow: 0 2px 8px #FF620066;
|
||
}
|
||
/* Forms */
|
||
.form-group {
|
||
margin-bottom: 22px;
|
||
}
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 7px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
color: ${accent};
|
||
font-size: 15pt;
|
||
}
|
||
.form-group input,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 13px 16px;
|
||
font-size: 16pt;
|
||
font-family: inherit;
|
||
color: #fff;
|
||
background: #232323;
|
||
outline: none;
|
||
transition: border-color 0.16s;
|
||
}
|
||
.form-group input:focus,
|
||
.form-group textarea:focus {
|
||
border-color: ${accent_alt};
|
||
}
|
||
.form-group textarea {
|
||
min-height: 130px;
|
||
resize: vertical;
|
||
}
|
||
.button {
|
||
padding: 12px 32px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-size: 16pt;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
background: ${accent};
|
||
color: #fff;
|
||
box-shadow: 0 2px 10px #00FFFF33;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.07em;
|
||
transition: background 0.12s, color 0.12s;
|
||
}
|
||
.button:hover {
|
||
background: ${accent_alt};
|
||
color: #fff;
|
||
}
|
||
.button:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
.new-thread-button {
|
||
display: block;
|
||
margin: 0 auto 24px auto;
|
||
width: 250px;
|
||
}
|
||
.reply-form {
|
||
width: 33%;
|
||
padding: 32px 18px 18px 18px;
|
||
margin-top: 0;
|
||
}
|
||
.reply-form h3 {
|
||
text-align: center;
|
||
color: ${accent};
|
||
font-size: 22pt;
|
||
margin-bottom: 18px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
}
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.modal.show {
|
||
display: flex;
|
||
}
|
||
.modal-content {
|
||
background: #181E22;
|
||
border-radius: 14px;
|
||
padding: 36px 32px;
|
||
max-width: 520px;
|
||
width: 96vw;
|
||
max-height: 90vh;
|
||
box-shadow: 0 2px 32px 0 #000C;
|
||
color: #fff;
|
||
}
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
.modal-title {
|
||
font-size: 24pt;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
color: ${accent};
|
||
}
|
||
.modal-close {
|
||
font-size: 32px;
|
||
cursor: pointer;
|
||
background: none;
|
||
border: none;
|
||
color: #A9A9A9;
|
||
transition: color 0.13s;
|
||
}
|
||
.modal-close:hover {
|
||
color: ${accent_alt};
|
||
}
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
padding: 24px 0 10px 0;
|
||
}
|
||
.page-button {
|
||
padding: 7px 18px;
|
||
border: 1.5px solid ${accent};
|
||
background: #232323;
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
color: #fff;
|
||
font-size: 14pt;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
transition: background 0.13s, color 0.13s;
|
||
}
|
||
.page-button:hover {
|
||
background: ${accent};
|
||
color: #181E22;
|
||
}
|
||
.page-button.active {
|
||
background: ${accent};
|
||
color: #181E22;
|
||
border-color: ${accent_alt};
|
||
}
|
||
/* Feature card style for content sections */
|
||
.feature-card {
|
||
background: #1A1A1A;
|
||
border-radius: 10px;
|
||
box-shadow: 0 1px 8px #0006;
|
||
border: 1px solid #232323;
|
||
padding: 28px 22px;
|
||
margin-bottom: 20px;
|
||
color: #fff;
|
||
}
|
||
.feature-card .title {
|
||
color: ${accent_alt};
|
||
font-size: 20pt;
|
||
font-weight: 700;
|
||
margin-bottom: 9px;
|
||
text-transform: uppercase;
|
||
}
|
||
/* Minimal snake icon for branding */
|
||
.snake-icon {
|
||
display: inline-block;
|
||
width: 42px;
|
||
height: 42px;
|
||
margin-bottom: -10px;
|
||
vertical-align: middle;
|
||
}
|
||
.snake-icon path {
|
||
stroke: ${accent};
|
||
stroke-width: 3;
|
||
fill: none;
|
||
}
|
||
</style>
|
||
<div class="container">
|
||
<div class="content" id="main-content">
|
||
<!-- Content will be rendered here -->
|
||
</div>
|
||
</div>
|
||
<div class="modal" id="new-thread-modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title">NEW THREAD</h2>
|
||
<button class="modal-close" type="button">×</button>
|
||
</div>
|
||
<form id="new-thread-form">
|
||
<div class="form-group">
|
||
<label>Title</label>
|
||
<input type="text" name="title" required minlength="5" maxlength="200" placeholder="Thread Title">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Content</label>
|
||
<textarea name="content" required minlength="1" placeholder="Write your post..."></textarea>
|
||
</div>
|
||
<button type="submit" class="button">CREATE THREAD</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Breadcrumb render
|
||
this.updateBreadcrumb();
|
||
|
||
// Add event listeners
|
||
this.shadowRoot.addEventListener('click', (e) => {
|
||
// Breadcrumb navigation
|
||
if (e.target.closest('.breadcrumb a')) {
|
||
e.preventDefault();
|
||
const node = e.target.closest('.breadcrumb a');
|
||
const action = node.dataset.action;
|
||
if (action === "forums") {
|
||
this.loadForums();
|
||
} else if (action === "forum") {
|
||
this.loadForum(node.dataset.slug);
|
||
}
|
||
}
|
||
// Forum item
|
||
else if (e.target.closest('[data-forum-slug]')) {
|
||
this.loadForum(e.target.closest('[data-forum-slug]').dataset.forumSlug);
|
||
}
|
||
// Thread item
|
||
else if (e.target.closest('[data-thread-slug]')) {
|
||
this.loadThread(e.target.closest('[data-thread-slug]').dataset.threadSlug);
|
||
}
|
||
// Like button
|
||
else if (e.target.closest('.like-button')) {
|
||
this.toggleLike(e.target.closest('[data-post-uid]').dataset.postUid);
|
||
}
|
||
// New thread button
|
||
else if (e.target.closest('.new-thread-button')) {
|
||
this.shadowRoot.getElementById('new-thread-modal').classList.add('show');
|
||
}
|
||
// Modal close
|
||
else if (e.target.closest('.modal-close')) {
|
||
e.target.closest('.modal').classList.remove('show');
|
||
}
|
||
// Pagination buttons
|
||
else if (e.target.matches('.page-button[data-page]')) {
|
||
const page = parseInt(e.target.dataset.page, 10);
|
||
if (this.currentView === "forum") {
|
||
this.loadForum(this.currentForum.slug, page);
|
||
} else if (this.currentView === "thread") {
|
||
this.loadThread(this.currentThread.slug, page);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Form submissions
|
||
this.shadowRoot.getElementById('new-thread-form').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const formData = new FormData(e.target);
|
||
this.createThread(this.currentForum.slug, formData.get('title'), formData.get('content'));
|
||
e.target.reset();
|
||
this.shadowRoot.getElementById('new-thread-modal').classList.remove('show');
|
||
});
|
||
}
|
||
|
||
updateBreadcrumb() {
|
||
return;
|
||
const crumb = [];
|
||
crumb.push(`<a style="display: none;" href="#" data-action="forums">FORUMS</a>`);
|
||
if (this.currentView === "forum" && this.currentForum) {
|
||
crumb.push(`<span>›</span>`);
|
||
crumb.push(`<span>${this.currentForum.name.toUpperCase()}</span>`);
|
||
}
|
||
if (this.currentView === "thread" && this.currentThread && this.currentForum) {
|
||
crumb.push(`<span>›</span>`);
|
||
crumb.push(`<a href="#" data-action="forum" data-slug="${this.currentForum.slug}">${this.currentForum.name.toUpperCase()}</a>`);
|
||
crumb.push(`<span>›</span>`);
|
||
crumb.push(`<span>${this.currentThread.title.toUpperCase()}</span>`);
|
||
}
|
||
this.shadowRoot.getElementById('breadcrumb').innerHTML = crumb.join(' ');
|
||
}
|
||
|
||
renderForums(forums) {
|
||
const content = this.shadowRoot.getElementById('main-content');
|
||
content.innerHTML = `
|
||
<div class="forums-list">
|
||
${forums.map(forum => `
|
||
<div class="forum-item" data-forum-slug="${forum.slug}">
|
||
<div class="forum-icon">${forum.icon || '📁'}</div>
|
||
<div class="forum-info">
|
||
<div class="forum-name">${forum.name}</div>
|
||
${forum.description ? `<div class="forum-description">${forum.description}</div>` : ''}
|
||
<div class="forum-stats">
|
||
${forum.thread_count} threads · ${forum.post_count} posts
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderForum(data) {
|
||
const { forum, threads } = data;
|
||
const content = this.shadowRoot.getElementById('main-content');
|
||
content.innerHTML = `
|
||
<div style="padding: 18px;">
|
||
<button class="button new-thread-button">NEW THREAD</button>
|
||
<div style="clear: both;"></div>
|
||
<div class="threads-list">
|
||
${threads.map(thread => `
|
||
<div class="thread-item ${thread.is_pinned ? 'pinned' : ''}" data-thread-slug="${thread.slug}">
|
||
<div class="thread-info">
|
||
<div class="thread-title">
|
||
${thread.title}
|
||
${thread.is_pinned ? '<span class="badge pinned">PINNED</span>' : ''}
|
||
${thread.is_locked ? '<span class="badge locked">LOCKED</span>' : ''}
|
||
</div>
|
||
<div class="thread-meta">
|
||
Started by <span style="color:#00FFFF;font-weight:700">${thread.author.nick}</span>
|
||
· ${this.formatDate(thread.created_at)}
|
||
</div>
|
||
</div>
|
||
<div class="thread-stats">
|
||
<div>${thread.post_count} replies</div>
|
||
<div>${thread.view_count} views</div>
|
||
${thread.last_post_author ? `
|
||
<div style="margin-top: 7px; font-size: 12pt;">
|
||
Last: <span style="color:#FF6200;">${thread.last_post_author.nick}</span><br>
|
||
${this.formatDate(thread.last_post_at)}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
${data.hasMore ? `
|
||
<div class="pagination">
|
||
<button class="page-button" data-page="${data.page - 1}" ${data.page === 1 ? 'disabled' : ''}>Previous</button>
|
||
<span class="page-button active">${data.page}</span>
|
||
<button class="page-button" data-page="${data.page + 1}">Next</button>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
renderThread(data) {
|
||
const { thread, forum, posts } = data;
|
||
const content = this.shadowRoot.getElementById('main-content');
|
||
content.innerHTML = `
|
||
<div class="posts-list">
|
||
${posts.map(post => `
|
||
<div class="post" data-post-uid="${post.uid}">
|
||
<div class="post-author">
|
||
<div class="author-avatar" style="color: ${post.author.color}">
|
||
${post.author.nick.charAt(0).toUpperCase()}
|
||
</div>
|
||
<div class="author-name">${post.author.nick}</div>
|
||
</div>
|
||
<div class="post-content">
|
||
<div class="post-header">
|
||
Posted ${this.formatDate(post.created_at)}
|
||
${post.edited_at ? `· Edited ${this.formatDate(post.edited_at)}` : ''}
|
||
</div>
|
||
<div class="post-body">${this.escapeHtml(post.content)}</div>
|
||
<div class="post-footer">
|
||
<span class="post-action like-button ${post.is_liked ? 'liked' : ''}">
|
||
<span>${post.is_liked ? '❤️' : '🤍'}</span>
|
||
<span class="like-count">${post.like_count}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
${!thread.is_locked ? `
|
||
<div class="reply-form">
|
||
<h3>Reply</h3>
|
||
<form id="reply-form">
|
||
<div class="form-group">
|
||
<textarea name="content" placeholder="Write your reply..." required></textarea>
|
||
</div>
|
||
<button type="submit" class="button">POST REPLY</button>
|
||
</form>
|
||
</div>
|
||
` : `
|
||
<div class="reply-form">
|
||
<p style="text-align: center; color: #A9A9A9; font-size: 18pt;">This thread is locked.</p>
|
||
</div>
|
||
`}
|
||
${data.hasMore ? `
|
||
<div class="pagination">
|
||
<button class="page-button" data-page="${data.page - 1}" ${data.page === 1 ? 'disabled' : ''}>Previous</button>
|
||
<span class="page-button active">${data.page}</span>
|
||
<button class="page-button" data-page="${data.page + 1}">Next</button>
|
||
</div>
|
||
` : ''}
|
||
`;
|
||
const replyForm = this.shadowRoot.getElementById('reply-form');
|
||
if (replyForm) {
|
||
replyForm.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const formData = new FormData(e.target);
|
||
this.createPost(thread.uid, formData.get('content'));
|
||
e.target.reset();
|
||
});
|
||
}
|
||
}
|
||
|
||
addNewPost(post) {
|
||
const postsList = this.shadowRoot.querySelector('.posts-list');
|
||
if (!postsList) return;
|
||
const postHtml = `
|
||
<div class="post" data-post-uid="${post.uid}">
|
||
<div class="post-author">
|
||
<div class="author-avatar" style="color: ${post.author.color}">
|
||
${post.author.nick.charAt(0).toUpperCase()}
|
||
</div>
|
||
<div class="author-name">${post.author.nick}</div>
|
||
</div>
|
||
<div class="post-content">
|
||
<div class="post-header">
|
||
Posted ${this.formatDate(post.created_at)}
|
||
</div>
|
||
<div class="post-body">${this.escapeHtml(post.content)}</div>
|
||
<div class="post-footer">
|
||
<span class="post-action like-button">
|
||
<span>🤍</span>
|
||
<span class="like-count">0</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
postsList.insertAdjacentHTML('beforeend', postHtml);
|
||
}
|
||
|
||
updatePost(post) {
|
||
const postEl = this.shadowRoot.querySelector(`[data-post-uid="${post.uid}"]`);
|
||
if (postEl) {
|
||
const bodyEl = postEl.querySelector('.post-body');
|
||
const headerEl = postEl.querySelector('.post-header');
|
||
if (bodyEl) bodyEl.textContent = post.content;
|
||
if (headerEl && post.edited_at) {
|
||
headerEl.innerHTML += ` · Edited ${this.formatDate(post.edited_at)}`;
|
||
}
|
||
}
|
||
}
|
||
|
||
removePost(postUid) {
|
||
const postEl = this.shadowRoot.querySelector(`[data-post-uid="${postUid}"]`);
|
||
if (postEl) {
|
||
postEl.remove();
|
||
}
|
||
}
|
||
|
||
formatDate(dateStr) {
|
||
const date = new Date(dateStr);
|
||
const now = new Date();
|
||
const diff = now - date;
|
||
if (diff < 60000) return 'just now';
|
||
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`;
|
||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours ago`;
|
||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} days ago`;
|
||
return date.toLocaleDateString();
|
||
}
|
||
|
||
escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
}
|
||
customElements.define('snek-forum', SnekForum);
|
||
</script>
|
||
<snek-forum></snek-forum>
|
||
{% endblock %}
|
||
|