|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>OnFiRe</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #dae0e6;
|
|
color: #1c1c1c;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header {
|
|
background: #ff4500;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.header-nav {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-nav button {
|
|
background: white;
|
|
color: #ff4500;
|
|
border: none;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.header-nav button:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.main-content {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 250px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-card {
|
|
background: white;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.sidebar-card h3 {
|
|
font-size: 14px;
|
|
margin-bottom: 10px;
|
|
text-transform: uppercase;
|
|
color: #7c7c7c;
|
|
}
|
|
|
|
.sidebar-list {
|
|
list-style: none;
|
|
}
|
|
|
|
.sidebar-list li {
|
|
padding: 5px 0;
|
|
cursor: pointer;
|
|
color: #0079d3;
|
|
}
|
|
|
|
.sidebar-list li:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.post-card {
|
|
background: white;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
}
|
|
|
|
.vote-section {
|
|
width: 40px;
|
|
background: #f8f9fa;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
border-right: 1px solid #ccc;
|
|
}
|
|
|
|
.vote-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 18px;
|
|
color: #878a8c;
|
|
padding: 2px;
|
|
}
|
|
|
|
.vote-btn:hover {
|
|
color: #ff4500;
|
|
}
|
|
|
|
.vote-btn.upvoted {
|
|
color: #ff4500;
|
|
}
|
|
|
|
.vote-btn.downvoted {
|
|
color: #7193ff;
|
|
}
|
|
|
|
.vote-score {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #1c1c1c;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.post-content {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.post-meta {
|
|
font-size: 12px;
|
|
color: #7c7c7c;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.post-meta a {
|
|
color: #0079d3;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.post-meta a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.post-title {
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
margin-bottom: 5px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.post-title:hover {
|
|
color: #0079d3;
|
|
}
|
|
|
|
.post-body {
|
|
font-size: 14px;
|
|
margin-bottom: 10px;
|
|
color: #1c1c1c;
|
|
}
|
|
|
|
.post-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.post-action {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: #878a8c;
|
|
font-weight: 700;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.post-action:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 200;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 4px;
|
|
padding: 20px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal-header {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #edeff1;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-group textarea {
|
|
resize: vertical;
|
|
min-height: 100px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #0079d3;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #0060a0;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #f0f0f0;
|
|
color: #1c1c1c;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #e0e0e0;
|
|
}
|
|
|
|
.sort-tabs {
|
|
display: flex;
|
|
gap: 5px;
|
|
margin-bottom: 15px;
|
|
background: white;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.sort-tab, .post-sort-tab, .comment-sort-tab {
|
|
background: none;
|
|
border: none;
|
|
padding: 6px 12px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
color: #878a8c;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.sort-tab:hover, .post-sort-tab:hover, .comment-sort-tab:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.sort-tab.active, .post-sort-tab.active, .comment-sort-tab.active {
|
|
background: #0079d3;
|
|
color: white;
|
|
}
|
|
|
|
.comment {
|
|
border-left: 2px solid #edeff1;
|
|
padding-left: 10px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.comment-header {
|
|
font-size: 12px;
|
|
color: #7c7c7c;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.comment-header strong {
|
|
color: #1c1c1c;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.comment-body {
|
|
font-size: 14px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.comment-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.comment-reply {
|
|
margin-left: 20px;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.search-bar {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.search-bar input {
|
|
flex: 1;
|
|
padding: 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 2px 6px;
|
|
background: #0079d3;
|
|
color: white;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.badge-nsfw {
|
|
background: #ff4500;
|
|
}
|
|
|
|
.badge-spoiler {
|
|
background: #333;
|
|
}
|
|
|
|
.badge-sticky {
|
|
background: #46d160;
|
|
}
|
|
|
|
.badge-locked {
|
|
background: #ffd635;
|
|
color: #1c1c1c;
|
|
}
|
|
|
|
.user-karma {
|
|
font-size: 12px;
|
|
color: #7c7c7c;
|
|
}
|
|
|
|
.message-item {
|
|
background: white;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.message-header {
|
|
font-size: 12px;
|
|
color: #7c7c7c;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.message-subject {
|
|
font-weight: 600;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.message-body {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.time-filters {
|
|
display: flex;
|
|
gap: 5px;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.time-filter {
|
|
background: none;
|
|
border: 1px solid #ccc;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.time-filter.active {
|
|
background: #0079d3;
|
|
color: white;
|
|
border-color: #0079d3;
|
|
}
|
|
|
|
.flair-tag {
|
|
display: inline-block;
|
|
padding: 2px 6px;
|
|
background: #edeff1;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
margin-right: 5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
class App {
|
|
constructor() {
|
|
this.currentUser = null;
|
|
this.sessionToken = null;
|
|
this.currentView = 'home';
|
|
this.currentSort = 'hot';
|
|
this.currentTimeFilter = 'all';
|
|
this.currentCommunity = null;
|
|
this.currentPost = null;
|
|
this.currentCommentSort = 'best';
|
|
window.addEventListener('popstate', (event) => {
|
|
this.handlePopState(event);
|
|
});
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
this.init();
|
|
});
|
|
}
|
|
|
|
async init() {
|
|
try {
|
|
this.sessionToken = localStorage.getItem('sessionToken');
|
|
} catch (e) {
|
|
console.error('localStorage error:', e);
|
|
this.sessionToken = null;
|
|
}
|
|
|
|
if (this.sessionToken) {
|
|
const result = await this.api('/me');
|
|
if (result.success) {
|
|
this.currentUser = result.user;
|
|
this.updateUI();
|
|
} else {
|
|
this.sessionToken = null;
|
|
try {
|
|
localStorage.removeItem('sessionToken');
|
|
} catch (e) {
|
|
console.error('localStorage error:', e);
|
|
}
|
|
}
|
|
}
|
|
this.updateUI();
|
|
await this.loadCommunities();
|
|
this.handleRoute(window.location.search);
|
|
|
|
// Add Escape key listener for modal
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
this.closeModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
async handleRoute(search) {
|
|
const params = new URLSearchParams(search);
|
|
|
|
if (params.has('post')) {
|
|
const postId = params.get('post');
|
|
await this.showPost(Number(postId), false);
|
|
} else if (params.has('community')) {
|
|
const communityId = params.get('community');
|
|
await this.showCommunity(Number(communityId), false);
|
|
} else if (params.has('user')) {
|
|
const username = params.get('user');
|
|
await this.showProfile(username, false);
|
|
} else if (params.has('messages')) {
|
|
await this.showMessages(false);
|
|
} else {
|
|
this.showHome(false);
|
|
}
|
|
}
|
|
|
|
updateURL(params, replace = false) {
|
|
const baseUrl = window.location.origin + window.location.pathname;
|
|
const urlParams = new URLSearchParams();
|
|
|
|
if (params.view) {
|
|
if (params.view === 'post' && params.postId) {
|
|
urlParams.set('post', params.postId);
|
|
} else if (params.view === 'community' && params.communityId) {
|
|
urlParams.set('community', params.communityId);
|
|
} else if (params.view === 'profile' && params.username) {
|
|
urlParams.set('user', params.username);
|
|
} else if (params.view === 'messages') {
|
|
urlParams.set('messages', '1');
|
|
}
|
|
}
|
|
|
|
const newUrl = urlParams.toString() ? `${baseUrl}?${urlParams.toString()}` : baseUrl;
|
|
|
|
if (replace) {
|
|
window.history.replaceState(params, '', newUrl);
|
|
} else {
|
|
window.history.pushState(params, '', newUrl);
|
|
}
|
|
}
|
|
|
|
handlePopState(event) {
|
|
if (!event.state) {
|
|
this.showHome(true);
|
|
return;
|
|
}
|
|
const state = event.state;
|
|
switch (state.view) {
|
|
case 'post':
|
|
this.showPost(state.postId, true);
|
|
break;
|
|
case 'community':
|
|
this.showCommunity(state.communityId, true);
|
|
break;
|
|
case 'profile':
|
|
this.showProfile(state.username, true);
|
|
break;
|
|
case 'messages':
|
|
this.showMessages(true);
|
|
break;
|
|
default:
|
|
this.showHome(true);
|
|
}
|
|
}
|
|
|
|
updateUI() {
|
|
if (this.currentUser) {
|
|
document.getElementById('user-info').classList.remove('hidden');
|
|
document.getElementById('user-info').innerHTML = `<span onclick="document.app.showProfile('${this.currentUser.username}')" style="cursor:pointer;color:white;">${this.currentUser.username}</span> <span class="user-karma">${this.currentUser.karma} karma</span>`;
|
|
document.getElementById('login-btn').classList.add('hidden');
|
|
document.getElementById('register-btn').classList.add('hidden');
|
|
document.getElementById('logout-btn').classList.remove('hidden');
|
|
document.getElementById('create-community-btn').classList.remove('hidden');
|
|
document.getElementById('create-post-btn').classList.remove('hidden');
|
|
document.getElementById('messages-btn').classList.remove('hidden');
|
|
document.getElementById('subscriptions-card').classList.remove('hidden');
|
|
this.loadSubscriptions();
|
|
} else {
|
|
document.getElementById('user-info').classList.add('hidden');
|
|
document.getElementById('login-btn').classList.remove('hidden');
|
|
document.getElementById('register-btn').classList.remove('hidden');
|
|
document.getElementById('logout-btn').classList.add('hidden');
|
|
document.getElementById('create-community-btn').classList.add('hidden');
|
|
document.getElementById('create-post-btn').classList.add('hidden');
|
|
document.getElementById('messages-btn').classList.add('hidden');
|
|
document.getElementById('subscriptions-card').classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
async api(endpoint, data = null) {
|
|
const options = {
|
|
method: data ? 'POST' : 'GET',
|
|
headers: {'Content-Type': 'application/json'}
|
|
};
|
|
if (this.sessionToken) {
|
|
options.headers['X-Session-Token'] = this.sessionToken;
|
|
}
|
|
if (data) {
|
|
options.body = JSON.stringify(data);
|
|
}
|
|
try {
|
|
const protocol = window.location.protocol === 'https:' ? 'https' : 'http';
|
|
const response = await fetch(`${protocol}://${window.location.host}/api${endpoint}`, options);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('API error:', error);
|
|
return {success: false, error: 'Network error. Please try again.'};
|
|
}
|
|
}
|
|
|
|
showLogin() {
|
|
document.getElementById('modal-title').textContent = 'Login';
|
|
document.getElementById('modal-body').innerHTML = `
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input type="text" id="login-username">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" id="login-password">
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn btn-secondary" onclick="document.app.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="document.app.login()">Login</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('modal').classList.add('active');
|
|
}
|
|
|
|
async login() {
|
|
const username = document.getElementById('login-username').value.trim();
|
|
const password = document.getElementById('login-password').value;
|
|
|
|
if (!username || !password) {
|
|
alert('Please enter both username and password');
|
|
return;
|
|
}
|
|
|
|
const result = await this.api('/login', {username, password});
|
|
|
|
if (result.success) {
|
|
this.sessionToken = result.token;
|
|
try {
|
|
localStorage.setItem('sessionToken', result.token);
|
|
} catch (e) {
|
|
console.error('localStorage error:', e);
|
|
}
|
|
this.currentUser = {
|
|
id: result.user_id,
|
|
username: result.username,
|
|
karma: result.karma
|
|
};
|
|
this.updateUI();
|
|
this.closeModal();
|
|
this.showHome();
|
|
} else {
|
|
alert(result.error || 'Login failed');
|
|
}
|
|
}
|
|
|
|
showRegister() {
|
|
document.getElementById('modal-title').textContent = 'Register';
|
|
document.getElementById('modal-body').innerHTML = `
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input type="text" id="register-username">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" id="register-password">
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn btn-secondary" onclick="document.app.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="document.app.register()">Register</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('modal').classList.add('active');
|
|
}
|
|
|
|
async register() {
|
|
const username = document.getElementById('register-username').value.trim();
|
|
const password = document.getElementById('register-password').value;
|
|
|
|
if (!username || !password) {
|
|
alert('Please enter both username and password');
|
|
return;
|
|
}
|
|
|
|
if (username.length < 3) {
|
|
alert('Username must be at least 3 characters');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
alert('Password must be at least 6 characters');
|
|
return;
|
|
}
|
|
|
|
const result = await this.api('/register', {username, password});
|
|
|
|
if (result.success) {
|
|
// Auto-login after successful registration
|
|
this.sessionToken = result.token;
|
|
try {
|
|
localStorage.setItem('sessionToken', result.token);
|
|
} catch (e) {
|
|
console.error('localStorage error:', e);
|
|
}
|
|
this.currentUser = {
|
|
username: result.username,
|
|
karma: result.karma
|
|
};
|
|
this.updateUI();
|
|
this.closeModal();
|
|
this.showHome();
|
|
} else {
|
|
alert(result.error || 'Registration failed');
|
|
}
|
|
}
|
|
|
|
async logout() {
|
|
await this.api('/logout');
|
|
this.currentUser = null;
|
|
this.sessionToken = null;
|
|
try {
|
|
localStorage.removeItem('sessionToken');
|
|
} catch (e) {
|
|
console.error('localStorage error:', e);
|
|
}
|
|
this.updateUI();
|
|
this.showHome();
|
|
}
|
|
|
|
async loadCommunities() {
|
|
const result = await this.api('/communities');
|
|
const list = document.getElementById('communities-list');
|
|
list.innerHTML = '';
|
|
|
|
result.communities.forEach(community => {
|
|
const li = document.createElement('li');
|
|
li.textContent = `${community.name} (${community.subscribers})`;
|
|
li.onclick = () => this.showCommunity(community.id);
|
|
list.appendChild(li);
|
|
});
|
|
}
|
|
|
|
async loadSubscriptions() {
|
|
if (!this.currentUser) return;
|
|
|
|
const result = await this.api(`/subscriptions`);
|
|
const list = document.getElementById('subscriptions-list');
|
|
list.innerHTML = '';
|
|
|
|
result.subscriptions.forEach(sub => {
|
|
const li = document.createElement('li');
|
|
li.textContent = sub.name;
|
|
li.onclick = () => this.showCommunity(sub.id);
|
|
list.appendChild(li);
|
|
});
|
|
}
|
|
|
|
showCreateCommunity() {
|
|
if (!this.currentUser) {
|
|
alert('Please login first');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('modal-title').textContent = 'Create Community';
|
|
document.getElementById('modal-body').innerHTML = `
|
|
<div class="form-group">
|
|
<label>Name</label>
|
|
<input type="text" id="community-name">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description</label>
|
|
<textarea id="community-description"></textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn btn-secondary" onclick="document.app.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="document.app.createCommunity()">Create</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('modal').classList.add('active');
|
|
}
|
|
|
|
async createCommunity() {
|
|
const name = document.getElementById('community-name').value.trim();
|
|
const description = document.getElementById('community-description').value.trim();
|
|
|
|
if (!name) {
|
|
alert('Please enter a community name');
|
|
return;
|
|
}
|
|
|
|
if (name.length < 3) {
|
|
alert('Community name must be at least 3 characters');
|
|
return;
|
|
}
|
|
|
|
const result = await this.api('/create_community', {
|
|
name,
|
|
description
|
|
});
|
|
|
|
if (result.success) {
|
|
this.closeModal();
|
|
this.loadCommunities();
|
|
this.showCommunity(result.community_id);
|
|
} else {
|
|
alert(result.error || 'Failed to create community');
|
|
}
|
|
}
|
|
|
|
showCreatePost() {
|
|
if (!this.currentUser) {
|
|
alert('Please login first');
|
|
return;
|
|
}
|
|
|
|
this.api('/communities').then(result => {
|
|
let options = result.communities.map(c =>
|
|
`<option value="${c.id}">${c.name}</option>`
|
|
).join('');
|
|
|
|
document.getElementById('modal-title').textContent = 'Create Post';
|
|
document.getElementById('modal-body').innerHTML = `
|
|
<div class="form-group">
|
|
<label>Community</label>
|
|
<select id="post-community">${options}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Title</label>
|
|
<input type="text" id="post-title">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Type</label>
|
|
<select id="post-type" onchange="document.app.togglePostType()">
|
|
<option value="text">Text</option>
|
|
<option value="link">Link</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group" id="post-content-group">
|
|
<label>Content</label>
|
|
<textarea id="post-content"></textarea>
|
|
</div>
|
|
<div class="form-group hidden" id="post-url-group">
|
|
<label>URL</label>
|
|
<input type="text" id="post-url">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Flair (optional)</label>
|
|
<input type="text" id="post-flair">
|
|
</div>
|
|
<div class="form-group">
|
|
<label><input type="checkbox" id="post-nsfw"> NSFW</label>
|
|
<label><input type="checkbox" id="post-spoiler"> Spoiler</label>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn btn-secondary" onclick="document.app.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="document.app.createPost()">Post</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('modal').classList.add('active');
|
|
});
|
|
}
|
|
|
|
togglePostType() {
|
|
const type = document.getElementById('post-type').value;
|
|
if (type === 'text') {
|
|
document.getElementById('post-content-group').classList.remove('hidden');
|
|
document.getElementById('post-url-group').classList.add('hidden');
|
|
} else {
|
|
document.getElementById('post-content-group').classList.add('hidden');
|
|
document.getElementById('post-url-group').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
async createPost() {
|
|
const community_id = document.getElementById('post-community').value;
|
|
const title = document.getElementById('post-title').value.trim();
|
|
const type = document.getElementById('post-type').value;
|
|
const content = type === 'text' ? document.getElementById('post-content').value.trim() : '';
|
|
const url = type === 'link' ? document.getElementById('post-url').value.trim() : '';
|
|
const flair = document.getElementById('post-flair').value.trim();
|
|
const nsfw = document.getElementById('post-nsfw').checked ? 1 : 0;
|
|
const spoiler = document.getElementById('post-spoiler').checked ? 1 : 0;
|
|
|
|
if (!title) {
|
|
alert('Please enter a title');
|
|
return;
|
|
}
|
|
|
|
if (title.length < 3) {
|
|
alert('Title must be at least 3 characters');
|
|
return;
|
|
}
|
|
|
|
if (type === 'link' && !url) {
|
|
alert('Please enter a URL');
|
|
return;
|
|
}
|
|
|
|
const result = await this.api('/create_post', {
|
|
community_id,
|
|
title,
|
|
content,
|
|
url,
|
|
type,
|
|
flair,
|
|
nsfw,
|
|
spoiler
|
|
});
|
|
|
|
if (result.success) {
|
|
this.closeModal();
|
|
this.showPost(result.post_id);
|
|
} else {
|
|
alert(result.error || 'Failed to create post');
|
|
}
|
|
}
|
|
|
|
async loadFeed(append) {
|
|
let endpoint = `/posts?sort=${this.currentSort}&time=${this.currentTimeFilter}`;
|
|
|
|
if (this.currentCommunity) {
|
|
endpoint += `&community_id=${this.currentCommunity}`;
|
|
}
|
|
|
|
if (this.currentUser && this.currentView === 'home') {
|
|
endpoint += '&feed=home';
|
|
}
|
|
|
|
const result = await this.api(endpoint);
|
|
this.renderPosts(result.posts, append);
|
|
}
|
|
|
|
renderPosts(posts, append) {
|
|
const feed = document.getElementById('feed-content');
|
|
if (!append) {
|
|
feed.innerHTML = '';
|
|
}
|
|
posts.forEach(post => {
|
|
const postEl = document.createElement('div');
|
|
postEl.className = 'post-card';
|
|
|
|
let badges = '';
|
|
if (post.sticky) badges += '<span class="badge badge-sticky">STICKY</span>';
|
|
if (post.nsfw) badges += '<span class="badge badge-nsfw">NSFW</span>';
|
|
if (post.spoiler) badges += '<span class="badge badge-spoiler">SPOILER</span>';
|
|
if (post.locked) badges += '<span class="badge badge-locked">LOCKED</span>';
|
|
if (post.flair) badges += `<span class="flair-tag">${post.flair}</span>`;
|
|
|
|
postEl.innerHTML = `
|
|
<div class="vote-section">
|
|
<button class="vote-btn ${post.user_vote === 1 ? 'upvoted' : ''}" onclick="document.app.vote('post', ${post.id}, 1)">â–²</button>
|
|
<div class="vote-score">${post.score}</div>
|
|
<button class="vote-btn ${post.user_vote === -1 ? 'downvoted' : ''}" onclick="document.app.vote('post', ${post.id}, -1)">â–¼</button>
|
|
</div>
|
|
<div class="post-content">
|
|
<div class="post-meta">
|
|
<a onclick="document.app.showCommunity(${post.community_id})">${post.community_name}</a> •
|
|
Posted by <a onclick="document.app.showProfile('${post.username}')">${post.username}</a> •
|
|
${this.formatTime(post.created_at)}
|
|
${badges}
|
|
</div>
|
|
<div class="post-title" onclick="document.app.showPost(${post.id})">${post.title}</div>
|
|
${post.content ? `<div class="post-body">${this.renderMarkdown(post.content.substring(0, 300))}${post.content.length > 300 ? '...' : ''}</div>` : ''}
|
|
${post.url ? `<div class="post-body"><a href="${post.url}" target="_blank">${post.url}</a></div>` : ''}
|
|
<div class="post-actions">
|
|
<button class="post-action" onclick="document.app.showPost(${post.id})">💬 ${post.comment_count} Comments</button>
|
|
<button class="post-action" onclick="document.app.savePost(${post.id})">Save</button>
|
|
<button class="post-action" onclick="document.app.hidePost(${post.id})">Hide</button>
|
|
<button class="post-action" onclick="document.app.sharePost(${post.id})">Share</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
feed.appendChild(postEl);
|
|
});
|
|
}
|
|
|
|
async showPost(postId, fromPopState = false) {
|
|
const postResult = await this.api(`/post?id=${postId}`);
|
|
const commentsResult = await this.api(`/comments?post_id=${postId}&sort=${this.currentCommentSort}`);
|
|
|
|
if (!postResult.success) {
|
|
alert('Post not found');
|
|
return;
|
|
}
|
|
|
|
this.currentPost = postResult.post;
|
|
this.currentView = 'post';
|
|
this.currentCommunity = null;
|
|
this.currentSort = 'hot';
|
|
this.currentTimeFilter = 'all';
|
|
|
|
if (!fromPopState) {
|
|
this.updateURL({view: 'post', postId});
|
|
}
|
|
|
|
const feed = document.getElementById('feed-content');
|
|
feed.innerHTML = '';
|
|
|
|
const post = postResult.post;
|
|
|
|
let badges = '';
|
|
if (post.sticky) badges += '<span class="badge badge-sticky">STICKY</span>';
|
|
if (post.nsfw) badges += '<span class="badge badge-nsfw">NSFW</span>';
|
|
if (post.spoiler) badges += '<span class="badge badge-spoiler">SPOILER</span>';
|
|
if (post.locked) badges += '<span class="badge badge-locked">LOCKED</span>';
|
|
if (post.flair) badges += `<span class="flair-tag">${post.flair}</span>`;
|
|
|
|
const postEl = document.createElement('div');
|
|
postEl.className = 'post-card';
|
|
postEl.innerHTML = `
|
|
<div class="vote-section">
|
|
<button class="vote-btn ${post.user_vote === 1 ? 'upvoted' : ''}" onclick="document.app.vote('post', ${post.id}, 1)">â–²</button>
|
|
<div class="vote-score">${post.score}</div>
|
|
<button class="vote-btn ${post.user_vote === -1 ? 'downvoted' : ''}" onclick="document.app.vote('post', ${post.id}, -1)">â–¼</button>
|
|
</div>
|
|
<div class="post-content">
|
|
<div class="post-meta">
|
|
<a onclick="document.app.showCommunity(${post.community_id})">${post.community_name}</a> •
|
|
Posted by <a onclick="document.app.showProfile('${post.username}')">${post.username}</a> •
|
|
${this.formatTime(post.created_at)}
|
|
${badges}
|
|
</div>
|
|
<div class="post-title">${post.title}</div>
|
|
${post.content ? `<div class="post-body">${this.renderMarkdown(post.content)}</div>` : ''}
|
|
${post.url ? `<div class="post-body"><a href="${post.url}" target="_blank">${post.url}</a></div>` : ''}
|
|
<div class="post-actions">
|
|
<button class="post-action" onclick="document.app.showCommentForm(${post.id}, null)">💬 Add Comment</button>
|
|
<button class="post-action" onclick="document.app.savePost(${post.id})">Save</button>
|
|
<button class="post-action" onclick="document.app.hidePost(${post.id})">Hide</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
feed.appendChild(postEl);
|
|
|
|
const sortTabs = document.createElement('div');
|
|
sortTabs.className = 'sort-tabs';
|
|
sortTabs.innerHTML = `
|
|
<button class="comment-sort-tab ${this.currentCommentSort === 'best' ? 'active' : ''}" data-sort="best">Best</button>
|
|
<button class="comment-sort-tab ${this.currentCommentSort === 'top' ? 'active' : ''}" data-sort="top">Top</button>
|
|
<button class="comment-sort-tab ${this.currentCommentSort === 'new' ? 'active' : ''}" data-sort="new">New</button>
|
|
<button class="comment-sort-tab ${this.currentCommentSort === 'old' ? 'active' : ''}" data-sort="old">Old</button>
|
|
<button class="comment-sort-tab ${this.currentCommentSort === 'controversial' ? 'active' : ''}" data-sort="controversial">Controversial</button>
|
|
`;
|
|
sortTabs.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('comment-sort-tab')) {
|
|
const sort = e.target.dataset.sort;
|
|
this.changeCommentSort(postId, sort);
|
|
}
|
|
});
|
|
feed.appendChild(sortTabs);
|
|
|
|
const commentsContainer = document.createElement('div');
|
|
commentsContainer.id = 'comments-container';
|
|
this.renderComments(commentsResult.comments, commentsContainer);
|
|
feed.appendChild(commentsContainer);
|
|
}
|
|
|
|
renderComments(comments, container, parentId = null, level = 0) {
|
|
const filteredComments = comments.filter(c => c.parent_id === parentId);
|
|
|
|
filteredComments.forEach(comment => {
|
|
const commentEl = document.createElement('div');
|
|
commentEl.className = level > 0 ? 'comment comment-reply' : 'comment';
|
|
commentEl.innerHTML = `
|
|
<div class="comment-header">
|
|
<strong onclick="document.app.showProfile('${comment.username}')">${comment.username}</strong> •
|
|
${comment.score} points •
|
|
${this.formatTime(comment.created_at)}
|
|
</div>
|
|
<div class="comment-body">${this.renderMarkdown(comment.content)}</div>
|
|
<div class="comment-actions">
|
|
<button class="post-action ${comment.user_vote === 1 ? 'upvoted' : ''}" onclick="document.app.vote('comment', ${comment.id}, 1)">â–²</button>
|
|
<button class="post-action ${comment.user_vote === -1 ? 'downvoted' : ''}" onclick="document.app.vote('comment', ${comment.id}, -1)">â–¼</button>
|
|
<button class="post-action" onclick="document.app.showCommentForm(${comment.post_id}, ${comment.id})">Reply</button>
|
|
<button class="post-action" onclick="document.app.saveComment(${comment.id})">Save</button>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(commentEl);
|
|
|
|
const children = comments.filter(c => c.parent_id === comment.id);
|
|
if (children.length > 0) {
|
|
const childContainer = document.createElement('div');
|
|
commentEl.appendChild(childContainer);
|
|
this.renderComments(comments, childContainer, comment.id, level + 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
showCommentForm(postId, parentId) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to comment');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('modal-title').textContent = parentId ? 'Reply to Comment' : 'Add Comment';
|
|
document.getElementById('modal-body').innerHTML = `
|
|
<div class="form-group">
|
|
<label>Comment</label>
|
|
<textarea id="comment-content"></textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn btn-secondary" onclick="document.app.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="document.app.postComment(${postId}, ${parentId})">Post</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('modal').classList.add('active');
|
|
}
|
|
|
|
async postComment(postId, parentId) {
|
|
const content = document.getElementById('comment-content').value.trim();
|
|
|
|
if (!content) {
|
|
alert('Please enter a comment');
|
|
return;
|
|
}
|
|
|
|
if (content.length < 1) {
|
|
alert('Comment cannot be empty');
|
|
return;
|
|
}
|
|
|
|
const result = await this.api('/comment', {
|
|
post_id: postId,
|
|
parent_id: parentId,
|
|
content
|
|
});
|
|
|
|
if (result.success) {
|
|
this.closeModal();
|
|
this.showPost(postId);
|
|
} else {
|
|
alert(result.error || 'Failed to post comment');
|
|
}
|
|
}
|
|
|
|
async changeCommentSort(postId, sort) {
|
|
this.currentCommentSort = sort;
|
|
const commentsResult = await this.api(`/comments?post_id=${postId}&sort=${sort}`);
|
|
const container = document.getElementById('comments-container');
|
|
container.innerHTML = '';
|
|
this.renderComments(commentsResult.comments, container);
|
|
|
|
document.querySelectorAll('.comment-sort-tab').forEach(tab => tab.classList.remove('active'));
|
|
const activeTab = [...document.querySelectorAll('.comment-sort-tab')].find(tab => tab.dataset.sort === sort);
|
|
if (activeTab) activeTab.classList.add('active');
|
|
}
|
|
|
|
async vote(targetType, targetId, voteValue) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to vote');
|
|
return;
|
|
}
|
|
|
|
await this.api('/vote', {
|
|
target_type: targetType,
|
|
target_id: targetId,
|
|
vote: voteValue
|
|
});
|
|
|
|
if (this.currentPost) {
|
|
this.showPost(this.currentPost.id);
|
|
} else {
|
|
this.loadFeed();
|
|
}
|
|
}
|
|
|
|
async showCommunity(communityId, fromPopState = false) {
|
|
this.currentCommunity = communityId;
|
|
this.currentView = 'community';
|
|
this.currentPost = null;
|
|
this.currentCommentSort = 'best';
|
|
this.currentSort = 'hot';
|
|
this.currentTimeFilter = 'all';
|
|
|
|
if (!fromPopState) {
|
|
this.updateURL({view: 'community', communityId});
|
|
}
|
|
|
|
const result = await this.api(`/community?id=${communityId}`);
|
|
|
|
if (result.success) {
|
|
const community = result.community;
|
|
const feed = document.getElementById('feed-content');
|
|
|
|
const infoCard = document.createElement('div');
|
|
infoCard.className = 'sidebar-card';
|
|
infoCard.innerHTML = `
|
|
<h2>${community.name}</h2>
|
|
<p>${community.description || 'No description'}</p>
|
|
${this.currentUser ? `<button class="btn btn-primary" onclick="document.app.toggleSubscription(${community.id})">Subscribe</button>` : ''}
|
|
${community.sidebar ? `<div style="margin-top:10px;">${this.renderMarkdown(community.sidebar)}</div>` : ''}
|
|
`;
|
|
|
|
feed.innerHTML = '';
|
|
feed.appendChild(infoCard);
|
|
}
|
|
|
|
this.loadFeed(true);
|
|
}
|
|
|
|
async toggleSubscription(communityId) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to subscribe');
|
|
return;
|
|
}
|
|
|
|
await this.api('/subscribe', {
|
|
community_id: communityId,
|
|
action: 'subscribe'
|
|
});
|
|
|
|
this.loadSubscriptions();
|
|
this.loadFeed(false);
|
|
}
|
|
|
|
showHome(fromPopState = false) {
|
|
this.currentCommunity = null;
|
|
this.currentPost = null;
|
|
this.currentView = 'home';
|
|
this.currentCommentSort = 'best';
|
|
this.currentSort = 'hot';
|
|
this.currentTimeFilter = 'all';
|
|
|
|
if (!fromPopState) {
|
|
this.updateURL({}, false);
|
|
}
|
|
|
|
this.loadFeed();
|
|
}
|
|
|
|
changeSort(sort) {
|
|
this.currentSort = sort;
|
|
|
|
document.querySelectorAll('.post-sort-tab').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
if (tab.dataset.sort === sort) {
|
|
tab.classList.add('active');
|
|
}
|
|
});
|
|
|
|
const timeFilters = document.getElementById('time-filters');
|
|
if (sort === 'top') {
|
|
timeFilters.classList.remove('hidden');
|
|
} else {
|
|
timeFilters.classList.add('hidden');
|
|
this.currentTimeFilter = 'all';
|
|
}
|
|
|
|
this.loadFeed();
|
|
}
|
|
|
|
changeTimeFilter(time) {
|
|
this.currentTimeFilter = time;
|
|
|
|
document.querySelectorAll('.time-filter').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
if (btn.dataset.time === time) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
this.loadFeed();
|
|
}
|
|
|
|
async search() {
|
|
const query = document.getElementById('search-input').value;
|
|
if (!query) return;
|
|
|
|
const result = await this.api(`/search?q=${encodeURIComponent(query)}`);
|
|
|
|
const feed = document.getElementById('feed-content');
|
|
feed.innerHTML = '<h2>Search Results</h2>';
|
|
|
|
if (result.posts.length > 0) {
|
|
const postsHeader = document.createElement('h3');
|
|
postsHeader.textContent = 'Posts';
|
|
feed.appendChild(postsHeader);
|
|
this.renderPosts(result.posts);
|
|
}
|
|
|
|
if (result.communities.length > 0) {
|
|
const commHeader = document.createElement('h3');
|
|
commHeader.textContent = 'Communities';
|
|
feed.appendChild(commHeader);
|
|
|
|
result.communities.forEach(community => {
|
|
const commEl = document.createElement('div');
|
|
commEl.className = 'sidebar-card';
|
|
commEl.innerHTML = `
|
|
<h3 onclick="document.app.showCommunity(${community.id})" style="cursor:pointer;">${community.name}</h3>
|
|
<p>${community.description}</p>
|
|
`;
|
|
feed.appendChild(commEl);
|
|
});
|
|
}
|
|
}
|
|
|
|
async showProfile(username, fromPopState = false) {
|
|
const result = await this.api(`/user?username=${username}`);
|
|
|
|
if (!result.success) {
|
|
alert('User not found');
|
|
return;
|
|
}
|
|
|
|
this.currentView = 'profile';
|
|
this.currentCommunity = null;
|
|
this.currentPost = null;
|
|
this.currentCommentSort = 'best';
|
|
|
|
if (!fromPopState) {
|
|
this.updateURL({view: 'profile', username});
|
|
}
|
|
|
|
const user = result.user;
|
|
const feed = document.getElementById('feed-content');
|
|
feed.innerHTML = '';
|
|
|
|
const profileCard = document.createElement('div');
|
|
profileCard.className = 'sidebar-card';
|
|
profileCard.innerHTML = `
|
|
<h2>${user.username}</h2>
|
|
<p>${user.karma} karma</p>
|
|
<p>Member since ${this.formatTime(user.created_at)}</p>
|
|
${this.currentUser && this.currentUser.username !== username ? `<button class="btn btn-primary" onclick="document.app.showSendMessage('${username}')">Send Message</button>` : ''}
|
|
`;
|
|
feed.appendChild(profileCard);
|
|
|
|
if (result.posts.length > 0) {
|
|
const postsHeader = document.createElement('h3');
|
|
postsHeader.textContent = 'Recent Posts';
|
|
feed.appendChild(postsHeader);
|
|
this.renderPosts(result.posts, true);
|
|
}
|
|
}
|
|
|
|
showSendMessage(toUsername) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to send messages');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('modal-title').textContent = 'Send Message';
|
|
document.getElementById('modal-body').innerHTML = `
|
|
<div class="form-group">
|
|
<label>To</label>
|
|
<input type="text" id="message-to" value="${toUsername}" readonly>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Subject</label>
|
|
<input type="text" id="message-subject">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Message</label>
|
|
<textarea id="message-content"></textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="btn btn-secondary" onclick="document.app.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="document.app.sendMessage()">Send</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('modal').classList.add('active');
|
|
}
|
|
|
|
async sendMessage() {
|
|
const to_username = document.getElementById('message-to').value.trim();
|
|
const subject = document.getElementById('message-subject').value.trim();
|
|
const content = document.getElementById('message-content').value.trim();
|
|
|
|
if (!to_username || !subject || !content) {
|
|
alert('Please fill in all fields');
|
|
return;
|
|
}
|
|
|
|
if (subject.length < 1) {
|
|
alert('Subject cannot be empty');
|
|
return;
|
|
}
|
|
|
|
if (content.length < 1) {
|
|
alert('Message cannot be empty');
|
|
return;
|
|
}
|
|
|
|
const result = await this.api('/message', {
|
|
to_username,
|
|
subject,
|
|
content
|
|
});
|
|
|
|
if (result.success) {
|
|
alert('Message sent!');
|
|
this.closeModal();
|
|
} else {
|
|
alert(result.error || 'Failed to send message');
|
|
}
|
|
}
|
|
|
|
async showMessages(fromPopState = false) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to view messages');
|
|
return;
|
|
}
|
|
|
|
this.currentView = 'messages';
|
|
this.currentCommunity = null;
|
|
this.currentPost = null;
|
|
this.currentCommentSort = 'best';
|
|
|
|
if (!fromPopState) {
|
|
this.updateURL({view: 'messages'});
|
|
}
|
|
|
|
const result = await this.api(`/messages`);
|
|
const feed = document.getElementById('feed-content');
|
|
feed.innerHTML = '<h2>Messages</h2>';
|
|
|
|
result.messages.forEach(message => {
|
|
const msgEl = document.createElement('div');
|
|
msgEl.className = 'message-item';
|
|
msgEl.innerHTML = `
|
|
<div class="message-header">From ${message.from_username} • ${this.formatTime(message.created_at)}</div>
|
|
<div class="message-subject">${message.subject}</div>
|
|
<div class="message-body">${message.content}</div>
|
|
<button class="btn btn-secondary" onclick="document.app.showSendMessage('${message.from_username}')">Reply</button>
|
|
`;
|
|
feed.appendChild(msgEl);
|
|
});
|
|
}
|
|
|
|
async savePost(postId) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to save posts');
|
|
return;
|
|
}
|
|
|
|
await this.api('/save', {
|
|
target_type: 'post',
|
|
target_id: postId,
|
|
action: 'save'
|
|
});
|
|
|
|
alert('Post saved!');
|
|
}
|
|
|
|
async hidePost(postId) {
|
|
if (!this.currentUser) {
|
|
alert('Please login to hide posts');
|
|
return;
|
|
}
|
|
|
|
await this.api('/hide', {
|
|
post_id: postId,
|
|
action: 'hide'
|
|
});
|
|
|
|
this.loadFeed();
|
|
}
|
|
|
|
sharePost(postId) {
|
|
const url = `${document.location.origin}/?post=${postId}`;
|
|
prompt('Share this link:', url);
|
|
}
|
|
|
|
renderMarkdown(text) {
|
|
let html = text;
|
|
|
|
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
html = html.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
|
|
html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
|
|
return html;
|
|
}
|
|
|
|
formatTime(timestamp) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const diff = now - timestamp;
|
|
|
|
if (diff < 60) return `${diff}s ago`;
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
if (diff < 2592000) return `${Math.floor(diff / 86400)}d ago`;
|
|
if (diff < 31536000) return `${Math.floor(diff / 2592000)}mo ago`;
|
|
return `${Math.floor(diff / 31536000)}y ago`;
|
|
}
|
|
|
|
closeModal(event) {
|
|
if (!event || event.target.id === 'modal') {
|
|
document.getElementById('modal').classList.remove('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
const app = new App();
|
|
document.app = app;
|
|
</script>
|
|
<div class="header">
|
|
<h1 onclick="document.app.showHome()">OnFiRe</h1>
|
|
<div class="header-nav">
|
|
<span id="user-info" class="hidden"></span>
|
|
<button id="login-btn" onclick="document.app.showLogin()">Login</button>
|
|
<button id="register-btn" onclick="document.app.showRegister()">Register</button>
|
|
<button id="logout-btn" class="hidden" onclick="document.app.logout()">Logout</button>
|
|
<button id="create-community-btn" class="hidden" onclick="document.app.showCreateCommunity()">Create Community</button>
|
|
<button id="create-post-btn" class="hidden" onclick="document.app.showCreatePost()">Create Post</button>
|
|
<button id="messages-btn" class="hidden" onclick="document.app.showMessages()">Messages</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="main-content">
|
|
<div class="sidebar">
|
|
<div class="sidebar-card">
|
|
<h3>Communities</h3>
|
|
<ul class="sidebar-list" id="communities-list"></ul>
|
|
</div>
|
|
<div class="sidebar-card hidden" id="subscriptions-card">
|
|
<h3>My Subscriptions</h3>
|
|
<ul class="sidebar-list" id="subscriptions-list"></ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feed">
|
|
<div class="search-bar">
|
|
<input type="text" id="search-input" placeholder="Search...">
|
|
<button class="btn btn-primary" onclick="document.app.search()">Search</button>
|
|
</div>
|
|
|
|
<div class="sort-tabs" id="post-sort-tabs">
|
|
<button class="post-sort-tab active" data-sort="hot" onclick="document.app.changeSort('hot')">Hot</button>
|
|
<button class="post-sort-tab" data-sort="new" onclick="document.app.changeSort('new')">New</button>
|
|
<button class="post-sort-tab" data-sort="top" onclick="document.app.changeSort('top')">Top</button>
|
|
<button class="post-sort-tab" data-sort="rising" onclick="document.app.changeSort('rising')">Rising</button>
|
|
<button class="post-sort-tab" data-sort="controversial" onclick="document.app.changeSort('controversial')">Controversial</button>
|
|
<div class="time-filters hidden" id="time-filters">
|
|
<button class="time-filter active" data-time="all" onclick="document.app.changeTimeFilter('all')">All</button>
|
|
<button class="time-filter" data-time="year" onclick="document.app.changeTimeFilter('year')">Year</button>
|
|
<button class="time-filter" data-time="month" onclick="document.app.changeTimeFilter('month')">Month</button>
|
|
<button class="time-filter" data-time="week" onclick="document.app.changeTimeFilter('week')">Week</button>
|
|
<button class="time-filter" data-time="day" onclick="document.app.changeTimeFilter('day')">Day</button>
|
|
<button class="time-filter" data-time="hour" onclick="document.app.changeTimeFilter('hour')">Hour</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="feed-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="modal" class="modal" onclick="document.app.closeModal(event)">
|
|
<div class="modal-content" onclick="event.stopPropagation()">
|
|
<div class="modal-header" id="modal-title"></div>
|
|
<div id="modal-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|